浏览代码

Merge pull request #6931 from open-webui/groups

feat: user groups
Timothy Jaeryang Baek 5 月之前
父节点
当前提交
034674c19c
共有 100 个文件被更改,包括 7208 次插入4260 次删除
  1. 83 73
      backend/open_webui/apps/ollama/main.py
  2. 60 49
      backend/open_webui/apps/openai/main.py
  3. 16 7
      backend/open_webui/apps/retrieval/utils.py
  4. 15 7
      backend/open_webui/apps/webui/main.py
  5. 0 157
      backend/open_webui/apps/webui/models/documents.py
  6. 181 0
      backend/open_webui/apps/webui/models/groups.py
  7. 55 9
      backend/open_webui/apps/webui/models/knowledge.py
  8. 87 2
      backend/open_webui/apps/webui/models/models.py
  9. 39 5
      backend/open_webui/apps/webui/models/prompts.py
  10. 38 1
      backend/open_webui/apps/webui/models/tools.py
  11. 95 31
      backend/open_webui/apps/webui/routers/auths.py
  12. 9 5
      backend/open_webui/apps/webui/routers/chats.py
  13. 120 0
      backend/open_webui/apps/webui/routers/groups.py
  14. 192 77
      backend/open_webui/apps/webui/routers/knowledge.py
  15. 105 36
      backend/open_webui/apps/webui/routers/models.py
  16. 52 5
      backend/open_webui/apps/webui/routers/prompts.py
  17. 128 78
      backend/open_webui/apps/webui/routers/tools.py
  18. 41 4
      backend/open_webui/apps/webui/routers/users.py
  19. 1 1
      backend/open_webui/apps/webui/utils.py
  20. 39 20
      backend/open_webui/config.py
  21. 257 191
      backend/open_webui/main.py
  22. 85 0
      backend/open_webui/migrations/versions/922e7a387820_add_group_table.py
  23. 95 0
      backend/open_webui/utils/access_control.py
  24. 22 12
      backend/open_webui/utils/tools.py
  25. 7 2
      backend/open_webui/utils/utils.py
  26. 1 0
      backend/requirements.txt
  27. 1 0
      pyproject.toml
  28. 163 0
      src/lib/apis/groups/index.ts
  29. 6 20
      src/lib/apis/index.ts
  30. 38 4
      src/lib/apis/knowledge/index.ts
  31. 84 9
      src/lib/apis/models/index.ts
  32. 4 2
      src/lib/apis/ollama/index.ts
  33. 52 13
      src/lib/apis/prompts/index.ts
  34. 33 0
      src/lib/apis/tools/index.ts
  35. 34 4
      src/lib/apis/users/index.ts
  36. 15 28
      src/lib/components/admin/Functions.svelte
  37. 1 1
      src/lib/components/admin/Functions/FunctionEditor.svelte
  38. 0 0
      src/lib/components/admin/Functions/FunctionMenu.svelte
  39. 1 1
      src/lib/components/admin/Settings.svelte
  40. 1 0
      src/lib/components/admin/Settings/Connections.svelte
  41. 1019 0
      src/lib/components/admin/Settings/Connections/ManageOllamaModal.svelte
  42. 20 1
      src/lib/components/admin/Settings/Connections/OllamaConnection.svelte
  43. 5 12
      src/lib/components/admin/Settings/Documents.svelte
  44. 279 1018
      src/lib/components/admin/Settings/Models.svelte
  45. 0 214
      src/lib/components/admin/Settings/Users.svelte
  46. 28 4
      src/lib/components/admin/Users.svelte
  47. 117 3
      src/lib/components/admin/Users/Groups.svelte
  48. 149 0
      src/lib/components/admin/Users/Groups/AddGroupModal.svelte
  49. 61 0
      src/lib/components/admin/Users/Groups/Display.svelte
  50. 328 0
      src/lib/components/admin/Users/Groups/EditGroupModal.svelte
  51. 84 0
      src/lib/components/admin/Users/Groups/GroupItem.svelte
  52. 204 0
      src/lib/components/admin/Users/Groups/Permissions.svelte
  53. 122 0
      src/lib/components/admin/Users/Groups/Users.svelte
  54. 287 301
      src/lib/components/admin/Users/UserList.svelte
  55. 27 18
      src/lib/components/chat/Chat.svelte
  56. 550 349
      src/lib/components/chat/MessageInput.svelte
  57. 2 2
      src/lib/components/chat/MessageInput/Commands.svelte
  58. 19 18
      src/lib/components/chat/MessageInput/InputMenu.svelte
  59. 10 12
      src/lib/components/chat/Messages/ResponseMessage.svelte
  60. 1 1
      src/lib/components/chat/ModelSelector.svelte
  61. 10 23
      src/lib/components/chat/ModelSelector/Selector.svelte
  62. 1 2
      src/lib/components/chat/Placeholder.svelte
  63. 2 4
      src/lib/components/common/Switch.svelte
  64. 19 0
      src/lib/components/icons/LockClosed.svelte
  65. 11 0
      src/lib/components/icons/UserCircleSolid.svelte
  66. 9 0
      src/lib/components/icons/UserPlusSolid.svelte
  67. 9 0
      src/lib/components/icons/UsersSolid.svelte
  68. 20 0
      src/lib/components/icons/Wrench.svelte
  69. 1 1
      src/lib/components/layout/Sidebar.svelte
  70. 113 101
      src/lib/components/workspace/Knowledge.svelte
  71. 16 3
      src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte
  72. 78 44
      src/lib/components/workspace/Knowledge/KnowledgeBase.svelte
  73. 0 0
      src/lib/components/workspace/Knowledge/KnowledgeBase/AddContentMenu.svelte
  74. 0 0
      src/lib/components/workspace/Knowledge/KnowledgeBase/AddTextContentModal.svelte
  75. 0 0
      src/lib/components/workspace/Knowledge/KnowledgeBase/Files.svelte
  76. 288 489
      src/lib/components/workspace/Models.svelte
  77. 1 1
      src/lib/components/workspace/Models/Knowledge.svelte
  78. 94 57
      src/lib/components/workspace/Models/ModelEditor.svelte
  79. 1 64
      src/lib/components/workspace/Models/ModelMenu.svelte
  80. 240 231
      src/lib/components/workspace/Prompts.svelte
  81. 32 4
      src/lib/components/workspace/Prompts/PromptEditor.svelte
  82. 306 302
      src/lib/components/workspace/Tools.svelte
  83. 30 13
      src/lib/components/workspace/Tools/ToolkitEditor.svelte
  84. 193 0
      src/lib/components/workspace/common/AccessControl.svelte
  85. 43 0
      src/lib/components/workspace/common/AccessControlModal.svelte
  86. 1 1
      src/routes/(app)/+layout.svelte
  87. 7 0
      src/routes/(app)/admin/+layout.svelte
  88. 1 1
      src/routes/(app)/admin/functions/+page.svelte
  89. 1 1
      src/routes/(app)/admin/functions/create/+page.svelte
  90. 1 1
      src/routes/(app)/admin/functions/edit/+page.svelte
  91. 59 49
      src/routes/(app)/workspace/+layout.svelte
  92. 2 2
      src/routes/(app)/workspace/knowledge/+page.svelte
  93. 2 2
      src/routes/(app)/workspace/knowledge/[id]/+page.svelte
  94. 2 2
      src/routes/(app)/workspace/knowledge/create/+page.svelte
  95. 5 2
      src/routes/(app)/workspace/models/create/+page.svelte
  96. 6 3
      src/routes/(app)/workspace/models/edit/+page.svelte
  97. 1 15
      src/routes/(app)/workspace/prompts/+page.svelte
  98. 14 11
      src/routes/(app)/workspace/prompts/create/+page.svelte
  99. 20 11
      src/routes/(app)/workspace/prompts/edit/+page.svelte
  100. 1 13
      src/routes/(app)/workspace/tools/+page.svelte

+ 83 - 73
backend/open_webui/apps/ollama/main.py

@@ -13,9 +13,7 @@ import requests
 from open_webui.apps.webui.models.models import Models
 from open_webui.apps.webui.models.models import Models
 from open_webui.config import (
 from open_webui.config import (
     CORS_ALLOW_ORIGIN,
     CORS_ALLOW_ORIGIN,
-    ENABLE_MODEL_FILTER,
     ENABLE_OLLAMA_API,
     ENABLE_OLLAMA_API,
-    MODEL_FILTER_LIST,
     OLLAMA_BASE_URLS,
     OLLAMA_BASE_URLS,
     OLLAMA_API_CONFIGS,
     OLLAMA_API_CONFIGS,
     UPLOAD_DIR,
     UPLOAD_DIR,
@@ -66,32 +64,16 @@ app.add_middleware(
 
 
 app.state.config = AppConfig()
 app.state.config = AppConfig()
 
 
-app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
-app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
-
 app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
 app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
 app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
 app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
 app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
 app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
 
 
-app.state.MODELS = {}
-
 
 
 # TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances.
 # TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances.
 # Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin,
 # Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin,
 # least connections, or least response time for better resource utilization and performance optimization.
 # least connections, or least response time for better resource utilization and performance optimization.
 
 
 
 
-@app.middleware("http")
-async def check_url(request: Request, call_next):
-    if len(app.state.MODELS) == 0:
-        await get_all_models()
-    else:
-        pass
-
-    response = await call_next(request)
-    return response
-
-
 @app.head("/")
 @app.head("/")
 @app.get("/")
 @app.get("/")
 async def get_status():
 async def get_status():
@@ -326,8 +308,6 @@ async def get_all_models():
     else:
     else:
         models = {"models": []}
         models = {"models": []}
 
 
-    app.state.MODELS = {model["model"]: model for model in models["models"]}
-
     return models
     return models
 
 
 
 
@@ -339,16 +319,18 @@ async def get_ollama_tags(
     if url_idx is None:
     if url_idx is None:
         models = await get_all_models()
         models = await get_all_models()
 
 
-        if app.state.config.ENABLE_MODEL_FILTER:
-            if user.role == "user":
-                models["models"] = list(
-                    filter(
-                        lambda model: model["name"]
-                        in app.state.config.MODEL_FILTER_LIST,
-                        models["models"],
-                    )
-                )
-                return models
+        # TODO: Check User Group and Filter Models
+        # if app.state.config.ENABLE_MODEL_FILTER:
+        #     if user.role == "user":
+        #         models["models"] = list(
+        #             filter(
+        #                 lambda model: model["name"]
+        #                 in app.state.config.MODEL_FILTER_LIST,
+        #                 models["models"],
+        #             )
+        #         )
+        #         return models
+
         return models
         return models
     else:
     else:
         url = app.state.config.OLLAMA_BASE_URLS[url_idx]
         url = app.state.config.OLLAMA_BASE_URLS[url_idx]
@@ -473,8 +455,11 @@ async def push_model(
     user=Depends(get_admin_user),
     user=Depends(get_admin_user),
 ):
 ):
     if url_idx is None:
     if url_idx is None:
-        if form_data.name in app.state.MODELS:
-            url_idx = app.state.MODELS[form_data.name]["urls"][0]
+        model_list = await get_all_models()
+        models = {model["model"]: model for model in model_list["models"]}
+
+        if form_data.name in models:
+            url_idx = models[form_data.name]["urls"][0]
         else:
         else:
             raise HTTPException(
             raise HTTPException(
                 status_code=400,
                 status_code=400,
@@ -523,8 +508,11 @@ async def copy_model(
     user=Depends(get_admin_user),
     user=Depends(get_admin_user),
 ):
 ):
     if url_idx is None:
     if url_idx is None:
-        if form_data.source in app.state.MODELS:
-            url_idx = app.state.MODELS[form_data.source]["urls"][0]
+        model_list = await get_all_models()
+        models = {model["model"]: model for model in model_list["models"]}
+
+        if form_data.source in models:
+            url_idx = models[form_data.source]["urls"][0]
         else:
         else:
             raise HTTPException(
             raise HTTPException(
                 status_code=400,
                 status_code=400,
@@ -579,8 +567,11 @@ async def delete_model(
     user=Depends(get_admin_user),
     user=Depends(get_admin_user),
 ):
 ):
     if url_idx is None:
     if url_idx is None:
-        if form_data.name in app.state.MODELS:
-            url_idx = app.state.MODELS[form_data.name]["urls"][0]
+        model_list = await get_all_models()
+        models = {model["model"]: model for model in model_list["models"]}
+
+        if form_data.name in models:
+            url_idx = models[form_data.name]["urls"][0]
         else:
         else:
             raise HTTPException(
             raise HTTPException(
                 status_code=400,
                 status_code=400,
@@ -628,13 +619,16 @@ async def delete_model(
 
 
 @app.post("/api/show")
 @app.post("/api/show")
 async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
 async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
-    if form_data.name not in app.state.MODELS:
+    model_list = await get_all_models()
+    models = {model["model"]: model for model in model_list["models"]}
+
+    if form_data.name not in models:
         raise HTTPException(
         raise HTTPException(
             status_code=400,
             status_code=400,
             detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
             detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
         )
         )
 
 
-    url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
+    url_idx = random.choice(models[form_data.name]["urls"])
     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
     log.info(f"url: {url}")
 
 
@@ -704,23 +698,26 @@ async def generate_embeddings(
     url_idx: Optional[int] = None,
     url_idx: Optional[int] = None,
     user=Depends(get_verified_user),
     user=Depends(get_verified_user),
 ):
 ):
-    return generate_ollama_embeddings(form_data=form_data, url_idx=url_idx)
+    return await generate_ollama_embeddings(form_data=form_data, url_idx=url_idx)
 
 
 
 
-def generate_ollama_embeddings(
+async def generate_ollama_embeddings(
     form_data: GenerateEmbeddingsForm,
     form_data: GenerateEmbeddingsForm,
     url_idx: Optional[int] = None,
     url_idx: Optional[int] = None,
 ):
 ):
     log.info(f"generate_ollama_embeddings {form_data}")
     log.info(f"generate_ollama_embeddings {form_data}")
 
 
     if url_idx is None:
     if url_idx is None:
+        model_list = await get_all_models()
+        models = {model["model"]: model for model in model_list["models"]}
+
         model = form_data.model
         model = form_data.model
 
 
         if ":" not in model:
         if ":" not in model:
             model = f"{model}:latest"
             model = f"{model}:latest"
 
 
-        if model in app.state.MODELS:
-            url_idx = random.choice(app.state.MODELS[model]["urls"])
+        if model in models:
+            url_idx = random.choice(models[model]["urls"])
         else:
         else:
             raise HTTPException(
             raise HTTPException(
                 status_code=400,
                 status_code=400,
@@ -771,20 +768,23 @@ def generate_ollama_embeddings(
         )
         )
 
 
 
 
-def generate_ollama_batch_embeddings(
+async def generate_ollama_batch_embeddings(
     form_data: GenerateEmbedForm,
     form_data: GenerateEmbedForm,
     url_idx: Optional[int] = None,
     url_idx: Optional[int] = None,
 ):
 ):
     log.info(f"generate_ollama_batch_embeddings {form_data}")
     log.info(f"generate_ollama_batch_embeddings {form_data}")
 
 
     if url_idx is None:
     if url_idx is None:
+        model_list = await get_all_models()
+        models = {model["model"]: model for model in model_list["models"]}
+
         model = form_data.model
         model = form_data.model
 
 
         if ":" not in model:
         if ":" not in model:
             model = f"{model}:latest"
             model = f"{model}:latest"
 
 
-        if model in app.state.MODELS:
-            url_idx = random.choice(app.state.MODELS[model]["urls"])
+        if model in models:
+            url_idx = random.choice(models[model]["urls"])
         else:
         else:
             raise HTTPException(
             raise HTTPException(
                 status_code=400,
                 status_code=400,
@@ -854,13 +854,16 @@ async def generate_completion(
     user=Depends(get_verified_user),
     user=Depends(get_verified_user),
 ):
 ):
     if url_idx is None:
     if url_idx is None:
+        model_list = await get_all_models()
+        models = {model["model"]: model for model in model_list["models"]}
+
         model = form_data.model
         model = form_data.model
 
 
         if ":" not in model:
         if ":" not in model:
             model = f"{model}:latest"
             model = f"{model}:latest"
 
 
-        if model in app.state.MODELS:
-            url_idx = random.choice(app.state.MODELS[model]["urls"])
+        if model in models:
+            url_idx = random.choice(models[model]["urls"])
         else:
         else:
             raise HTTPException(
             raise HTTPException(
                 status_code=400,
                 status_code=400,
@@ -895,14 +898,17 @@ class GenerateChatCompletionForm(BaseModel):
     keep_alive: Optional[Union[int, str]] = None
     keep_alive: Optional[Union[int, str]] = None
 
 
 
 
-def get_ollama_url(url_idx: Optional[int], model: str):
+async def get_ollama_url(url_idx: Optional[int], model: str):
     if url_idx is None:
     if url_idx is None:
-        if model not in app.state.MODELS:
+        model_list = await get_all_models()
+        models = {model["model"]: model for model in model_list["models"]}
+
+        if model not in models:
             raise HTTPException(
             raise HTTPException(
                 status_code=400,
                 status_code=400,
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
             )
             )
-        url_idx = random.choice(app.state.MODELS[model]["urls"])
+        url_idx = random.choice(models[model]["urls"])
     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     return url
     return url
 
 
@@ -922,12 +928,14 @@ async def generate_chat_completion(
 
 
     model_id = form_data.model
     model_id = form_data.model
 
 
-    if not bypass_filter and app.state.config.ENABLE_MODEL_FILTER:
-        if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
-            raise HTTPException(
-                status_code=403,
-                detail="Model not found",
-            )
+    # TODO: Check User Group and Filter Models
+    # if not bypass_filter:
+    #     if app.state.config.ENABLE_MODEL_FILTER:
+    #         if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
+    #             raise HTTPException(
+    #                 status_code=403,
+    #                 detail="Model not found",
+    #             )
 
 
     model_info = Models.get_model_by_id(model_id)
     model_info = Models.get_model_by_id(model_id)
 
 
@@ -949,7 +957,7 @@ async def generate_chat_completion(
     if ":" not in payload["model"]:
     if ":" not in payload["model"]:
         payload["model"] = f"{payload['model']}:latest"
         payload["model"] = f"{payload['model']}:latest"
 
 
-    url = get_ollama_url(url_idx, payload["model"])
+    url = await get_ollama_url(url_idx, payload["model"])
     log.info(f"url: {url}")
     log.info(f"url: {url}")
     log.debug(f"generate_chat_completion() - 2.payload = {payload}")
     log.debug(f"generate_chat_completion() - 2.payload = {payload}")
 
 
@@ -1008,12 +1016,13 @@ async def generate_openai_chat_completion(
 
 
     model_id = completion_form.model
     model_id = completion_form.model
 
 
-    if app.state.config.ENABLE_MODEL_FILTER:
-        if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
-            raise HTTPException(
-                status_code=403,
-                detail="Model not found",
-            )
+    # TODO: Check User Group and Filter Models
+    # if app.state.config.ENABLE_MODEL_FILTER:
+    #     if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
+    #         raise HTTPException(
+    #             status_code=403,
+    #             detail="Model not found",
+    #         )
 
 
     model_info = Models.get_model_by_id(model_id)
     model_info = Models.get_model_by_id(model_id)
 
 
@@ -1030,7 +1039,7 @@ async def generate_openai_chat_completion(
     if ":" not in payload["model"]:
     if ":" not in payload["model"]:
         payload["model"] = f"{payload['model']}:latest"
         payload["model"] = f"{payload['model']}:latest"
 
 
-    url = get_ollama_url(url_idx, payload["model"])
+    url = await get_ollama_url(url_idx, payload["model"])
     log.info(f"url: {url}")
     log.info(f"url: {url}")
 
 
     api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
     api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
@@ -1054,15 +1063,16 @@ async def get_openai_models(
     if url_idx is None:
     if url_idx is None:
         models = await get_all_models()
         models = await get_all_models()
 
 
-        if app.state.config.ENABLE_MODEL_FILTER:
-            if user.role == "user":
-                models["models"] = list(
-                    filter(
-                        lambda model: model["name"]
-                        in app.state.config.MODEL_FILTER_LIST,
-                        models["models"],
-                    )
-                )
+        # TODO: Check User Group and Filter Models
+        # if app.state.config.ENABLE_MODEL_FILTER:
+        #     if user.role == "user":
+        #         models["models"] = list(
+        #             filter(
+        #                 lambda model: model["name"]
+        #                 in app.state.config.MODEL_FILTER_LIST,
+        #                 models["models"],
+        #             )
+        #         )
 
 
         return {
         return {
             "data": [
             "data": [

+ 60 - 49
backend/open_webui/apps/openai/main.py

@@ -11,9 +11,7 @@ from open_webui.apps.webui.models.models import Models
 from open_webui.config import (
 from open_webui.config import (
     CACHE_DIR,
     CACHE_DIR,
     CORS_ALLOW_ORIGIN,
     CORS_ALLOW_ORIGIN,
-    ENABLE_MODEL_FILTER,
     ENABLE_OPENAI_API,
     ENABLE_OPENAI_API,
-    MODEL_FILTER_LIST,
     OPENAI_API_BASE_URLS,
     OPENAI_API_BASE_URLS,
     OPENAI_API_KEYS,
     OPENAI_API_KEYS,
     OPENAI_API_CONFIGS,
     OPENAI_API_CONFIGS,
@@ -39,6 +37,8 @@ from open_webui.utils.payload import (
 )
 )
 
 
 from open_webui.utils.utils import get_admin_user, get_verified_user
 from open_webui.utils.utils import get_admin_user, get_verified_user
+from open_webui.utils.access_control import has_access
+
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["OPENAI"])
 log.setLevel(SRC_LOG_LEVELS["OPENAI"])
@@ -61,25 +61,11 @@ app.add_middleware(
 
 
 app.state.config = AppConfig()
 app.state.config = AppConfig()
 
 
-app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
-app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
-
 app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
 app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
 app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
 app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
 app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
 app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
 app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
 app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
 
 
-app.state.MODELS = {}
-
-
-@app.middleware("http")
-async def check_url(request: Request, call_next):
-    if len(app.state.MODELS) == 0:
-        await get_all_models()
-
-    response = await call_next(request)
-    return response
-
 
 
 @app.get("/config")
 @app.get("/config")
 async def get_config(user=Depends(get_admin_user)):
 async def get_config(user=Depends(get_admin_user)):
@@ -264,7 +250,7 @@ def merge_models_lists(model_lists):
     return merged_list
     return merged_list
 
 
 
 
-async def get_all_models_raw() -> list:
+async def get_all_models_responses() -> list:
     if not app.state.config.ENABLE_OPENAI_API:
     if not app.state.config.ENABLE_OPENAI_API:
         return []
         return []
 
 
@@ -335,22 +321,13 @@ async def get_all_models_raw() -> list:
     return responses
     return responses
 
 
 
 
-@overload
-async def get_all_models(raw: Literal[True]) -> list: ...
-
-
-@overload
-async def get_all_models(raw: Literal[False] = False) -> dict[str, list]: ...
-
-
-async def get_all_models(raw=False) -> dict[str, list] | list:
+async def get_all_models() -> dict[str, list]:
     log.info("get_all_models()")
     log.info("get_all_models()")
+
     if not app.state.config.ENABLE_OPENAI_API:
     if not app.state.config.ENABLE_OPENAI_API:
-        return [] if raw else {"data": []}
+        return {"data": []}
 
 
-    responses = await get_all_models_raw()
-    if raw:
-        return responses
+    responses = await get_all_models_responses()
 
 
     def extract_data(response):
     def extract_data(response):
         if response and "data" in response:
         if response and "data" in response:
@@ -360,9 +337,7 @@ async def get_all_models(raw=False) -> dict[str, list] | list:
         return None
         return None
 
 
     models = {"data": merge_models_lists(map(extract_data, responses))}
     models = {"data": merge_models_lists(map(extract_data, responses))}
-
     log.debug(f"models: {models}")
     log.debug(f"models: {models}")
-    app.state.MODELS = {model["id"]: model for model in models["data"]}
 
 
     return models
     return models
 
 
@@ -370,18 +345,12 @@ async def get_all_models(raw=False) -> dict[str, list] | list:
 @app.get("/models")
 @app.get("/models")
 @app.get("/models/{url_idx}")
 @app.get("/models/{url_idx}")
 async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
 async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
+    models = {
+        "data": [],
+    }
+
     if url_idx is None:
     if url_idx is None:
         models = await get_all_models()
         models = await get_all_models()
-        if app.state.config.ENABLE_MODEL_FILTER:
-            if user.role == "user":
-                models["data"] = list(
-                    filter(
-                        lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
-                        models["data"],
-                    )
-                )
-                return models
-        return models
     else:
     else:
         url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
         url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
         key = app.state.config.OPENAI_API_KEYS[url_idx]
         key = app.state.config.OPENAI_API_KEYS[url_idx]
@@ -389,6 +358,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
         headers = {}
         headers = {}
         headers["Authorization"] = f"Bearer {key}"
         headers["Authorization"] = f"Bearer {key}"
         headers["Content-Type"] = "application/json"
         headers["Content-Type"] = "application/json"
+
         if ENABLE_FORWARD_USER_INFO_HEADERS:
         if ENABLE_FORWARD_USER_INFO_HEADERS:
             headers["X-OpenWebUI-User-Name"] = user.name
             headers["X-OpenWebUI-User-Name"] = user.name
             headers["X-OpenWebUI-User-Id"] = user.id
             headers["X-OpenWebUI-User-Id"] = user.id
@@ -430,8 +400,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
                             )
                             )
                         ]
                         ]
 
 
-                    return response_data
-
+                    models = response_data
             except aiohttp.ClientError as e:
             except aiohttp.ClientError as e:
                 # ClientError covers all aiohttp requests issues
                 # ClientError covers all aiohttp requests issues
                 log.exception(f"Client error: {str(e)}")
                 log.exception(f"Client error: {str(e)}")
@@ -445,6 +414,22 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
                 error_detail = f"Unexpected error: {str(e)}"
                 error_detail = f"Unexpected error: {str(e)}"
                 raise HTTPException(status_code=500, detail=error_detail)
                 raise HTTPException(status_code=500, detail=error_detail)
 
 
+    if user.role == "user":
+        # Filter models based on user access control
+        filtered_models = []
+        for model in models.get("data", []):
+            model_info = Models.get_model_by_id(model["id"])
+            if model_info:
+                if has_access(
+                    user.id, type="read", access_control=model_info.access_control
+                ):
+                    filtered_models.append(model)
+            else:
+                filtered_models.append(model)
+        models["data"] = filtered_models
+
+    return models
+
 
 
 class ConnectionVerificationForm(BaseModel):
 class ConnectionVerificationForm(BaseModel):
     url: str
     url: str
@@ -492,11 +477,10 @@ async def verify_connection(
 
 
 
 
 @app.post("/chat/completions")
 @app.post("/chat/completions")
-@app.post("/chat/completions/{url_idx}")
 async def generate_chat_completion(
 async def generate_chat_completion(
     form_data: dict,
     form_data: dict,
-    url_idx: Optional[int] = None,
     user=Depends(get_verified_user),
     user=Depends(get_verified_user),
+    bypass_filter: Optional[bool] = False,
 ):
 ):
     idx = 0
     idx = 0
     payload = {**form_data}
     payload = {**form_data}
@@ -507,6 +491,7 @@ async def generate_chat_completion(
     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)
 
 
+    # Check model info and override the payload
     if model_info:
     if model_info:
         if model_info.base_model_id:
         if model_info.base_model_id:
             payload["model"] = model_info.base_model_id
             payload["model"] = model_info.base_model_id
@@ -515,9 +500,33 @@ async def generate_chat_completion(
         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, user)
 
 
-    model = app.state.MODELS[payload.get("model")]
-    idx = model["urlIdx"]
+        # Check if user has access to the model
+        if user.role == "user" and not has_access(
+            user.id, type="read", access_control=model_info.access_control
+        ):
+            raise HTTPException(
+                status_code=403,
+                detail="Model not found",
+            )
+
+    # Attemp to get urlIdx from the model
+    models = await get_all_models()
+
+    # Find the model from the list
+    model = next(
+        (model for model in models["data"] if model["id"] == payload.get("model")),
+        None,
+    )
+
+    if model:
+        idx = model["urlIdx"]
+    else:
+        raise HTTPException(
+            status_code=404,
+            detail="Model not found",
+        )
 
 
+    # Get the API config for the model
     api_config = app.state.config.OPENAI_API_CONFIGS.get(
     api_config = app.state.config.OPENAI_API_CONFIGS.get(
         app.state.config.OPENAI_API_BASE_URLS[idx], {}
         app.state.config.OPENAI_API_BASE_URLS[idx], {}
     )
     )
@@ -526,6 +535,7 @@ async def generate_chat_completion(
     if prefix_id:
     if prefix_id:
         payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
         payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
 
 
+    # Add user info to the payload if the model is a pipeline
     if "pipeline" in model and model.get("pipeline"):
     if "pipeline" in model and model.get("pipeline"):
         payload["user"] = {
         payload["user"] = {
             "name": user.name,
             "name": user.name,
@@ -536,8 +546,9 @@ async def generate_chat_completion(
 
 
     url = app.state.config.OPENAI_API_BASE_URLS[idx]
     url = app.state.config.OPENAI_API_BASE_URLS[idx]
     key = app.state.config.OPENAI_API_KEYS[idx]
     key = app.state.config.OPENAI_API_KEYS[idx]
-    is_o1 = payload["model"].lower().startswith("o1-")
 
 
+    # Fix: O1 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens"
+    is_o1 = payload["model"].lower().startswith("o1-")
     # Change max_completion_tokens to max_tokens (Backward compatible)
     # Change max_completion_tokens to max_tokens (Backward compatible)
     if "api.openai.com" not in url and not is_o1:
     if "api.openai.com" not in url and not is_o1:
         if "max_completion_tokens" in payload:
         if "max_completion_tokens" in payload:

+ 16 - 7
backend/open_webui/apps/retrieval/utils.py

@@ -3,6 +3,7 @@ import os
 import uuid
 import uuid
 from typing import Optional, Union
 from typing import Optional, Union
 
 
+import asyncio
 import requests
 import requests
 
 
 from huggingface_hub import snapshot_download
 from huggingface_hub import snapshot_download
@@ -291,7 +292,13 @@ def get_embedding_function(
     if embedding_engine == "":
     if embedding_engine == "":
         return lambda query: embedding_function.encode(query).tolist()
         return lambda query: embedding_function.encode(query).tolist()
     elif embedding_engine in ["ollama", "openai"]:
     elif embedding_engine in ["ollama", "openai"]:
-        func = lambda query: generate_embeddings(
+
+        # Wrapper to run the async generate_embeddings synchronously.
+        def sync_generate_embeddings(*args, **kwargs):
+            return asyncio.run(generate_embeddings(*args, **kwargs))
+
+        # Semantic expectation from the original version (using sync wrapper).
+        func = lambda query: sync_generate_embeddings(
             engine=embedding_engine,
             engine=embedding_engine,
             model=embedding_model,
             model=embedding_model,
             text=query,
             text=query,
@@ -469,7 +476,7 @@ def get_model_path(model: str, update_model: bool = False):
         return model
         return model
 
 
 
 
-def generate_openai_batch_embeddings(
+async def generate_openai_batch_embeddings(
     model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
     model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
 ) -> Optional[list[list[float]]]:
 ) -> Optional[list[list[float]]]:
     try:
     try:
@@ -492,14 +499,16 @@ def generate_openai_batch_embeddings(
         return None
         return None
 
 
 
 
-def generate_embeddings(engine: str, model: str, text: Union[str, list[str]], **kwargs):
+async def generate_embeddings(
+    engine: str, model: str, text: Union[str, list[str]], **kwargs
+):
     if engine == "ollama":
     if engine == "ollama":
         if isinstance(text, list):
         if isinstance(text, list):
-            embeddings = generate_ollama_batch_embeddings(
+            embeddings = await generate_ollama_batch_embeddings(
                 GenerateEmbedForm(**{"model": model, "input": text})
                 GenerateEmbedForm(**{"model": model, "input": text})
             )
             )
         else:
         else:
-            embeddings = generate_ollama_batch_embeddings(
+            embeddings = await generate_ollama_batch_embeddings(
                 GenerateEmbedForm(**{"model": model, "input": [text]})
                 GenerateEmbedForm(**{"model": model, "input": [text]})
             )
             )
         return (
         return (
@@ -512,9 +521,9 @@ def generate_embeddings(engine: str, model: str, text: Union[str, list[str]], **
         url = kwargs.get("url", "https://api.openai.com/v1")
         url = kwargs.get("url", "https://api.openai.com/v1")
 
 
         if isinstance(text, list):
         if isinstance(text, list):
-            embeddings = generate_openai_batch_embeddings(model, text, key, url)
+            embeddings = await generate_openai_batch_embeddings(model, text, key, url)
         else:
         else:
-            embeddings = generate_openai_batch_embeddings(model, [text], key, url)
+            embeddings = await generate_openai_batch_embeddings(model, [text], key, url)
 
 
         return embeddings[0] if isinstance(text, str) else embeddings
         return embeddings[0] if isinstance(text, str) else embeddings
 
 

+ 15 - 7
backend/open_webui/apps/webui/main.py

@@ -12,6 +12,7 @@ from open_webui.apps.webui.routers import (
     chats,
     chats,
     folders,
     folders,
     configs,
     configs,
+    groups,
     files,
     files,
     functions,
     functions,
     memories,
     memories,
@@ -85,7 +86,11 @@ from open_webui.utils.payload import (
 
 
 from open_webui.utils.tools import get_tools
 from open_webui.utils.tools import get_tools
 
 
-app = FastAPI(docs_url="/docs" if ENV == "dev" else None, openapi_url="/openapi.json" if ENV == "dev" else None, redoc_url=None)
+app = FastAPI(
+    docs_url="/docs" if ENV == "dev" else None,
+    openapi_url="/openapi.json" if ENV == "dev" else None,
+    redoc_url=None,
+)
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 
 
@@ -105,6 +110,8 @@ app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
 app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
 app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
 app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
 app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
 app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
 app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
+
+
 app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
 app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
 app.state.config.WEBHOOK_URL = WEBHOOK_URL
 app.state.config.WEBHOOK_URL = WEBHOOK_URL
 app.state.config.BANNERS = WEBUI_BANNERS
 app.state.config.BANNERS = WEBUI_BANNERS
@@ -137,7 +144,6 @@ app.state.config.LDAP_USE_TLS = LDAP_USE_TLS
 app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
 app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
 app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
 app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
 
 
-app.state.MODELS = {}
 app.state.TOOLS = {}
 app.state.TOOLS = {}
 app.state.FUNCTIONS = {}
 app.state.FUNCTIONS = {}
 
 
@@ -161,13 +167,15 @@ app.include_router(models.router, prefix="/models", tags=["models"])
 app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"])
 app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"])
 app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
 app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
 app.include_router(tools.router, prefix="/tools", tags=["tools"])
 app.include_router(tools.router, prefix="/tools", tags=["tools"])
-app.include_router(functions.router, prefix="/functions", tags=["functions"])
 
 
 app.include_router(memories.router, prefix="/memories", tags=["memories"])
 app.include_router(memories.router, prefix="/memories", tags=["memories"])
-app.include_router(evaluations.router, prefix="/evaluations", tags=["evaluations"])
-
 app.include_router(folders.router, prefix="/folders", tags=["folders"])
 app.include_router(folders.router, prefix="/folders", tags=["folders"])
+
+app.include_router(groups.router, prefix="/groups", tags=["groups"])
 app.include_router(files.router, prefix="/files", tags=["files"])
 app.include_router(files.router, prefix="/files", tags=["files"])
+app.include_router(functions.router, prefix="/functions", tags=["functions"])
+app.include_router(evaluations.router, prefix="/evaluations", tags=["evaluations"])
+
 
 
 app.include_router(utils.router, prefix="/utils", tags=["utils"])
 app.include_router(utils.router, prefix="/utils", tags=["utils"])
 
 
@@ -362,7 +370,7 @@ def get_function_params(function_module, form_data, user, extra_params=None):
     return params
     return params
 
 
 
 
-async def generate_function_chat_completion(form_data, user):
+async def generate_function_chat_completion(form_data, user, models: dict = {}):
     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)
 
 
@@ -405,7 +413,7 @@ async def generate_function_chat_completion(form_data, user):
         user,
         user,
         {
         {
             **extra_params,
             **extra_params,
-            "__model__": app.state.MODELS[form_data["model"]],
+            "__model__": models.get(form_data["model"], None),
             "__messages__": form_data["messages"],
             "__messages__": form_data["messages"],
             "__files__": files,
             "__files__": files,
         },
         },

+ 0 - 157
backend/open_webui/apps/webui/models/documents.py

@@ -1,157 +0,0 @@
-import json
-import logging
-import time
-from typing import Optional
-
-from open_webui.apps.webui.internal.db import Base, get_db
-from open_webui.env import SRC_LOG_LEVELS
-from pydantic import BaseModel, ConfigDict
-from sqlalchemy import BigInteger, Column, String, Text
-
-log = logging.getLogger(__name__)
-log.setLevel(SRC_LOG_LEVELS["MODELS"])
-
-####################
-# Documents DB Schema
-####################
-
-
-class Document(Base):
-    __tablename__ = "document"
-
-    collection_name = Column(String, primary_key=True)
-    name = Column(String, unique=True)
-    title = Column(Text)
-    filename = Column(Text)
-    content = Column(Text, nullable=True)
-    user_id = Column(String)
-    timestamp = Column(BigInteger)
-
-
-class DocumentModel(BaseModel):
-    model_config = ConfigDict(from_attributes=True)
-
-    collection_name: str
-    name: str
-    title: str
-    filename: str
-    content: Optional[str] = None
-    user_id: str
-    timestamp: int  # timestamp in epoch
-
-
-####################
-# Forms
-####################
-
-
-class DocumentResponse(BaseModel):
-    collection_name: str
-    name: str
-    title: str
-    filename: str
-    content: Optional[dict] = None
-    user_id: str
-    timestamp: int  # timestamp in epoch
-
-
-class DocumentUpdateForm(BaseModel):
-    name: str
-    title: str
-
-
-class DocumentForm(DocumentUpdateForm):
-    collection_name: str
-    filename: str
-    content: Optional[str] = None
-
-
-class DocumentsTable:
-    def insert_new_doc(
-        self, user_id: str, form_data: DocumentForm
-    ) -> Optional[DocumentModel]:
-        with get_db() as db:
-            document = DocumentModel(
-                **{
-                    **form_data.model_dump(),
-                    "user_id": user_id,
-                    "timestamp": int(time.time()),
-                }
-            )
-
-            try:
-                result = Document(**document.model_dump())
-                db.add(result)
-                db.commit()
-                db.refresh(result)
-                if result:
-                    return DocumentModel.model_validate(result)
-                else:
-                    return None
-            except Exception:
-                return None
-
-    def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
-        try:
-            with get_db() as db:
-                document = db.query(Document).filter_by(name=name).first()
-                return DocumentModel.model_validate(document) if document else None
-        except Exception:
-            return None
-
-    def get_docs(self) -> list[DocumentModel]:
-        with get_db() as db:
-            return [
-                DocumentModel.model_validate(doc) for doc in db.query(Document).all()
-            ]
-
-    def update_doc_by_name(
-        self, name: str, form_data: DocumentUpdateForm
-    ) -> Optional[DocumentModel]:
-        try:
-            with get_db() as db:
-                db.query(Document).filter_by(name=name).update(
-                    {
-                        "title": form_data.title,
-                        "name": form_data.name,
-                        "timestamp": int(time.time()),
-                    }
-                )
-                db.commit()
-                return self.get_doc_by_name(form_data.name)
-        except Exception as e:
-            log.exception(e)
-            return None
-
-    def update_doc_content_by_name(
-        self, name: str, updated: dict
-    ) -> Optional[DocumentModel]:
-        try:
-            doc = self.get_doc_by_name(name)
-            doc_content = json.loads(doc.content if doc.content else "{}")
-            doc_content = {**doc_content, **updated}
-
-            with get_db() as db:
-                db.query(Document).filter_by(name=name).update(
-                    {
-                        "content": json.dumps(doc_content),
-                        "timestamp": int(time.time()),
-                    }
-                )
-                db.commit()
-                return self.get_doc_by_name(name)
-        except Exception as e:
-            log.exception(e)
-            return None
-
-    def delete_doc_by_name(self, name: str) -> bool:
-        try:
-            with get_db() as db:
-                db.query(Document).filter_by(name=name).delete()
-                db.commit()
-                return True
-        except Exception:
-            return False
-
-
-Documents = DocumentsTable()

+ 181 - 0
backend/open_webui/apps/webui/models/groups.py

@@ -0,0 +1,181 @@
+import json
+import logging
+import time
+from typing import Optional
+import uuid
+
+from open_webui.apps.webui.internal.db import Base, get_db
+from open_webui.env import SRC_LOG_LEVELS
+
+from open_webui.apps.webui.models.files import FileMetadataResponse
+
+
+from pydantic import BaseModel, ConfigDict
+from sqlalchemy import BigInteger, Column, String, Text, JSON
+
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+####################
+# UserGroup DB Schema
+####################
+
+
+class Group(Base):
+    __tablename__ = "group"
+
+    id = Column(Text, unique=True, primary_key=True)
+    user_id = Column(Text)
+
+    name = Column(Text)
+    description = Column(Text)
+
+    data = Column(JSON, nullable=True)
+    meta = Column(JSON, nullable=True)
+
+    permissions = Column(JSON, nullable=True)
+    user_ids = Column(JSON, nullable=True)
+
+    created_at = Column(BigInteger)
+    updated_at = Column(BigInteger)
+
+
+class GroupModel(BaseModel):
+    model_config = ConfigDict(from_attributes=True)
+    id: str
+    user_id: str
+
+    name: str
+    description: str
+
+    data: Optional[dict] = None
+    meta: Optional[dict] = None
+
+    permissions: Optional[dict] = None
+    user_ids: list[str] = []
+
+    created_at: int  # timestamp in epoch
+    updated_at: int  # timestamp in epoch
+
+
+####################
+# Forms
+####################
+
+
+class GroupResponse(BaseModel):
+    id: str
+    user_id: str
+    name: str
+    description: str
+    permissions: Optional[dict] = None
+    data: Optional[dict] = None
+    meta: Optional[dict] = None
+    user_ids: list[str] = []
+    created_at: int  # timestamp in epoch
+    updated_at: int  # timestamp in epoch
+
+
+class GroupForm(BaseModel):
+    name: str
+    description: str
+
+
+class GroupUpdateForm(GroupForm):
+    permissions: Optional[dict] = None
+    user_ids: Optional[list[str]] = None
+    admin_ids: Optional[list[str]] = None
+
+
+class GroupTable:
+    def insert_new_group(
+        self, user_id: str, form_data: GroupForm
+    ) -> Optional[GroupModel]:
+        with get_db() as db:
+            group = GroupModel(
+                **{
+                    **form_data.model_dump(),
+                    "id": str(uuid.uuid4()),
+                    "user_id": user_id,
+                    "created_at": int(time.time()),
+                    "updated_at": int(time.time()),
+                }
+            )
+
+            try:
+                result = Group(**group.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return GroupModel.model_validate(result)
+                else:
+                    return None
+
+            except Exception:
+                return None
+
+    def get_groups(self) -> list[GroupModel]:
+        with get_db() as db:
+            return [
+                GroupModel.model_validate(group)
+                for group in db.query(Group).order_by(Group.updated_at.desc()).all()
+            ]
+
+    def get_groups_by_member_id(self, user_id: str) -> list[GroupModel]:
+        with get_db() as db:
+            return [
+                GroupModel.model_validate(group)
+                for group in db.query(Group)
+                .filter(Group.user_ids.contains([user_id]))
+                .order_by(Group.updated_at.desc())
+                .all()
+            ]
+
+    def get_group_by_id(self, id: str) -> Optional[GroupModel]:
+        try:
+            with get_db() as db:
+                group = db.query(Group).filter_by(id=id).first()
+                return GroupModel.model_validate(group) if group else None
+        except Exception:
+            return None
+
+    def update_group_by_id(
+        self, id: str, form_data: GroupUpdateForm, overwrite: bool = False
+    ) -> Optional[GroupModel]:
+        try:
+            with get_db() as db:
+                db.query(Group).filter_by(id=id).update(
+                    {
+                        **form_data.model_dump(exclude_none=True),
+                        "updated_at": int(time.time()),
+                    }
+                )
+                db.commit()
+                return self.get_group_by_id(id=id)
+        except Exception as e:
+            log.exception(e)
+            return None
+
+    def delete_group_by_id(self, id: str) -> bool:
+        try:
+            with get_db() as db:
+                db.query(Group).filter_by(id=id).delete()
+                db.commit()
+                return True
+        except Exception:
+            return False
+
+    def delete_all_groups(self) -> bool:
+        with get_db() as db:
+            try:
+                db.query(Group).delete()
+                db.commit()
+
+                return True
+            except Exception:
+                return False
+
+
+Groups = GroupTable()

+ 55 - 9
backend/open_webui/apps/webui/models/knowledge.py

@@ -13,6 +13,7 @@ from open_webui.apps.webui.models.files import FileMetadataResponse
 from pydantic import BaseModel, ConfigDict
 from pydantic import BaseModel, ConfigDict
 from sqlalchemy import BigInteger, Column, String, Text, JSON
 from sqlalchemy import BigInteger, Column, String, Text, JSON
 
 
+from open_webui.utils.access_control import has_access
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -34,6 +35,23 @@ class Knowledge(Base):
     data = Column(JSON, nullable=True)
     data = Column(JSON, nullable=True)
     meta = Column(JSON, nullable=True)
     meta = Column(JSON, nullable=True)
 
 
+    access_control = Column(JSON, nullable=True)  # Controls data access levels.
+    # Defines access control rules for this entry.
+    # - `None`: Public access, available to all users with the "user" role.
+    # - `{}`: Private access, restricted exclusively to the owner.
+    # - Custom permissions: Specific access control for reading and writing;
+    #   Can specify group or user-level restrictions:
+    #   {
+    #      "read": {
+    #          "group_ids": ["group_id1", "group_id2"],
+    #          "user_ids":  ["user_id1", "user_id2"]
+    #      },
+    #      "write": {
+    #          "group_ids": ["group_id1", "group_id2"],
+    #          "user_ids":  ["user_id1", "user_id2"]
+    #      }
+    #   }
+
     created_at = Column(BigInteger)
     created_at = Column(BigInteger)
     updated_at = Column(BigInteger)
     updated_at = Column(BigInteger)
 
 
@@ -50,6 +68,8 @@ class KnowledgeModel(BaseModel):
     data: Optional[dict] = None
     data: Optional[dict] = None
     meta: Optional[dict] = None
     meta: Optional[dict] = None
 
 
+    access_control: Optional[dict] = None
+
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
 
 
@@ -65,6 +85,8 @@ class KnowledgeResponse(BaseModel):
     description: str
     description: str
     data: Optional[dict] = None
     data: Optional[dict] = None
     meta: Optional[dict] = None
     meta: Optional[dict] = None
+
+    access_control: Optional[dict] = None
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
 
 
@@ -75,12 +97,7 @@ class KnowledgeForm(BaseModel):
     name: str
     name: str
     description: str
     description: str
     data: Optional[dict] = None
     data: Optional[dict] = None
-
-
-class KnowledgeUpdateForm(BaseModel):
-    name: Optional[str] = None
-    description: Optional[str] = None
-    data: Optional[dict] = None
+    access_control: Optional[dict] = None
 
 
 
 
 class KnowledgeTable:
 class KnowledgeTable:
@@ -110,7 +127,7 @@ class KnowledgeTable:
             except Exception:
             except Exception:
                 return None
                 return None
 
 
-    def get_knowledge_items(self) -> list[KnowledgeModel]:
+    def get_knowledge_bases(self) -> list[KnowledgeModel]:
         with get_db() as db:
         with get_db() as db:
             return [
             return [
                 KnowledgeModel.model_validate(knowledge)
                 KnowledgeModel.model_validate(knowledge)
@@ -119,6 +136,17 @@ class KnowledgeTable:
                 .all()
                 .all()
             ]
             ]
 
 
+    def get_knowledge_bases_by_user_id(
+        self, user_id: str, permission: str = "write"
+    ) -> list[KnowledgeModel]:
+        knowledge_bases = self.get_knowledge_bases()
+        return [
+            knowledge_base
+            for knowledge_base in knowledge_bases
+            if knowledge_base.user_id == user_id
+            or has_access(user_id, permission, knowledge_base.access_control)
+        ]
+
     def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
     def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
         try:
         try:
             with get_db() as db:
             with get_db() as db:
@@ -128,14 +156,32 @@ class KnowledgeTable:
             return None
             return None
 
 
     def update_knowledge_by_id(
     def update_knowledge_by_id(
-        self, id: str, form_data: KnowledgeUpdateForm, overwrite: bool = False
+        self, id: str, form_data: KnowledgeForm, overwrite: bool = False
+    ) -> Optional[KnowledgeModel]:
+        try:
+            with get_db() as db:
+                knowledge = self.get_knowledge_by_id(id=id)
+                db.query(Knowledge).filter_by(id=id).update(
+                    {
+                        **form_data.model_dump(),
+                        "updated_at": int(time.time()),
+                    }
+                )
+                db.commit()
+                return self.get_knowledge_by_id(id=id)
+        except Exception as e:
+            log.exception(e)
+            return None
+
+    def update_knowledge_data_by_id(
+        self, id: str, data: dict
     ) -> Optional[KnowledgeModel]:
     ) -> Optional[KnowledgeModel]:
         try:
         try:
             with get_db() as db:
             with get_db() as db:
                 knowledge = self.get_knowledge_by_id(id=id)
                 knowledge = self.get_knowledge_by_id(id=id)
                 db.query(Knowledge).filter_by(id=id).update(
                 db.query(Knowledge).filter_by(id=id).update(
                     {
                     {
-                        **form_data.model_dump(exclude_none=True),
+                        "data": data,
                         "updated_at": int(time.time()),
                         "updated_at": int(time.time()),
                     }
                     }
                 )
                 )

+ 87 - 2
backend/open_webui/apps/webui/models/models.py

@@ -4,8 +4,19 @@ from typing import Optional
 
 
 from open_webui.apps.webui.internal.db import Base, JSONField, get_db
 from open_webui.apps.webui.internal.db import Base, JSONField, get_db
 from open_webui.env import SRC_LOG_LEVELS
 from open_webui.env import SRC_LOG_LEVELS
+
+from open_webui.apps.webui.models.groups import Groups
+
+
 from pydantic import BaseModel, ConfigDict
 from pydantic import BaseModel, ConfigDict
-from sqlalchemy import BigInteger, Column, Text
+
+from sqlalchemy import or_, and_, func
+from sqlalchemy.dialects import postgresql, sqlite
+from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
+
+
+from open_webui.utils.access_control import has_access
+
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -67,6 +78,25 @@ class Model(Base):
         Holds a JSON encoded blob of metadata, see `ModelMeta`.
         Holds a JSON encoded blob of metadata, see `ModelMeta`.
     """
     """
 
 
+    access_control = Column(JSON, nullable=True)  # Controls data access levels.
+    # Defines access control rules for this entry.
+    # - `None`: Public access, available to all users with the "user" role.
+    # - `{}`: Private access, restricted exclusively to the owner.
+    # - Custom permissions: Specific access control for reading and writing;
+    #   Can specify group or user-level restrictions:
+    #   {
+    #      "read": {
+    #          "group_ids": ["group_id1", "group_id2"],
+    #          "user_ids":  ["user_id1", "user_id2"]
+    #      },
+    #      "write": {
+    #          "group_ids": ["group_id1", "group_id2"],
+    #          "user_ids":  ["user_id1", "user_id2"]
+    #      }
+    #   }
+
+    is_active = Column(Boolean, default=True)
+
     updated_at = Column(BigInteger)
     updated_at = Column(BigInteger)
     created_at = Column(BigInteger)
     created_at = Column(BigInteger)
 
 
@@ -80,6 +110,9 @@ class ModelModel(BaseModel):
     params: ModelParams
     params: ModelParams
     meta: ModelMeta
     meta: ModelMeta
 
 
+    access_control: Optional[dict] = None
+
+    is_active: bool
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
@@ -93,8 +126,16 @@ class ModelModel(BaseModel):
 
 
 class ModelResponse(BaseModel):
 class ModelResponse(BaseModel):
     id: str
     id: str
+    user_id: str
+    base_model_id: Optional[str] = None
+
     name: str
     name: str
+    params: ModelParams
     meta: ModelMeta
     meta: ModelMeta
+
+    access_control: Optional[dict] = None
+
+    is_active: bool
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
@@ -105,6 +146,8 @@ class ModelForm(BaseModel):
     name: str
     name: str
     meta: ModelMeta
     meta: ModelMeta
     params: ModelParams
     params: ModelParams
+    access_control: Optional[dict] = None
+    is_active: bool = True
 
 
 
 
 class ModelsTable:
 class ModelsTable:
@@ -138,6 +181,31 @@ class ModelsTable:
         with get_db() as db:
         with get_db() as db:
             return [ModelModel.model_validate(model) for model in db.query(Model).all()]
             return [ModelModel.model_validate(model) for model in db.query(Model).all()]
 
 
+    def get_models(self) -> list[ModelModel]:
+        with get_db() as db:
+            return [
+                ModelModel.model_validate(model)
+                for model in db.query(Model).filter(Model.base_model_id != None).all()
+            ]
+
+    def get_base_models(self) -> list[ModelModel]:
+        with get_db() as db:
+            return [
+                ModelModel.model_validate(model)
+                for model in db.query(Model).filter(Model.base_model_id == None).all()
+            ]
+
+    def get_models_by_user_id(
+        self, user_id: str, permission: str = "write"
+    ) -> list[ModelModel]:
+        models = self.get_all_models()
+        return [
+            model
+            for model in models
+            if model.user_id == user_id
+            or has_access(user_id, permission, model.access_control)
+        ]
+
     def get_model_by_id(self, id: str) -> Optional[ModelModel]:
     def get_model_by_id(self, id: str) -> Optional[ModelModel]:
         try:
         try:
             with get_db() as db:
             with get_db() as db:
@@ -146,6 +214,23 @@ class ModelsTable:
         except Exception:
         except Exception:
             return None
             return None
 
 
+    def toggle_model_by_id(self, id: str) -> Optional[ModelModel]:
+        with get_db() as db:
+            try:
+                is_active = db.query(Model).filter_by(id=id).first().is_active
+
+                db.query(Model).filter_by(id=id).update(
+                    {
+                        "is_active": not is_active,
+                        "updated_at": int(time.time()),
+                    }
+                )
+                db.commit()
+
+                return self.get_model_by_id(id)
+            except Exception:
+                return None
+
     def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
     def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
         try:
         try:
             with get_db() as db:
             with get_db() as db:
@@ -153,7 +238,7 @@ class ModelsTable:
                 result = (
                 result = (
                     db.query(Model)
                     db.query(Model)
                     .filter_by(id=id)
                     .filter_by(id=id)
-                    .update(model.model_dump(exclude={"id"}, exclude_none=True))
+                    .update(model.model_dump(exclude={"id"}))
                 )
                 )
                 db.commit()
                 db.commit()
 
 

+ 39 - 5
backend/open_webui/apps/webui/models/prompts.py

@@ -2,8 +2,12 @@ import time
 from typing import Optional
 from typing import Optional
 
 
 from open_webui.apps.webui.internal.db import Base, get_db
 from open_webui.apps.webui.internal.db import Base, get_db
+from open_webui.apps.webui.models.groups import Groups
+
 from pydantic import BaseModel, ConfigDict
 from pydantic import BaseModel, ConfigDict
-from sqlalchemy import BigInteger, Column, String, Text
+from sqlalchemy import BigInteger, Column, String, Text, JSON
+
+from open_webui.utils.access_control import has_access
 
 
 ####################
 ####################
 # Prompts DB Schema
 # Prompts DB Schema
@@ -19,6 +23,23 @@ class Prompt(Base):
     content = Column(Text)
     content = Column(Text)
     timestamp = Column(BigInteger)
     timestamp = Column(BigInteger)
 
 
+    access_control = Column(JSON, nullable=True)  # Controls data access levels.
+    # Defines access control rules for this entry.
+    # - `None`: Public access, available to all users with the "user" role.
+    # - `{}`: Private access, restricted exclusively to the owner.
+    # - Custom permissions: Specific access control for reading and writing;
+    #   Can specify group or user-level restrictions:
+    #   {
+    #      "read": {
+    #          "group_ids": ["group_id1", "group_id2"],
+    #          "user_ids":  ["user_id1", "user_id2"]
+    #      },
+    #      "write": {
+    #          "group_ids": ["group_id1", "group_id2"],
+    #          "user_ids":  ["user_id1", "user_id2"]
+    #      }
+    #   }
+
 
 
 class PromptModel(BaseModel):
 class PromptModel(BaseModel):
     command: str
     command: str
@@ -27,6 +48,7 @@ class PromptModel(BaseModel):
     content: str
     content: str
     timestamp: int  # timestamp in epoch
     timestamp: int  # timestamp in epoch
 
 
+    access_control: Optional[dict] = None
     model_config = ConfigDict(from_attributes=True)
     model_config = ConfigDict(from_attributes=True)
 
 
 
 
@@ -39,6 +61,7 @@ class PromptForm(BaseModel):
     command: str
     command: str
     title: str
     title: str
     content: str
     content: str
+    access_control: Optional[dict] = None
 
 
 
 
 class PromptsTable:
 class PromptsTable:
@@ -48,16 +71,14 @@ class PromptsTable:
         prompt = PromptModel(
         prompt = PromptModel(
             **{
             **{
                 "user_id": user_id,
                 "user_id": user_id,
-                "command": form_data.command,
-                "title": form_data.title,
-                "content": form_data.content,
+                **form_data.model_dump(),
                 "timestamp": int(time.time()),
                 "timestamp": int(time.time()),
             }
             }
         )
         )
 
 
         try:
         try:
             with get_db() as db:
             with get_db() as db:
-                result = Prompt(**prompt.dict())
+                result = Prompt(**prompt.model_dump())
                 db.add(result)
                 db.add(result)
                 db.commit()
                 db.commit()
                 db.refresh(result)
                 db.refresh(result)
@@ -82,6 +103,18 @@ class PromptsTable:
                 PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
                 PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
             ]
             ]
 
 
+    def get_prompts_by_user_id(
+        self, user_id: str, permission: str = "write"
+    ) -> list[PromptModel]:
+        prompts = self.get_prompts()
+
+        return [
+            prompt
+            for prompt in prompts
+            if prompt.user_id == user_id
+            or has_access(user_id, permission, prompt.access_control)
+        ]
+
     def update_prompt_by_command(
     def update_prompt_by_command(
         self, command: str, form_data: PromptForm
         self, command: str, form_data: PromptForm
     ) -> Optional[PromptModel]:
     ) -> Optional[PromptModel]:
@@ -90,6 +123,7 @@ class PromptsTable:
                 prompt = db.query(Prompt).filter_by(command=command).first()
                 prompt = db.query(Prompt).filter_by(command=command).first()
                 prompt.title = form_data.title
                 prompt.title = form_data.title
                 prompt.content = form_data.content
                 prompt.content = form_data.content
+                prompt.access_control = form_data.access_control
                 prompt.timestamp = int(time.time())
                 prompt.timestamp = int(time.time())
                 db.commit()
                 db.commit()
                 return PromptModel.model_validate(prompt)
                 return PromptModel.model_validate(prompt)

+ 38 - 1
backend/open_webui/apps/webui/models/tools.py

@@ -6,7 +6,10 @@ from open_webui.apps.webui.internal.db import Base, JSONField, get_db
 from open_webui.apps.webui.models.users import Users
 from open_webui.apps.webui.models.users import Users
 from open_webui.env import SRC_LOG_LEVELS
 from open_webui.env import SRC_LOG_LEVELS
 from pydantic import BaseModel, ConfigDict
 from pydantic import BaseModel, ConfigDict
-from sqlalchemy import BigInteger, Column, String, Text
+from sqlalchemy import BigInteger, Column, String, Text, JSON
+
+from open_webui.utils.access_control import has_access
+
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -26,6 +29,24 @@ class Tool(Base):
     specs = Column(JSONField)
     specs = Column(JSONField)
     meta = Column(JSONField)
     meta = Column(JSONField)
     valves = Column(JSONField)
     valves = Column(JSONField)
+
+    access_control = Column(JSON, nullable=True)  # Controls data access levels.
+    # Defines access control rules for this entry.
+    # - `None`: Public access, available to all users with the "user" role.
+    # - `{}`: Private access, restricted exclusively to the owner.
+    # - Custom permissions: Specific access control for reading and writing;
+    #   Can specify group or user-level restrictions:
+    #   {
+    #      "read": {
+    #          "group_ids": ["group_id1", "group_id2"],
+    #          "user_ids":  ["user_id1", "user_id2"]
+    #      },
+    #      "write": {
+    #          "group_ids": ["group_id1", "group_id2"],
+    #          "user_ids":  ["user_id1", "user_id2"]
+    #      }
+    #   }
+
     updated_at = Column(BigInteger)
     updated_at = Column(BigInteger)
     created_at = Column(BigInteger)
     created_at = Column(BigInteger)
 
 
@@ -42,6 +63,8 @@ class ToolModel(BaseModel):
     content: str
     content: str
     specs: list[dict]
     specs: list[dict]
     meta: ToolMeta
     meta: ToolMeta
+    access_control: Optional[dict] = None
+
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
@@ -58,6 +81,7 @@ class ToolResponse(BaseModel):
     user_id: str
     user_id: str
     name: str
     name: str
     meta: ToolMeta
     meta: ToolMeta
+    access_control: Optional[dict] = None
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
@@ -67,6 +91,7 @@ class ToolForm(BaseModel):
     name: str
     name: str
     content: str
     content: str
     meta: ToolMeta
     meta: ToolMeta
+    access_control: Optional[dict] = None
 
 
 
 
 class ToolValves(BaseModel):
 class ToolValves(BaseModel):
@@ -113,6 +138,18 @@ class ToolsTable:
         with get_db() as db:
         with get_db() as db:
             return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
             return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
 
 
+    def get_tools_by_user_id(
+        self, user_id: str, permission: str = "write"
+    ) -> list[ToolModel]:
+        tools = self.get_tools()
+
+        return [
+            tool
+            for tool in tools
+            if tool.user_id == user_id
+            or has_access(user_id, permission, tool.access_control)
+        ]
+
     def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
     def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
         try:
         try:
             with get_db() as db:
             with get_db() as db:

+ 95 - 31
backend/open_webui/apps/webui/routers/auths.py

@@ -40,10 +40,12 @@ from open_webui.utils.utils import (
     get_password_hash,
     get_password_hash,
 )
 )
 from open_webui.utils.webhook import post_webhook
 from open_webui.utils.webhook import post_webhook
+from open_webui.utils.access_control import get_permissions
+
 from typing import Optional, List
 from typing import Optional, List
 
 
-from ldap3 import Server, Connection, ALL, Tls
 from ssl import CERT_REQUIRED, PROTOCOL_TLS
 from ssl import CERT_REQUIRED, PROTOCOL_TLS
+from ldap3 import Server, Connection, ALL, Tls
 from ldap3.utils.conv import escape_filter_chars
 from ldap3.utils.conv import escape_filter_chars
 
 
 router = APIRouter()
 router = APIRouter()
@@ -58,6 +60,7 @@ log.setLevel(SRC_LOG_LEVELS["MAIN"])
 
 
 class SessionUserResponse(Token, UserResponse):
 class SessionUserResponse(Token, UserResponse):
     expires_at: Optional[int] = None
     expires_at: Optional[int] = None
+    permissions: Optional[dict] = None
 
 
 
 
 @router.get("/", response_model=SessionUserResponse)
 @router.get("/", response_model=SessionUserResponse)
@@ -90,6 +93,10 @@ async def get_session_user(
         secure=WEBUI_SESSION_COOKIE_SECURE,
         secure=WEBUI_SESSION_COOKIE_SECURE,
     )
     )
 
 
+    user_permissions = get_permissions(
+        user.id, request.app.state.config.USER_PERMISSIONS
+    )
+
     return {
     return {
         "token": token,
         "token": token,
         "token_type": "Bearer",
         "token_type": "Bearer",
@@ -99,6 +106,7 @@ async def get_session_user(
         "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,
     }
     }
 
 
 
 
@@ -163,40 +171,67 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
     LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
     LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
     LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
     LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
     LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
     LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
-    LDAP_CIPHERS = request.app.state.config.LDAP_CIPHERS if request.app.state.config.LDAP_CIPHERS else 'ALL'
+    LDAP_CIPHERS = (
+        request.app.state.config.LDAP_CIPHERS
+        if request.app.state.config.LDAP_CIPHERS
+        else "ALL"
+    )
 
 
     if not ENABLE_LDAP:
     if not ENABLE_LDAP:
         raise HTTPException(400, detail="LDAP authentication is not enabled")
         raise HTTPException(400, detail="LDAP authentication is not enabled")
 
 
     try:
     try:
-        tls = Tls(validate=CERT_REQUIRED, version=PROTOCOL_TLS, ca_certs_file=LDAP_CA_CERT_FILE, ciphers=LDAP_CIPHERS)
+        tls = Tls(
+            validate=CERT_REQUIRED,
+            version=PROTOCOL_TLS,
+            ca_certs_file=LDAP_CA_CERT_FILE,
+            ciphers=LDAP_CIPHERS,
+        )
     except Exception as e:
     except Exception as e:
         log.error(f"An error occurred on TLS: {str(e)}")
         log.error(f"An error occurred on TLS: {str(e)}")
         raise HTTPException(400, detail=str(e))
         raise HTTPException(400, detail=str(e))
 
 
     try:
     try:
-        server = Server(host=LDAP_SERVER_HOST, port=LDAP_SERVER_PORT, get_info=ALL, use_ssl=LDAP_USE_TLS, tls=tls)
-        connection_app = Connection(server, LDAP_APP_DN, LDAP_APP_PASSWORD, auto_bind='NONE', authentication='SIMPLE')
+        server = Server(
+            host=LDAP_SERVER_HOST,
+            port=LDAP_SERVER_PORT,
+            get_info=ALL,
+            use_ssl=LDAP_USE_TLS,
+            tls=tls,
+        )
+        connection_app = Connection(
+            server,
+            LDAP_APP_DN,
+            LDAP_APP_PASSWORD,
+            auto_bind="NONE",
+            authentication="SIMPLE",
+        )
         if not connection_app.bind():
         if not connection_app.bind():
             raise HTTPException(400, detail="Application account bind failed")
             raise HTTPException(400, detail="Application account bind failed")
 
 
         search_success = connection_app.search(
         search_success = connection_app.search(
             search_base=LDAP_SEARCH_BASE,
             search_base=LDAP_SEARCH_BASE,
-            search_filter=f'(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})',
-            attributes=[f'{LDAP_ATTRIBUTE_FOR_USERNAME}', 'mail', 'cn']
+            search_filter=f"(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})",
+            attributes=[f"{LDAP_ATTRIBUTE_FOR_USERNAME}", "mail", "cn"],
         )
         )
 
 
         if not search_success:
         if not search_success:
             raise HTTPException(400, detail="User not found in the LDAP server")
             raise HTTPException(400, detail="User not found in the LDAP server")
 
 
         entry = connection_app.entries[0]
         entry = connection_app.entries[0]
-        username = str(entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}']).lower()
-        mail = str(entry['mail'])
-        cn = str(entry['cn'])
+        username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
+        mail = str(entry["mail"])
+        cn = str(entry["cn"])
         user_dn = entry.entry_dn
         user_dn = entry.entry_dn
 
 
         if username == form_data.user.lower():
         if username == form_data.user.lower():
-            connection_user = Connection(server, user_dn, form_data.password, auto_bind='NONE', authentication='SIMPLE')
+            connection_user = Connection(
+                server,
+                user_dn,
+                form_data.password,
+                auto_bind="NONE",
+                authentication="SIMPLE",
+            )
             if not connection_user.bind():
             if not connection_user.bind():
                 raise HTTPException(400, f"Authentication failed for {form_data.user}")
                 raise HTTPException(400, f"Authentication failed for {form_data.user}")
 
 
@@ -205,14 +240,12 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
 
 
                 try:
                 try:
                     hashed = get_password_hash(form_data.password)
                     hashed = get_password_hash(form_data.password)
-                    user = Auths.insert_new_auth(
-                        mail,
-                        hashed,
-                        cn
-                    )
+                    user = Auths.insert_new_auth(mail, hashed, cn)
 
 
                     if not user:
                     if not user:
-                        raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
+                        raise HTTPException(
+                            500, detail=ERROR_MESSAGES.CREATE_USER_ERROR
+                        )
 
 
                 except HTTPException:
                 except HTTPException:
                     raise
                     raise
@@ -224,7 +257,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
             if user:
             if user:
                 token = create_token(
                 token = create_token(
                     data={"id": user.id},
                     data={"id": user.id},
-                    expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
+                    expires_delta=parse_duration(
+                        request.app.state.config.JWT_EXPIRES_IN
+                    ),
                 )
                 )
 
 
                 # Set the cookie token
                 # Set the cookie token
@@ -246,7 +281,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
             else:
             else:
                 raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
                 raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
         else:
         else:
-            raise HTTPException(400, f"User {form_data.user} does not match the record. Search result: {str(entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}'])}")
+            raise HTTPException(
+                400,
+                f"User {form_data.user} does not match the record. Search result: {str(entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}'])}",
+            )
     except Exception as e:
     except Exception as e:
         raise HTTPException(400, detail=str(e))
         raise HTTPException(400, detail=str(e))
 
 
@@ -325,6 +363,10 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
             secure=WEBUI_SESSION_COOKIE_SECURE,
             secure=WEBUI_SESSION_COOKIE_SECURE,
         )
         )
 
 
+        user_permissions = get_permissions(
+            user.id, request.app.state.config.USER_PERMISSIONS
+        )
+
         return {
         return {
             "token": token,
             "token": token,
             "token_type": "Bearer",
             "token_type": "Bearer",
@@ -334,6 +376,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
             "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)
@@ -426,6 +469,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
                     },
                     },
                 )
                 )
 
 
+            user_permissions = get_permissions(
+                user.id, request.app.state.config.USER_PERMISSIONS
+            )
+
             return {
             return {
                 "token": token,
                 "token": token,
                 "token_type": "Bearer",
                 "token_type": "Bearer",
@@ -435,6 +482,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
                 "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(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
             raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
@@ -583,19 +631,18 @@ class LdapServerConfig(BaseModel):
     label: str
     label: str
     host: str
     host: str
     port: Optional[int] = None
     port: Optional[int] = None
-    attribute_for_username: str = 'uid'
+    attribute_for_username: str = "uid"
     app_dn: str
     app_dn: str
     app_dn_password: str
     app_dn_password: str
     search_base: str
     search_base: str
-    search_filters: str = ''
+    search_filters: str = ""
     use_tls: bool = True
     use_tls: bool = True
     certificate_path: Optional[str] = None
     certificate_path: Optional[str] = None
-    ciphers: Optional[str] = 'ALL'
+    ciphers: Optional[str] = "ALL"
+
 
 
 @router.get("/admin/config/ldap/server", response_model=LdapServerConfig)
 @router.get("/admin/config/ldap/server", response_model=LdapServerConfig)
-async def get_ldap_server(
-    request: Request, user=Depends(get_admin_user)
-):
+async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
     return {
     return {
         "label": request.app.state.config.LDAP_SERVER_LABEL,
         "label": request.app.state.config.LDAP_SERVER_LABEL,
         "host": request.app.state.config.LDAP_SERVER_HOST,
         "host": request.app.state.config.LDAP_SERVER_HOST,
@@ -607,26 +654,38 @@ async def get_ldap_server(
         "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
         "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
         "use_tls": request.app.state.config.LDAP_USE_TLS,
         "use_tls": request.app.state.config.LDAP_USE_TLS,
         "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
         "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
-        "ciphers": request.app.state.config.LDAP_CIPHERS
+        "ciphers": request.app.state.config.LDAP_CIPHERS,
     }
     }
 
 
+
 @router.post("/admin/config/ldap/server")
 @router.post("/admin/config/ldap/server")
 async def update_ldap_server(
 async def update_ldap_server(
     request: Request, form_data: LdapServerConfig, user=Depends(get_admin_user)
     request: Request, form_data: LdapServerConfig, user=Depends(get_admin_user)
 ):
 ):
-    required_fields = ['label', 'host', 'attribute_for_username', 'app_dn', 'app_dn_password', 'search_base']
+    required_fields = [
+        "label",
+        "host",
+        "attribute_for_username",
+        "app_dn",
+        "app_dn_password",
+        "search_base",
+    ]
     for key in required_fields:
     for key in required_fields:
         value = getattr(form_data, key)
         value = getattr(form_data, key)
         if not value:
         if not value:
             raise HTTPException(400, detail=f"Required field {key} is empty")
             raise HTTPException(400, detail=f"Required field {key} is empty")
 
 
     if form_data.use_tls and not form_data.certificate_path:
     if form_data.use_tls and not form_data.certificate_path:
-        raise HTTPException(400, detail="TLS is enabled but certificate file path is missing")
+        raise HTTPException(
+            400, detail="TLS is enabled but certificate file path is missing"
+        )
 
 
     request.app.state.config.LDAP_SERVER_LABEL = form_data.label
     request.app.state.config.LDAP_SERVER_LABEL = form_data.label
     request.app.state.config.LDAP_SERVER_HOST = form_data.host
     request.app.state.config.LDAP_SERVER_HOST = form_data.host
     request.app.state.config.LDAP_SERVER_PORT = form_data.port
     request.app.state.config.LDAP_SERVER_PORT = form_data.port
-    request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = form_data.attribute_for_username
+    request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = (
+        form_data.attribute_for_username
+    )
     request.app.state.config.LDAP_APP_DN = form_data.app_dn
     request.app.state.config.LDAP_APP_DN = form_data.app_dn
     request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password
     request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password
     request.app.state.config.LDAP_SEARCH_BASE = form_data.search_base
     request.app.state.config.LDAP_SEARCH_BASE = form_data.search_base
@@ -646,18 +705,23 @@ async def update_ldap_server(
         "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
         "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
         "use_tls": request.app.state.config.LDAP_USE_TLS,
         "use_tls": request.app.state.config.LDAP_USE_TLS,
         "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
         "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
-        "ciphers": request.app.state.config.LDAP_CIPHERS
+        "ciphers": request.app.state.config.LDAP_CIPHERS,
     }
     }
 
 
+
 @router.get("/admin/config/ldap")
 @router.get("/admin/config/ldap")
 async def get_ldap_config(request: Request, user=Depends(get_admin_user)):
 async def get_ldap_config(request: Request, user=Depends(get_admin_user)):
     return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
     return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
 
 
+
 class LdapConfigForm(BaseModel):
 class LdapConfigForm(BaseModel):
     enable_ldap: Optional[bool] = None
     enable_ldap: Optional[bool] = None
 
 
+
 @router.post("/admin/config/ldap")
 @router.post("/admin/config/ldap")
-async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user)):
+async def update_ldap_config(
+    request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user)
+):
     request.app.state.config.ENABLE_LDAP = form_data.enable_ldap
     request.app.state.config.ENABLE_LDAP = form_data.enable_ldap
     return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
     return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
 
 

+ 9 - 5
backend/open_webui/apps/webui/routers/chats.py

@@ -17,7 +17,10 @@ from open_webui.constants import ERROR_MESSAGES
 from open_webui.env import SRC_LOG_LEVELS
 from open_webui.env import SRC_LOG_LEVELS
 from fastapi import APIRouter, Depends, HTTPException, Request, status
 from fastapi import APIRouter, Depends, HTTPException, Request, status
 from pydantic import BaseModel
 from pydantic import BaseModel
+
+
 from open_webui.utils.utils import get_admin_user, get_verified_user
 from open_webui.utils.utils import get_admin_user, get_verified_user
+from open_webui.utils.access_control import has_permission
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -50,9 +53,10 @@ async def get_session_user_chat_list(
 
 
 @router.delete("/", response_model=bool)
 @router.delete("/", response_model=bool)
 async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)):
 async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)):
-    if user.role == "user" and not request.app.state.config.USER_PERMISSIONS.get(
-        "chat", {}
-    ).get("deletion", {}):
+
+    if user.role == "user" and not has_permission(
+        user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
+    ):
         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,
@@ -385,8 +389,8 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified
 
 
         return result
         return result
     else:
     else:
-        if not request.app.state.config.USER_PERMISSIONS.get("chat", {}).get(
-            "deletion", {}
+        if not has_permission(
+            user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
         ):
         ):
             raise HTTPException(
             raise HTTPException(
                 status_code=status.HTTP_401_UNAUTHORIZED,
                 status_code=status.HTTP_401_UNAUTHORIZED,

+ 120 - 0
backend/open_webui/apps/webui/routers/groups.py

@@ -0,0 +1,120 @@
+import os
+from pathlib import Path
+from typing import Optional
+
+from open_webui.apps.webui.models.groups import (
+    Groups,
+    GroupForm,
+    GroupUpdateForm,
+    GroupResponse,
+)
+
+from open_webui.config import CACHE_DIR
+from open_webui.constants import ERROR_MESSAGES
+from fastapi import APIRouter, Depends, HTTPException, Request, status
+from open_webui.utils.utils import get_admin_user, get_verified_user
+
+router = APIRouter()
+
+############################
+# GetFunctions
+############################
+
+
+@router.get("/", response_model=list[GroupResponse])
+async def get_groups(user=Depends(get_verified_user)):
+    if user.role == "admin":
+        return Groups.get_groups()
+    else:
+        return Groups.get_groups_by_member_id(user.id)
+
+
+############################
+# CreateNewGroup
+############################
+
+
+@router.post("/create", response_model=Optional[GroupResponse])
+async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user)):
+    try:
+        group = Groups.insert_new_group(user.id, form_data)
+        if group:
+            return group
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error creating group"),
+            )
+    except Exception as e:
+        print(e)
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.DEFAULT(e),
+        )
+
+
+############################
+# GetGroupById
+############################
+
+
+@router.get("/id/{id}", response_model=Optional[GroupResponse])
+async def get_group_by_id(id: str, user=Depends(get_admin_user)):
+    group = Groups.get_group_by_id(id)
+    if group:
+        return group
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# UpdateGroupById
+############################
+
+
+@router.post("/id/{id}/update", response_model=Optional[GroupResponse])
+async def update_group_by_id(
+    id: str, form_data: GroupUpdateForm, user=Depends(get_admin_user)
+):
+    try:
+        group = Groups.update_group_by_id(id, form_data)
+        if group:
+            return group
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error updating group"),
+            )
+    except Exception as e:
+        print(e)
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.DEFAULT(e),
+        )
+
+
+############################
+# DeleteGroupById
+############################
+
+
+@router.delete("/id/{id}/delete", response_model=bool)
+async def delete_group_by_id(id: str, user=Depends(get_admin_user)):
+    try:
+        result = Groups.delete_group_by_id(id)
+        if result:
+            return result
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error deleting group"),
+            )
+    except Exception as e:
+        print(e)
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.DEFAULT(e),
+        )

+ 192 - 77
backend/open_webui/apps/webui/routers/knowledge.py

@@ -6,7 +6,6 @@ import logging
 
 
 from open_webui.apps.webui.models.knowledge import (
 from open_webui.apps.webui.models.knowledge import (
     Knowledges,
     Knowledges,
-    KnowledgeUpdateForm,
     KnowledgeForm,
     KnowledgeForm,
     KnowledgeResponse,
     KnowledgeResponse,
 )
 )
@@ -17,6 +16,9 @@ from open_webui.apps.retrieval.main import process_file, ProcessFileForm
 
 
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.utils.utils import get_admin_user, get_verified_user
 from open_webui.utils.utils import get_admin_user, get_verified_user
+from open_webui.utils.access_control import has_access
+
+
 from open_webui.env import SRC_LOG_LEVELS
 from open_webui.env import SRC_LOG_LEVELS
 
 
 
 
@@ -26,64 +28,98 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
 router = APIRouter()
 router = APIRouter()
 
 
 ############################
 ############################
-# GetKnowledgeItems
+# getKnowledgeBases
 ############################
 ############################
 
 
 
 
-@router.get(
-    "/", response_model=Optional[Union[list[KnowledgeResponse], KnowledgeResponse]]
-)
-async def get_knowledge_items(
-    id: Optional[str] = None, user=Depends(get_verified_user)
-):
-    if id:
-        knowledge = Knowledges.get_knowledge_by_id(id=id)
+@router.get("/", response_model=list[KnowledgeResponse])
+async def get_knowledge(user=Depends(get_verified_user)):
+    knowledge_bases = []
 
 
-        if knowledge:
-            return knowledge
-        else:
-            raise HTTPException(
-                status_code=status.HTTP_401_UNAUTHORIZED,
-                detail=ERROR_MESSAGES.NOT_FOUND,
-            )
+    if user.role == "admin":
+        knowledge_bases = Knowledges.get_knowledge_bases()
     else:
     else:
-        knowledge_bases = []
-
-        for knowledge in Knowledges.get_knowledge_items():
+        knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read")
+
+    # Get files for each knowledge base
+    for knowledge_base in knowledge_bases:
+        files = []
+        if knowledge_base.data:
+            files = Files.get_file_metadatas_by_ids(
+                knowledge_base.data.get("file_ids", [])
+            )
 
 
-            files = []
-            if knowledge.data:
-                files = Files.get_file_metadatas_by_ids(
-                    knowledge.data.get("file_ids", [])
+            # Check if all files exist
+            if len(files) != len(knowledge_base.data.get("file_ids", [])):
+                missing_files = list(
+                    set(knowledge_base.data.get("file_ids", []))
+                    - set([file.id for file in files])
                 )
                 )
+                if missing_files:
+                    data = knowledge_base.data or {}
+                    file_ids = data.get("file_ids", [])
+
+                    for missing_file in missing_files:
+                        file_ids.remove(missing_file)
 
 
-                # Check if all files exist
-                if len(files) != len(knowledge.data.get("file_ids", [])):
-                    missing_files = list(
-                        set(knowledge.data.get("file_ids", []))
-                        - set([file.id for file in files])
+                    data["file_ids"] = file_ids
+                    Knowledges.update_knowledge_data_by_id(
+                        id=knowledge_base.id, data=data
                     )
                     )
-                    if missing_files:
-                        data = knowledge.data or {}
-                        file_ids = data.get("file_ids", [])
 
 
-                        for missing_file in missing_files:
-                            file_ids.remove(missing_file)
+                    files = Files.get_file_metadatas_by_ids(file_ids)
 
 
-                        data["file_ids"] = file_ids
-                        Knowledges.update_knowledge_by_id(
-                            id=knowledge.id, form_data=KnowledgeUpdateForm(data=data)
-                        )
+        knowledge_base = KnowledgeResponse(
+            **knowledge_base.model_dump(),
+            files=files,
+        )
 
 
-                        files = Files.get_file_metadatas_by_ids(file_ids)
+    return knowledge_bases
 
 
-            knowledge_bases.append(
-                KnowledgeResponse(
-                    **knowledge.model_dump(),
-                    files=files,
-                )
+
+@router.get("/list", response_model=list[KnowledgeResponse])
+async def get_knowledge_list(user=Depends(get_verified_user)):
+    knowledge_bases = []
+
+    if user.role == "admin":
+        knowledge_bases = Knowledges.get_knowledge_bases()
+    else:
+        knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "write")
+
+    # Get files for each knowledge base
+    for knowledge_base in knowledge_bases:
+        files = []
+        if knowledge_base.data:
+            files = Files.get_file_metadatas_by_ids(
+                knowledge_base.data.get("file_ids", [])
             )
             )
-        return knowledge_bases
+
+            # Check if all files exist
+            if len(files) != len(knowledge_base.data.get("file_ids", [])):
+                missing_files = list(
+                    set(knowledge_base.data.get("file_ids", []))
+                    - set([file.id for file in files])
+                )
+                if missing_files:
+                    data = knowledge_base.data or {}
+                    file_ids = data.get("file_ids", [])
+
+                    for missing_file in missing_files:
+                        file_ids.remove(missing_file)
+
+                    data["file_ids"] = file_ids
+                    Knowledges.update_knowledge_data_by_id(
+                        id=knowledge_base.id, data=data
+                    )
+
+                    files = Files.get_file_metadatas_by_ids(file_ids)
+
+        knowledge_base = KnowledgeResponse(
+            **knowledge_base.model_dump(),
+            files=files,
+        )
+
+    return knowledge_bases
 
 
 
 
 ############################
 ############################
@@ -92,7 +128,9 @@ async def get_knowledge_items(
 
 
 
 
 @router.post("/create", response_model=Optional[KnowledgeResponse])
 @router.post("/create", response_model=Optional[KnowledgeResponse])
-async def create_new_knowledge(form_data: KnowledgeForm, user=Depends(get_admin_user)):
+async def create_new_knowledge(
+    form_data: KnowledgeForm, user=Depends(get_verified_user)
+):
     knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
     knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
 
 
     if knowledge:
     if knowledge:
@@ -118,13 +156,20 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
     knowledge = Knowledges.get_knowledge_by_id(id=id)
     knowledge = Knowledges.get_knowledge_by_id(id=id)
 
 
     if knowledge:
     if knowledge:
-        file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
-        files = Files.get_files_by_ids(file_ids)
 
 
-        return KnowledgeFilesResponse(
-            **knowledge.model_dump(),
-            files=files,
-        )
+        if (
+            user.role == "admin"
+            or knowledge.user_id == user.id
+            or has_access(user.id, "read", knowledge.access_control)
+        ):
+
+            file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
+            files = Files.get_files_by_ids(file_ids)
+
+            return KnowledgeFilesResponse(
+                **knowledge.model_dump(),
+                files=files,
+            )
     else:
     else:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             status_code=status.HTTP_401_UNAUTHORIZED,
@@ -140,11 +185,23 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
 @router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
 @router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
 async def update_knowledge_by_id(
 async def update_knowledge_by_id(
     id: str,
     id: str,
-    form_data: KnowledgeUpdateForm,
-    user=Depends(get_admin_user),
+    form_data: KnowledgeForm,
+    user=Depends(get_verified_user),
 ):
 ):
-    knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
+    knowledge = Knowledges.get_knowledge_by_id(id=id)
+    if not knowledge:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
 
 
+    if knowledge.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+        )
+
+    knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
     if knowledge:
     if knowledge:
         file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
         file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
         files = Files.get_files_by_ids(file_ids)
         files = Files.get_files_by_ids(file_ids)
@@ -173,9 +230,22 @@ class KnowledgeFileIdForm(BaseModel):
 def add_file_to_knowledge_by_id(
 def add_file_to_knowledge_by_id(
     id: str,
     id: str,
     form_data: KnowledgeFileIdForm,
     form_data: KnowledgeFileIdForm,
-    user=Depends(get_admin_user),
+    user=Depends(get_verified_user),
 ):
 ):
     knowledge = Knowledges.get_knowledge_by_id(id=id)
     knowledge = Knowledges.get_knowledge_by_id(id=id)
+
+    if not knowledge:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if knowledge.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+        )
+
     file = Files.get_file_by_id(form_data.file_id)
     file = Files.get_file_by_id(form_data.file_id)
     if not file:
     if not file:
         raise HTTPException(
         raise HTTPException(
@@ -206,9 +276,7 @@ def add_file_to_knowledge_by_id(
             file_ids.append(form_data.file_id)
             file_ids.append(form_data.file_id)
             data["file_ids"] = file_ids
             data["file_ids"] = file_ids
 
 
-            knowledge = Knowledges.update_knowledge_by_id(
-                id=id, form_data=KnowledgeUpdateForm(data=data)
-            )
+            knowledge = Knowledges.update_knowledge_data_by_id(id=id.id, data=data)
 
 
             if knowledge:
             if knowledge:
                 files = Files.get_files_by_ids(file_ids)
                 files = Files.get_files_by_ids(file_ids)
@@ -238,9 +306,21 @@ def add_file_to_knowledge_by_id(
 def update_file_from_knowledge_by_id(
 def update_file_from_knowledge_by_id(
     id: str,
     id: str,
     form_data: KnowledgeFileIdForm,
     form_data: KnowledgeFileIdForm,
-    user=Depends(get_admin_user),
+    user=Depends(get_verified_user),
 ):
 ):
     knowledge = Knowledges.get_knowledge_by_id(id=id)
     knowledge = Knowledges.get_knowledge_by_id(id=id)
+    if not knowledge:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if knowledge.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+        )
+
     file = Files.get_file_by_id(form_data.file_id)
     file = Files.get_file_by_id(form_data.file_id)
     if not file:
     if not file:
         raise HTTPException(
         raise HTTPException(
@@ -288,9 +368,21 @@ def update_file_from_knowledge_by_id(
 def remove_file_from_knowledge_by_id(
 def remove_file_from_knowledge_by_id(
     id: str,
     id: str,
     form_data: KnowledgeFileIdForm,
     form_data: KnowledgeFileIdForm,
-    user=Depends(get_admin_user),
+    user=Depends(get_verified_user),
 ):
 ):
     knowledge = Knowledges.get_knowledge_by_id(id=id)
     knowledge = Knowledges.get_knowledge_by_id(id=id)
+    if not knowledge:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if knowledge.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+        )
+
     file = Files.get_file_by_id(form_data.file_id)
     file = Files.get_file_by_id(form_data.file_id)
     if not file:
     if not file:
         raise HTTPException(
         raise HTTPException(
@@ -318,9 +410,7 @@ def remove_file_from_knowledge_by_id(
             file_ids.remove(form_data.file_id)
             file_ids.remove(form_data.file_id)
             data["file_ids"] = file_ids
             data["file_ids"] = file_ids
 
 
-            knowledge = Knowledges.update_knowledge_by_id(
-                id=id, form_data=KnowledgeUpdateForm(data=data)
-            )
+            knowledge = Knowledges.update_knowledge_data_by_id(id=id.id, data=data)
 
 
             if knowledge:
             if knowledge:
                 files = Files.get_files_by_ids(file_ids)
                 files = Files.get_files_by_ids(file_ids)
@@ -347,35 +437,60 @@ def remove_file_from_knowledge_by_id(
 
 
 
 
 ############################
 ############################
-# ResetKnowledgeById
+# DeleteKnowledgeById
 ############################
 ############################
 
 
 
 
-@router.post("/{id}/reset", response_model=Optional[KnowledgeResponse])
-async def reset_knowledge_by_id(id: str, user=Depends(get_admin_user)):
+@router.delete("/{id}/delete", response_model=bool)
+async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
+    knowledge = Knowledges.get_knowledge_by_id(id=id)
+    if not knowledge:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if knowledge.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+        )
+
     try:
     try:
         VECTOR_DB_CLIENT.delete_collection(collection_name=id)
         VECTOR_DB_CLIENT.delete_collection(collection_name=id)
     except Exception as e:
     except Exception as e:
         log.debug(e)
         log.debug(e)
         pass
         pass
-
-    knowledge = Knowledges.update_knowledge_by_id(
-        id=id, form_data=KnowledgeUpdateForm(data={"file_ids": []})
-    )
-    return knowledge
+    result = Knowledges.delete_knowledge_by_id(id=id)
+    return result
 
 
 
 
 ############################
 ############################
-# DeleteKnowledgeById
+# ResetKnowledgeById
 ############################
 ############################
 
 
 
 
-@router.delete("/{id}/delete", response_model=bool)
-async def delete_knowledge_by_id(id: str, user=Depends(get_admin_user)):
+@router.post("/{id}/reset", response_model=Optional[KnowledgeResponse])
+async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
+    knowledge = Knowledges.get_knowledge_by_id(id=id)
+    if not knowledge:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if knowledge.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+        )
+
     try:
     try:
         VECTOR_DB_CLIENT.delete_collection(collection_name=id)
         VECTOR_DB_CLIENT.delete_collection(collection_name=id)
     except Exception as e:
     except Exception as e:
         log.debug(e)
         log.debug(e)
         pass
         pass
-    result = Knowledges.delete_knowledge_by_id(id=id)
-    return result
+
+    knowledge = Knowledges.update_knowledge_data_by_id(id=id.id, data={"file_ids": []})
+
+    return knowledge

+ 105 - 36
backend/open_webui/apps/webui/routers/models.py

@@ -8,49 +8,58 @@ from open_webui.apps.webui.models.models import (
 )
 )
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
 from fastapi import APIRouter, Depends, HTTPException, Request, status
 from fastapi import APIRouter, Depends, HTTPException, Request, status
+
+
 from open_webui.utils.utils import get_admin_user, get_verified_user
 from open_webui.utils.utils import get_admin_user, get_verified_user
+from open_webui.utils.access_control import has_access
+
 
 
 router = APIRouter()
 router = APIRouter()
 
 
+
 ###########################
 ###########################
-# getModels
+# GetModels
 ###########################
 ###########################
 
 
 
 
 @router.get("/", response_model=list[ModelResponse])
 @router.get("/", response_model=list[ModelResponse])
 async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
 async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
-    if id:
-        model = Models.get_model_by_id(id)
-        if model:
-            return [model]
-        else:
-            raise HTTPException(
-                status_code=status.HTTP_401_UNAUTHORIZED,
-                detail=ERROR_MESSAGES.NOT_FOUND,
-            )
+    if user.role == "admin":
+        return Models.get_models()
     else:
     else:
-        return Models.get_all_models()
+        return Models.get_models_by_user_id(user.id)
+
+
+###########################
+# GetBaseModels
+###########################
+
+
+@router.get("/base", response_model=list[ModelResponse])
+async def get_base_models(user=Depends(get_admin_user)):
+    return Models.get_base_models()
 
 
 
 
 ############################
 ############################
-# AddNewModel
+# CreateNewModel
 ############################
 ############################
 
 
 
 
-@router.post("/add", response_model=Optional[ModelModel])
-async def add_new_model(
-    request: Request,
+@router.post("/create", response_model=Optional[ModelModel])
+async def create_new_model(
     form_data: ModelForm,
     form_data: ModelForm,
-    user=Depends(get_admin_user),
+    user=Depends(get_verified_user),
 ):
 ):
-    if form_data.id in request.app.state.MODELS:
+
+    model = Models.get_model_by_id(form_data.id)
+    if model:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
             detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
         )
         )
+
     else:
     else:
         model = Models.insert_new_model(form_data, user.id)
         model = Models.insert_new_model(form_data, user.id)
-
         if model:
         if model:
             return model
             return model
         else:
         else:
@@ -60,37 +69,84 @@ async def add_new_model(
             )
             )
 
 
 
 
+###########################
+# GetModelById
+###########################
+
+
+@router.get("/id/{id}", response_model=Optional[ModelResponse])
+async def get_model_by_id(id: str, user=Depends(get_verified_user)):
+    model = Models.get_model_by_id(id)
+    if model:
+        if (
+            user.role == "admin"
+            or model.user_id == user.id
+            or has_access(user.id, "read", model.access_control)
+        ):
+            return model
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
 ############################
 ############################
-# UpdateModelById
+# ToggelModelById
 ############################
 ############################
 
 
 
 
-@router.post("/update", response_model=Optional[ModelModel])
-async def update_model_by_id(
-    request: Request,
-    id: str,
-    form_data: ModelForm,
-    user=Depends(get_admin_user),
-):
+@router.post("/id/{id}/toggle", response_model=Optional[ModelResponse])
+async def toggle_model_by_id(id: str, user=Depends(get_verified_user)):
     model = Models.get_model_by_id(id)
     model = Models.get_model_by_id(id)
     if model:
     if model:
-        model = Models.update_model_by_id(id, form_data)
-        return model
-    else:
-        if form_data.id in request.app.state.MODELS:
-            model = Models.insert_new_model(form_data, user.id)
+        if (
+            user.role == "admin"
+            or model.user_id == user.id
+            or has_access(user.id, "write", model.access_control)
+        ):
+            model = Models.toggle_model_by_id(id)
+
             if model:
             if model:
                 return model
                 return model
             else:
             else:
                 raise HTTPException(
                 raise HTTPException(
-                    status_code=status.HTTP_401_UNAUTHORIZED,
-                    detail=ERROR_MESSAGES.DEFAULT(),
+                    status_code=status.HTTP_400_BAD_REQUEST,
+                    detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
                 )
                 )
         else:
         else:
             raise HTTPException(
             raise HTTPException(
                 status_code=status.HTTP_401_UNAUTHORIZED,
                 status_code=status.HTTP_401_UNAUTHORIZED,
-                detail=ERROR_MESSAGES.DEFAULT(),
+                detail=ERROR_MESSAGES.UNAUTHORIZED,
             )
             )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# UpdateModelById
+############################
+
+
+@router.post("/id/{id}/update", response_model=Optional[ModelModel])
+async def update_model_by_id(
+    id: str,
+    form_data: ModelForm,
+    user=Depends(get_verified_user),
+):
+    model = Models.get_model_by_id(id)
+
+    if not model:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    model = Models.update_model_by_id(id, form_data)
+    return model
 
 
 
 
 ############################
 ############################
@@ -98,7 +154,20 @@ async def update_model_by_id(
 ############################
 ############################
 
 
 
 
-@router.delete("/delete", response_model=bool)
-async def delete_model_by_id(id: str, user=Depends(get_admin_user)):
+@router.delete("/id/{id}/delete", response_model=bool)
+async def delete_model_by_id(id: str, user=Depends(get_verified_user)):
+    model = Models.get_model_by_id(id)
+    if not model:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if model.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.UNAUTHORIZED,
+        )
+
     result = Models.delete_model_by_id(id)
     result = Models.delete_model_by_id(id)
     return result
     return result

+ 52 - 5
backend/open_webui/apps/webui/routers/prompts.py

@@ -4,6 +4,7 @@ from open_webui.apps.webui.models.prompts import PromptForm, PromptModel, Prompt
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
 from fastapi import APIRouter, Depends, HTTPException, status
 from fastapi import APIRouter, Depends, HTTPException, status
 from open_webui.utils.utils import get_admin_user, get_verified_user
 from open_webui.utils.utils import get_admin_user, get_verified_user
+from open_webui.utils.access_control import has_access
 
 
 router = APIRouter()
 router = APIRouter()
 
 
@@ -14,7 +15,22 @@ router = APIRouter()
 
 
 @router.get("/", response_model=list[PromptModel])
 @router.get("/", response_model=list[PromptModel])
 async def get_prompts(user=Depends(get_verified_user)):
 async def get_prompts(user=Depends(get_verified_user)):
-    return Prompts.get_prompts()
+    if user.role == "admin":
+        prompts = Prompts.get_prompts()
+    else:
+        prompts = Prompts.get_prompts_by_user_id(user.id, "read")
+
+    return prompts
+
+
+@router.get("/list", response_model=list[PromptModel])
+async def get_prompt_list(user=Depends(get_verified_user)):
+    if user.role == "admin":
+        prompts = Prompts.get_prompts()
+    else:
+        prompts = Prompts.get_prompts_by_user_id(user.id, "write")
+
+    return prompts
 
 
 
 
 ############################
 ############################
@@ -23,7 +39,7 @@ async def get_prompts(user=Depends(get_verified_user)):
 
 
 
 
 @router.post("/create", response_model=Optional[PromptModel])
 @router.post("/create", response_model=Optional[PromptModel])
-async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user)):
+async def create_new_prompt(form_data: PromptForm, user=Depends(get_verified_user)):
     prompt = Prompts.get_prompt_by_command(form_data.command)
     prompt = Prompts.get_prompt_by_command(form_data.command)
     if prompt is None:
     if prompt is None:
         prompt = Prompts.insert_new_prompt(user.id, form_data)
         prompt = Prompts.insert_new_prompt(user.id, form_data)
@@ -50,7 +66,12 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
     prompt = Prompts.get_prompt_by_command(f"/{command}")
     prompt = Prompts.get_prompt_by_command(f"/{command}")
 
 
     if prompt:
     if prompt:
-        return prompt
+        if (
+            user.role == "admin"
+            or prompt.user_id == user.id
+            or has_access(user.id, "read", prompt.access_control)
+        ):
+            return prompt
     else:
     else:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             status_code=status.HTTP_401_UNAUTHORIZED,
@@ -67,8 +88,21 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
 async def update_prompt_by_command(
 async def update_prompt_by_command(
     command: str,
     command: str,
     form_data: PromptForm,
     form_data: PromptForm,
-    user=Depends(get_admin_user),
+    user=Depends(get_verified_user),
 ):
 ):
+    prompt = Prompts.get_prompt_by_command(f"/{command}")
+    if not prompt:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if prompt.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+        )
+
     prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
     prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
     if prompt:
     if prompt:
         return prompt
         return prompt
@@ -85,6 +119,19 @@ async def update_prompt_by_command(
 
 
 
 
 @router.delete("/command/{command}/delete", response_model=bool)
 @router.delete("/command/{command}/delete", response_model=bool)
-async def delete_prompt_by_command(command: str, user=Depends(get_admin_user)):
+async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)):
+    prompt = Prompts.get_prompt_by_command(f"/{command}")
+    if not prompt:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if prompt.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+        )
+
     result = Prompts.delete_prompt_by_command(f"/{command}")
     result = Prompts.delete_prompt_by_command(f"/{command}")
     return result
     return result

+ 128 - 78
backend/open_webui/apps/webui/routers/tools.py

@@ -3,48 +3,66 @@ from pathlib import Path
 from typing import Optional
 from typing import Optional
 
 
 from open_webui.apps.webui.models.tools import ToolForm, ToolModel, ToolResponse, Tools
 from open_webui.apps.webui.models.tools import ToolForm, ToolModel, ToolResponse, Tools
-from open_webui.apps.webui.utils import load_toolkit_module_by_id, replace_imports
+from open_webui.apps.webui.utils import load_tools_module_by_id, replace_imports
 from open_webui.config import CACHE_DIR, DATA_DIR
 from open_webui.config import CACHE_DIR, DATA_DIR
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
 from fastapi import APIRouter, Depends, HTTPException, Request, status
 from fastapi import APIRouter, Depends, HTTPException, Request, status
 from open_webui.utils.tools import get_tools_specs
 from open_webui.utils.tools import get_tools_specs
 from open_webui.utils.utils import get_admin_user, get_verified_user
 from open_webui.utils.utils import get_admin_user, get_verified_user
+from open_webui.utils.access_control import has_access
 
 
 
 
 router = APIRouter()
 router = APIRouter()
 
 
 ############################
 ############################
-# GetToolkits
+# GetTools
 ############################
 ############################
 
 
 
 
 @router.get("/", response_model=list[ToolResponse])
 @router.get("/", response_model=list[ToolResponse])
-async def get_toolkits(user=Depends(get_verified_user)):
-    toolkits = [toolkit for toolkit in Tools.get_tools()]
-    return toolkits
+async def get_tools(user=Depends(get_verified_user)):
+    if user.role == "admin":
+        tools = Tools.get_tools()
+    else:
+        tools = Tools.get_tools_by_user_id(user.id, "read")
+    return tools
+
+
+############################
+# GetToolList
+############################
+
+
+@router.get("/list", response_model=list[ToolResponse])
+async def get_tool_list(user=Depends(get_verified_user)):
+    if user.role == "admin":
+        tools = Tools.get_tools()
+    else:
+        tools = Tools.get_tools_by_user_id(user.id, "write")
+    return tools
 
 
 
 
 ############################
 ############################
-# ExportToolKits
+# ExportTools
 ############################
 ############################
 
 
 
 
 @router.get("/export", response_model=list[ToolModel])
 @router.get("/export", response_model=list[ToolModel])
-async def get_toolkits(user=Depends(get_admin_user)):
-    toolkits = [toolkit for toolkit in Tools.get_tools()]
-    return toolkits
+async def export_tools(user=Depends(get_admin_user)):
+    tools = Tools.get_tools()
+    return tools
 
 
 
 
 ############################
 ############################
-# CreateNewToolKit
+# CreateNewTools
 ############################
 ############################
 
 
 
 
 @router.post("/create", response_model=Optional[ToolResponse])
 @router.post("/create", response_model=Optional[ToolResponse])
-async def create_new_toolkit(
+async def create_new_tools(
     request: Request,
     request: Request,
     form_data: ToolForm,
     form_data: ToolForm,
-    user=Depends(get_admin_user),
+    user=Depends(get_verified_user),
 ):
 ):
     if not form_data.id.isidentifier():
     if not form_data.id.isidentifier():
         raise HTTPException(
         raise HTTPException(
@@ -54,30 +72,30 @@ async def create_new_toolkit(
 
 
     form_data.id = form_data.id.lower()
     form_data.id = form_data.id.lower()
 
 
-    toolkit = Tools.get_tool_by_id(form_data.id)
-    if toolkit is None:
+    tools = Tools.get_tool_by_id(form_data.id)
+    if tools is None:
         try:
         try:
             form_data.content = replace_imports(form_data.content)
             form_data.content = replace_imports(form_data.content)
-            toolkit_module, frontmatter = load_toolkit_module_by_id(
+            tools_module, frontmatter = load_tools_module_by_id(
                 form_data.id, content=form_data.content
                 form_data.id, content=form_data.content
             )
             )
             form_data.meta.manifest = frontmatter
             form_data.meta.manifest = frontmatter
 
 
             TOOLS = request.app.state.TOOLS
             TOOLS = request.app.state.TOOLS
-            TOOLS[form_data.id] = toolkit_module
+            TOOLS[form_data.id] = tools_module
 
 
             specs = get_tools_specs(TOOLS[form_data.id])
             specs = get_tools_specs(TOOLS[form_data.id])
-            toolkit = Tools.insert_new_tool(user.id, form_data, specs)
+            tools = Tools.insert_new_tool(user.id, form_data, specs)
 
 
             tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
             tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
             tool_cache_dir.mkdir(parents=True, exist_ok=True)
             tool_cache_dir.mkdir(parents=True, exist_ok=True)
 
 
-            if toolkit:
-                return toolkit
+            if tools:
+                return tools
             else:
             else:
                 raise HTTPException(
                 raise HTTPException(
                     status_code=status.HTTP_400_BAD_REQUEST,
                     status_code=status.HTTP_400_BAD_REQUEST,
-                    detail=ERROR_MESSAGES.DEFAULT("Error creating toolkit"),
+                    detail=ERROR_MESSAGES.DEFAULT("Error creating tools"),
                 )
                 )
         except Exception as e:
         except Exception as e:
             print(e)
             print(e)
@@ -93,16 +111,21 @@ async def create_new_toolkit(
 
 
 
 
 ############################
 ############################
-# GetToolkitById
+# GetToolsById
 ############################
 ############################
 
 
 
 
 @router.get("/id/{id}", response_model=Optional[ToolModel])
 @router.get("/id/{id}", response_model=Optional[ToolModel])
-async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
-    toolkit = Tools.get_tool_by_id(id)
-
-    if toolkit:
-        return toolkit
+async def get_tools_by_id(id: str, user=Depends(get_verified_user)):
+    tools = Tools.get_tool_by_id(id)
+
+    if tools:
+        if (
+            user.role == "admin"
+            or tools.user_id == user.id
+            or has_access(user.id, "read", tools.access_control)
+        ):
+            return tools
     else:
     else:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             status_code=status.HTTP_401_UNAUTHORIZED,
@@ -111,26 +134,39 @@ async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
 
 
 
 
 ############################
 ############################
-# UpdateToolkitById
+# UpdateToolsById
 ############################
 ############################
 
 
 
 
 @router.post("/id/{id}/update", response_model=Optional[ToolModel])
 @router.post("/id/{id}/update", response_model=Optional[ToolModel])
-async def update_toolkit_by_id(
+async def update_tools_by_id(
     request: Request,
     request: Request,
     id: str,
     id: str,
     form_data: ToolForm,
     form_data: ToolForm,
-    user=Depends(get_admin_user),
+    user=Depends(get_verified_user),
 ):
 ):
+    tools = Tools.get_tool_by_id(id)
+    if not tools:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if tools.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.UNAUTHORIZED,
+        )
+
     try:
     try:
         form_data.content = replace_imports(form_data.content)
         form_data.content = replace_imports(form_data.content)
-        toolkit_module, frontmatter = load_toolkit_module_by_id(
+        tools_module, frontmatter = load_tools_module_by_id(
             id, content=form_data.content
             id, content=form_data.content
         )
         )
         form_data.meta.manifest = frontmatter
         form_data.meta.manifest = frontmatter
 
 
         TOOLS = request.app.state.TOOLS
         TOOLS = request.app.state.TOOLS
-        TOOLS[id] = toolkit_module
+        TOOLS[id] = tools_module
 
 
         specs = get_tools_specs(TOOLS[id])
         specs = get_tools_specs(TOOLS[id])
 
 
@@ -140,14 +176,14 @@ async def update_toolkit_by_id(
         }
         }
 
 
         print(updated)
         print(updated)
-        toolkit = Tools.update_tool_by_id(id, updated)
+        tools = Tools.update_tool_by_id(id, updated)
 
 
-        if toolkit:
-            return toolkit
+        if tools:
+            return tools
         else:
         else:
             raise HTTPException(
             raise HTTPException(
                 status_code=status.HTTP_400_BAD_REQUEST,
                 status_code=status.HTTP_400_BAD_REQUEST,
-                detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"),
+                detail=ERROR_MESSAGES.DEFAULT("Error updating tools"),
             )
             )
 
 
     except Exception as e:
     except Exception as e:
@@ -158,14 +194,28 @@ async def update_toolkit_by_id(
 
 
 
 
 ############################
 ############################
-# DeleteToolkitById
+# DeleteToolsById
 ############################
 ############################
 
 
 
 
 @router.delete("/id/{id}/delete", response_model=bool)
 @router.delete("/id/{id}/delete", response_model=bool)
-async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)):
-    result = Tools.delete_tool_by_id(id)
+async def delete_tools_by_id(
+    request: Request, id: str, user=Depends(get_verified_user)
+):
+    tools = Tools.get_tool_by_id(id)
+    if not tools:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+    if tools.user_id != user.id and user.role != "admin":
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.UNAUTHORIZED,
+        )
 
 
+    result = Tools.delete_tool_by_id(id)
     if result:
     if result:
         TOOLS = request.app.state.TOOLS
         TOOLS = request.app.state.TOOLS
         if id in TOOLS:
         if id in TOOLS:
@@ -180,9 +230,9 @@ async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin
 
 
 
 
 @router.get("/id/{id}/valves", response_model=Optional[dict])
 @router.get("/id/{id}/valves", response_model=Optional[dict])
-async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
-    toolkit = Tools.get_tool_by_id(id)
-    if toolkit:
+async def get_tools_valves_by_id(id: str, user=Depends(get_verified_user)):
+    tools = Tools.get_tool_by_id(id)
+    if tools:
         try:
         try:
             valves = Tools.get_tool_valves_by_id(id)
             valves = Tools.get_tool_valves_by_id(id)
             return valves
             return valves
@@ -204,19 +254,19 @@ async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
 
 
 
 
 @router.get("/id/{id}/valves/spec", response_model=Optional[dict])
 @router.get("/id/{id}/valves/spec", response_model=Optional[dict])
-async def get_toolkit_valves_spec_by_id(
-    request: Request, id: str, user=Depends(get_admin_user)
+async def get_tools_valves_spec_by_id(
+    request: Request, id: str, user=Depends(get_verified_user)
 ):
 ):
-    toolkit = Tools.get_tool_by_id(id)
-    if toolkit:
+    tools = Tools.get_tool_by_id(id)
+    if tools:
         if id in request.app.state.TOOLS:
         if id in request.app.state.TOOLS:
-            toolkit_module = request.app.state.TOOLS[id]
+            tools_module = request.app.state.TOOLS[id]
         else:
         else:
-            toolkit_module, _ = load_toolkit_module_by_id(id)
-            request.app.state.TOOLS[id] = toolkit_module
+            tools_module, _ = load_tools_module_by_id(id)
+            request.app.state.TOOLS[id] = tools_module
 
 
-        if hasattr(toolkit_module, "Valves"):
-            Valves = toolkit_module.Valves
+        if hasattr(tools_module, "Valves"):
+            Valves = tools_module.Valves
             return Valves.schema()
             return Valves.schema()
         return None
         return None
     else:
     else:
@@ -232,19 +282,19 @@ async def get_toolkit_valves_spec_by_id(
 
 
 
 
 @router.post("/id/{id}/valves/update", response_model=Optional[dict])
 @router.post("/id/{id}/valves/update", response_model=Optional[dict])
-async def update_toolkit_valves_by_id(
-    request: Request, id: str, form_data: dict, user=Depends(get_admin_user)
+async def update_tools_valves_by_id(
+    request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
 ):
 ):
-    toolkit = Tools.get_tool_by_id(id)
-    if toolkit:
+    tools = Tools.get_tool_by_id(id)
+    if tools:
         if id in request.app.state.TOOLS:
         if id in request.app.state.TOOLS:
-            toolkit_module = request.app.state.TOOLS[id]
+            tools_module = request.app.state.TOOLS[id]
         else:
         else:
-            toolkit_module, _ = load_toolkit_module_by_id(id)
-            request.app.state.TOOLS[id] = toolkit_module
+            tools_module, _ = load_tools_module_by_id(id)
+            request.app.state.TOOLS[id] = tools_module
 
 
-        if hasattr(toolkit_module, "Valves"):
-            Valves = toolkit_module.Valves
+        if hasattr(tools_module, "Valves"):
+            Valves = tools_module.Valves
 
 
             try:
             try:
                 form_data = {k: v for k, v in form_data.items() if v is not None}
                 form_data = {k: v for k, v in form_data.items() if v is not None}
@@ -276,9 +326,9 @@ async def update_toolkit_valves_by_id(
 
 
 
 
 @router.get("/id/{id}/valves/user", response_model=Optional[dict])
 @router.get("/id/{id}/valves/user", response_model=Optional[dict])
-async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)):
-    toolkit = Tools.get_tool_by_id(id)
-    if toolkit:
+async def get_tools_user_valves_by_id(id: str, user=Depends(get_verified_user)):
+    tools = Tools.get_tool_by_id(id)
+    if tools:
         try:
         try:
             user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id)
             user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id)
             return user_valves
             return user_valves
@@ -295,19 +345,19 @@ async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)
 
 
 
 
 @router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
 @router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
-async def get_toolkit_user_valves_spec_by_id(
+async def get_tools_user_valves_spec_by_id(
     request: Request, id: str, user=Depends(get_verified_user)
     request: Request, id: str, user=Depends(get_verified_user)
 ):
 ):
-    toolkit = Tools.get_tool_by_id(id)
-    if toolkit:
+    tools = Tools.get_tool_by_id(id)
+    if tools:
         if id in request.app.state.TOOLS:
         if id in request.app.state.TOOLS:
-            toolkit_module = request.app.state.TOOLS[id]
+            tools_module = request.app.state.TOOLS[id]
         else:
         else:
-            toolkit_module, _ = load_toolkit_module_by_id(id)
-            request.app.state.TOOLS[id] = toolkit_module
+            tools_module, _ = load_tools_module_by_id(id)
+            request.app.state.TOOLS[id] = tools_module
 
 
-        if hasattr(toolkit_module, "UserValves"):
-            UserValves = toolkit_module.UserValves
+        if hasattr(tools_module, "UserValves"):
+            UserValves = tools_module.UserValves
             return UserValves.schema()
             return UserValves.schema()
         return None
         return None
     else:
     else:
@@ -318,20 +368,20 @@ async def get_toolkit_user_valves_spec_by_id(
 
 
 
 
 @router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
 @router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
-async def update_toolkit_user_valves_by_id(
+async def update_tools_user_valves_by_id(
     request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
     request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
 ):
 ):
-    toolkit = Tools.get_tool_by_id(id)
+    tools = Tools.get_tool_by_id(id)
 
 
-    if toolkit:
+    if tools:
         if id in request.app.state.TOOLS:
         if id in request.app.state.TOOLS:
-            toolkit_module = request.app.state.TOOLS[id]
+            tools_module = request.app.state.TOOLS[id]
         else:
         else:
-            toolkit_module, _ = load_toolkit_module_by_id(id)
-            request.app.state.TOOLS[id] = toolkit_module
+            tools_module, _ = load_tools_module_by_id(id)
+            request.app.state.TOOLS[id] = tools_module
 
 
-        if hasattr(toolkit_module, "UserValves"):
-            UserValves = toolkit_module.UserValves
+        if hasattr(tools_module, "UserValves"):
+            UserValves = tools_module.UserValves
 
 
             try:
             try:
                 form_data = {k: v for k, v in form_data.items() if v is not None}
                 form_data = {k: v for k, v in form_data.items() if v is not None}

+ 41 - 4
backend/open_webui/apps/webui/routers/users.py

@@ -31,21 +31,58 @@ async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)
     return Users.get_users(skip, limit)
     return Users.get_users(skip, limit)
 
 
 
 
+############################
+# User Groups
+############################
+
+
+@router.get("/groups")
+async def get_user_groups(user=Depends(get_verified_user)):
+    return Users.get_user_groups(user.id)
+
+
 ############################
 ############################
 # User Permissions
 # User Permissions
 ############################
 ############################
 
 
 
 
-@router.get("/permissions/user")
+@router.get("/permissions")
+async def get_user_permissisions(user=Depends(get_verified_user)):
+    return Users.get_user_groups(user.id)
+
+
+############################
+# User Default Permissions
+############################
+class WorkspacePermissions(BaseModel):
+    models: bool
+    knowledge: bool
+    prompts: bool
+    tools: bool
+
+
+class ChatPermissions(BaseModel):
+    file_upload: bool
+    delete: bool
+    edit: bool
+    temporary: bool
+
+
+class UserPermissions(BaseModel):
+    workspace: WorkspacePermissions
+    chat: ChatPermissions
+
+
+@router.get("/default/permissions")
 async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
 async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
     return request.app.state.config.USER_PERMISSIONS
     return request.app.state.config.USER_PERMISSIONS
 
 
 
 
-@router.post("/permissions/user")
+@router.post("/default/permissions")
 async def update_user_permissions(
 async def update_user_permissions(
-    request: Request, form_data: dict, user=Depends(get_admin_user)
+    request: Request, form_data: UserPermissions, user=Depends(get_admin_user)
 ):
 ):
-    request.app.state.config.USER_PERMISSIONS = form_data
+    request.app.state.config.USER_PERMISSIONS = form_data.model_dump()
     return request.app.state.config.USER_PERMISSIONS
     return request.app.state.config.USER_PERMISSIONS
 
 
 
 

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

@@ -63,7 +63,7 @@ def replace_imports(content):
     return content
     return content
 
 
 
 
-def load_toolkit_module_by_id(toolkit_id, content=None):
+def load_tools_module_by_id(toolkit_id, content=None):
 
 
     if content is None:
     if content is None:
         tool = Tools.get_tool_by_id(toolkit_id)
         tool = Tools.get_tool_by_id(toolkit_id)

+ 39 - 20
backend/open_webui/config.py

@@ -739,12 +739,36 @@ DEFAULT_USER_ROLE = PersistentConfig(
     os.getenv("DEFAULT_USER_ROLE", "pending"),
     os.getenv("DEFAULT_USER_ROLE", "pending"),
 )
 )
 
 
-USER_PERMISSIONS_CHAT_DELETION = (
-    os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
+
+USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
+    os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
+    == "true"
+)
+
+USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = (
+    os.environ.get("USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS", "False").lower()
+    == "true"
+)
+
+USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = (
+    os.environ.get("USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS", "False").lower()
+    == "true"
+)
+
+USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = (
+    os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS", "False").lower() == "true"
+)
+
+USER_PERMISSIONS_CHAT_FILE_UPLOAD = (
+    os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
 )
 )
 
 
-USER_PERMISSIONS_CHAT_EDITING = (
-    os.environ.get("USER_PERMISSIONS_CHAT_EDITING", "True").lower() == "true"
+USER_PERMISSIONS_CHAT_DELETE = (
+    os.environ.get("USER_PERMISSIONS_CHAT_DELETE", "True").lower() == "true"
+)
+
+USER_PERMISSIONS_CHAT_EDIT = (
+    os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
 )
 )
 
 
 USER_PERMISSIONS_CHAT_TEMPORARY = (
 USER_PERMISSIONS_CHAT_TEMPORARY = (
@@ -753,13 +777,20 @@ USER_PERMISSIONS_CHAT_TEMPORARY = (
 
 
 USER_PERMISSIONS = PersistentConfig(
 USER_PERMISSIONS = PersistentConfig(
     "USER_PERMISSIONS",
     "USER_PERMISSIONS",
-    "ui.user_permissions",
+    "user.permissions",
     {
     {
+        "workspace": {
+            "models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
+            "knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS,
+            "prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS,
+            "tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS,
+        },
         "chat": {
         "chat": {
-            "deletion": USER_PERMISSIONS_CHAT_DELETION,
-            "editing": USER_PERMISSIONS_CHAT_EDITING,
+            "file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
+            "delete": USER_PERMISSIONS_CHAT_DELETE,
+            "edit": USER_PERMISSIONS_CHAT_EDIT,
             "temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
             "temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
-        }
+        },
     },
     },
 )
 )
 
 
@@ -785,18 +816,6 @@ DEFAULT_ARENA_MODEL = {
     },
     },
 }
 }
 
 
-ENABLE_MODEL_FILTER = PersistentConfig(
-    "ENABLE_MODEL_FILTER",
-    "model_filter.enable",
-    os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true",
-)
-MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
-MODEL_FILTER_LIST = PersistentConfig(
-    "MODEL_FILTER_LIST",
-    "model_filter.list",
-    [model.strip() for model in MODEL_FILTER_LIST.split(";")],
-)
-
 WEBHOOK_URL = PersistentConfig(
 WEBHOOK_URL = PersistentConfig(
     "WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
     "WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
 )
 )

+ 257 - 191
backend/open_webui/main.py

@@ -11,6 +11,7 @@ import random
 from contextlib import asynccontextmanager
 from contextlib import asynccontextmanager
 from typing import Optional
 from typing import Optional
 
 
+from aiocache import cached
 import aiohttp
 import aiohttp
 import requests
 import requests
 from fastapi import (
 from fastapi import (
@@ -45,6 +46,7 @@ from open_webui.apps.openai.main import (
     app as openai_app,
     app as openai_app,
     generate_chat_completion as generate_openai_chat_completion,
     generate_chat_completion as generate_openai_chat_completion,
     get_all_models as get_openai_models,
     get_all_models as get_openai_models,
+    get_all_models_responses as get_openai_models_responses,
 )
 )
 from open_webui.apps.retrieval.main import app as retrieval_app
 from open_webui.apps.retrieval.main import app as retrieval_app
 from open_webui.apps.retrieval.utils import get_rag_context, rag_template
 from open_webui.apps.retrieval.utils import get_rag_context, rag_template
@@ -70,13 +72,11 @@ from open_webui.config import (
     DEFAULT_LOCALE,
     DEFAULT_LOCALE,
     ENABLE_ADMIN_CHAT_ACCESS,
     ENABLE_ADMIN_CHAT_ACCESS,
     ENABLE_ADMIN_EXPORT,
     ENABLE_ADMIN_EXPORT,
-    ENABLE_MODEL_FILTER,
     ENABLE_OLLAMA_API,
     ENABLE_OLLAMA_API,
     ENABLE_OPENAI_API,
     ENABLE_OPENAI_API,
     ENABLE_TAGS_GENERATION,
     ENABLE_TAGS_GENERATION,
     ENV,
     ENV,
     FRONTEND_BUILD_DIR,
     FRONTEND_BUILD_DIR,
-    MODEL_FILTER_LIST,
     OAUTH_PROVIDERS,
     OAUTH_PROVIDERS,
     ENABLE_SEARCH_QUERY,
     ENABLE_SEARCH_QUERY,
     SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
     SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
@@ -135,6 +135,7 @@ from open_webui.utils.utils import (
     get_http_authorization_cred,
     get_http_authorization_cred,
     get_verified_user,
     get_verified_user,
 )
 )
+from open_webui.utils.access_control import has_access
 
 
 if SAFE_MODE:
 if SAFE_MODE:
     print("SAFE MODE ENABLED")
     print("SAFE MODE ENABLED")
@@ -183,7 +184,10 @@ async def lifespan(app: FastAPI):
 
 
 
 
 app = FastAPI(
 app = FastAPI(
-    docs_url="/docs" if ENV == "dev" else None, openapi_url="/openapi.json" if ENV == "dev" else None, redoc_url=None, lifespan=lifespan
+    docs_url="/docs" if ENV == "dev" else None,
+    openapi_url="/openapi.json" if ENV == "dev" else None,
+    redoc_url=None,
+    lifespan=lifespan,
 )
 )
 
 
 app.state.config = AppConfig()
 app.state.config = AppConfig()
@@ -191,27 +195,26 @@ app.state.config = AppConfig()
 app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
 app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
 app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
 app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
 
 
-app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
-app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
-
 app.state.config.WEBHOOK_URL = WEBHOOK_URL
 app.state.config.WEBHOOK_URL = WEBHOOK_URL
 
 
 app.state.config.TASK_MODEL = TASK_MODEL
 app.state.config.TASK_MODEL = TASK_MODEL
 app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
 app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
+
 app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
 app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
-app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
+
 app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
 app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
+app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
+
+
+app.state.config.ENABLE_SEARCH_QUERY = ENABLE_SEARCH_QUERY
 app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
 app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
     SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
     SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
 )
 )
-app.state.config.ENABLE_SEARCH_QUERY = ENABLE_SEARCH_QUERY
+
 app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
 app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
     TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
     TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
 )
 )
 
 
-app.state.MODELS = {}
-
-
 ##################################
 ##################################
 #
 #
 # ChatCompletion Middleware
 # ChatCompletion Middleware
@@ -219,26 +222,6 @@ app.state.MODELS = {}
 ##################################
 ##################################
 
 
 
 
-def get_task_model_id(default_model_id):
-    # Set the task model
-    task_model_id = default_model_id
-    # Check if the user has a custom task model and use that model
-    if app.state.MODELS[task_model_id]["owned_by"] == "ollama":
-        if (
-            app.state.config.TASK_MODEL
-            and app.state.config.TASK_MODEL in app.state.MODELS
-        ):
-            task_model_id = app.state.config.TASK_MODEL
-    else:
-        if (
-            app.state.config.TASK_MODEL_EXTERNAL
-            and app.state.config.TASK_MODEL_EXTERNAL in app.state.MODELS
-        ):
-            task_model_id = app.state.config.TASK_MODEL_EXTERNAL
-
-    return task_model_id
-
-
 def get_filter_function_ids(model):
 def get_filter_function_ids(model):
     def get_priority(function_id):
     def get_priority(function_id):
         function = Functions.get_function_by_id(function_id)
         function = Functions.get_function_by_id(function_id)
@@ -368,8 +351,24 @@ async def get_content_from_response(response) -> Optional[str]:
     return content
     return content
 
 
 
 
+def get_task_model_id(
+    default_model_id: str, task_model: str, task_model_external: str, models
+) -> str:
+    # Set the task model
+    task_model_id = default_model_id
+    # Check if the user has a custom task model and use that model
+    if models[task_model_id]["owned_by"] == "ollama":
+        if task_model and task_model in models:
+            task_model_id = task_model
+    else:
+        if task_model_external and task_model_external in models:
+            task_model_id = task_model_external
+
+    return task_model_id
+
+
 async def chat_completion_tools_handler(
 async def chat_completion_tools_handler(
-    body: dict, user: UserModel, extra_params: dict
+    body: dict, user: UserModel, models, extra_params: dict
 ) -> tuple[dict, dict]:
 ) -> tuple[dict, dict]:
     # If tool_ids field is present, call the functions
     # If tool_ids field is present, call the functions
     metadata = body.get("metadata", {})
     metadata = body.get("metadata", {})
@@ -383,14 +382,19 @@ async def chat_completion_tools_handler(
     contexts = []
     contexts = []
     citations = []
     citations = []
 
 
-    task_model_id = get_task_model_id(body["model"])
+    task_model_id = get_task_model_id(
+        body["model"],
+        app.state.config.TASK_MODEL,
+        app.state.config.TASK_MODEL_EXTERNAL,
+        models,
+    )
     tools = get_tools(
     tools = get_tools(
         webui_app,
         webui_app,
         tool_ids,
         tool_ids,
         user,
         user,
         {
         {
             **extra_params,
             **extra_params,
-            "__model__": app.state.MODELS[task_model_id],
+            "__model__": models[task_model_id],
             "__messages__": body["messages"],
             "__messages__": body["messages"],
             "__files__": metadata.get("files", []),
             "__files__": metadata.get("files", []),
         },
         },
@@ -414,7 +418,7 @@ async def chat_completion_tools_handler(
     )
     )
 
 
     try:
     try:
-        payload = filter_pipeline(payload, user)
+        payload = filter_pipeline(payload, user, models)
     except Exception as e:
     except Exception as e:
         raise e
         raise e
 
 
@@ -515,16 +519,16 @@ def is_chat_completion_request(request):
     )
     )
 
 
 
 
-async def get_body_and_model_and_user(request):
+async def get_body_and_model_and_user(request, models):
     # Read the original request body
     # Read the original request body
     body = await request.body()
     body = await request.body()
     body_str = body.decode("utf-8")
     body_str = body.decode("utf-8")
     body = json.loads(body_str) if body_str else {}
     body = json.loads(body_str) if body_str else {}
 
 
     model_id = body["model"]
     model_id = body["model"]
-    if model_id not in app.state.MODELS:
+    if model_id not in models:
         raise Exception("Model not found")
         raise Exception("Model not found")
-    model = app.state.MODELS[model_id]
+    model = models[model_id]
 
 
     user = get_current_user(
     user = get_current_user(
         request,
         request,
@@ -540,14 +544,27 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
             return await call_next(request)
             return await call_next(request)
         log.debug(f"request.url.path: {request.url.path}")
         log.debug(f"request.url.path: {request.url.path}")
 
 
+        model_list = await get_all_models()
+        models = {model["id"]: model for model in model_list}
+
         try:
         try:
-            body, model, user = await get_body_and_model_and_user(request)
+            body, model, user = await get_body_and_model_and_user(request, models)
         except Exception as e:
         except Exception as e:
             return JSONResponse(
             return JSONResponse(
                 status_code=status.HTTP_400_BAD_REQUEST,
                 status_code=status.HTTP_400_BAD_REQUEST,
                 content={"detail": str(e)},
                 content={"detail": str(e)},
             )
             )
 
 
+        model_info = Models.get_model_by_id(model["id"])
+        if user.role == "user":
+            if model_info and not has_access(
+                user.id, type="read", access_control=model_info.access_control
+            ):
+                return JSONResponse(
+                    status_code=status.HTTP_403_FORBIDDEN,
+                    content={"detail": "User does not have access to the model"},
+                )
+
         metadata = {
         metadata = {
             "chat_id": body.pop("chat_id", None),
             "chat_id": body.pop("chat_id", None),
             "message_id": body.pop("id", None),
             "message_id": body.pop("id", None),
@@ -584,15 +601,20 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
                 content={"detail": str(e)},
                 content={"detail": str(e)},
             )
             )
 
 
+        tool_ids = body.pop("tool_ids", None)
+        files = body.pop("files", None)
+
         metadata = {
         metadata = {
             **metadata,
             **metadata,
-            "tool_ids": body.pop("tool_ids", None),
-            "files": body.pop("files", None),
+            "tool_ids": tool_ids,
+            "files": files,
         }
         }
         body["metadata"] = metadata
         body["metadata"] = metadata
 
 
         try:
         try:
-            body, flags = await chat_completion_tools_handler(body, user, extra_params)
+            body, flags = await chat_completion_tools_handler(
+                body, user, models, extra_params
+            )
             contexts.extend(flags.get("contexts", []))
             contexts.extend(flags.get("contexts", []))
             citations.extend(flags.get("citations", []))
             citations.extend(flags.get("citations", []))
         except Exception as e:
         except Exception as e:
@@ -689,10 +711,10 @@ app.add_middleware(ChatCompletionMiddleware)
 ##################################
 ##################################
 
 
 
 
-def get_sorted_filters(model_id):
+def get_sorted_filters(model_id, models):
     filters = [
     filters = [
         model
         model
-        for model in app.state.MODELS.values()
+        for model in models.values()
         if "pipeline" in model
         if "pipeline" in model
         and "type" in model["pipeline"]
         and "type" in model["pipeline"]
         and model["pipeline"]["type"] == "filter"
         and model["pipeline"]["type"] == "filter"
@@ -708,12 +730,12 @@ def get_sorted_filters(model_id):
     return sorted_filters
     return sorted_filters
 
 
 
 
-def filter_pipeline(payload, user):
+def filter_pipeline(payload, user, models):
     user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
     user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
     model_id = payload["model"]
     model_id = payload["model"]
-    sorted_filters = get_sorted_filters(model_id)
 
 
-    model = app.state.MODELS[model_id]
+    sorted_filters = get_sorted_filters(model_id, models)
+    model = models[model_id]
 
 
     if "pipeline" in model:
     if "pipeline" in model:
         sorted_filters.append(model)
         sorted_filters.append(model)
@@ -784,8 +806,11 @@ class PipelineMiddleware(BaseHTTPMiddleware):
                     content={"detail": "Not authenticated"},
                     content={"detail": "Not authenticated"},
                 )
                 )
 
 
+        model_list = await get_all_models()
+        models = {model["id"]: model for model in model_list}
+
         try:
         try:
-            data = filter_pipeline(data, user)
+            data = filter_pipeline(data, user, models)
         except Exception as e:
         except Exception as e:
             if len(e.args) > 1:
             if len(e.args) > 1:
                 return JSONResponse(
                 return JSONResponse(
@@ -864,16 +889,10 @@ async def commit_session_after_request(request: Request, call_next):
 
 
 @app.middleware("http")
 @app.middleware("http")
 async def check_url(request: Request, call_next):
 async def check_url(request: Request, call_next):
-    if len(app.state.MODELS) == 0:
-        await get_all_models()
-    else:
-        pass
-
     start_time = int(time.time())
     start_time = int(time.time())
     response = await call_next(request)
     response = await call_next(request)
     process_time = int(time.time()) - start_time
     process_time = int(time.time()) - start_time
     response.headers["X-Process-Time"] = str(process_time)
     response.headers["X-Process-Time"] = str(process_time)
-
     return response
     return response
 
 
 
 
@@ -913,12 +932,10 @@ app.mount("/retrieval/api/v1", retrieval_app)
 
 
 app.mount("/api/v1", webui_app)
 app.mount("/api/v1", webui_app)
 
 
-
 webui_app.state.EMBEDDING_FUNCTION = retrieval_app.state.EMBEDDING_FUNCTION
 webui_app.state.EMBEDDING_FUNCTION = retrieval_app.state.EMBEDDING_FUNCTION
 
 
 
 
-async def get_all_models():
-    # TODO: Optimize this function
+async def get_all_base_models():
     open_webui_models = []
     open_webui_models = []
     openai_models = []
     openai_models = []
     ollama_models = []
     ollama_models = []
@@ -944,9 +961,15 @@ async def get_all_models():
     open_webui_models = await get_open_webui_models()
     open_webui_models = await get_open_webui_models()
 
 
     models = open_webui_models + openai_models + ollama_models
     models = open_webui_models + openai_models + ollama_models
+    return models
+
+
+@cached(ttl=1)
+async def get_all_models():
+    models = await get_all_base_models()
 
 
     # If there are no models, return an empty list
     # If there are no models, return an empty list
-    if len([model for model in models if model["owned_by"] != "arena"]) == 0:
+    if len([model for model in models if not model.get("arena", False)]) == 0:
         return []
         return []
 
 
     global_action_ids = [
     global_action_ids = [
@@ -965,15 +988,23 @@ async def get_all_models():
                     custom_model.id == model["id"]
                     custom_model.id == model["id"]
                     or custom_model.id == model["id"].split(":")[0]
                     or custom_model.id == model["id"].split(":")[0]
                 ):
                 ):
-                    model["name"] = custom_model.name
-                    model["info"] = custom_model.model_dump()
+                    if custom_model.is_active:
+                        model["name"] = custom_model.name
+                        model["info"] = custom_model.model_dump()
+
+                        action_ids = []
+                        if "info" in model and "meta" in model["info"]:
+                            action_ids.extend(
+                                model["info"]["meta"].get("actionIds", [])
+                            )
 
 
-                    action_ids = []
-                    if "info" in model and "meta" in model["info"]:
-                        action_ids.extend(model["info"]["meta"].get("actionIds", []))
+                        model["action_ids"] = action_ids
+                    else:
+                        models.remove(model)
 
 
-                    model["action_ids"] = action_ids
-        else:
+        elif custom_model.is_active and (
+            custom_model.id not in [model["id"] for model in models]
+        ):
             owned_by = "openai"
             owned_by = "openai"
             pipe = None
             pipe = None
             action_ids = []
             action_ids = []
@@ -995,7 +1026,7 @@ async def get_all_models():
 
 
             models.append(
             models.append(
                 {
                 {
-                    "id": custom_model.id,
+                    "id": f"{custom_model.id}",
                     "name": custom_model.name,
                     "name": custom_model.name,
                     "object": "model",
                     "object": "model",
                     "created": custom_model.created_at,
                     "created": custom_model.created_at,
@@ -1007,66 +1038,54 @@ async def get_all_models():
                 }
                 }
             )
             )
 
 
-    for model in models:
-        action_ids = []
-        if "action_ids" in model:
-            action_ids = model["action_ids"]
-            del model["action_ids"]
+    # Process action_ids to get the actions
+    def get_action_items_from_module(module):
+        actions = []
+        if hasattr(module, "actions"):
+            actions = module.actions
+            return [
+                {
+                    "id": f"{module.id}.{action['id']}",
+                    "name": action.get("name", f"{module.name} ({action['id']})"),
+                    "description": module.meta.description,
+                    "icon_url": action.get(
+                        "icon_url", module.meta.manifest.get("icon_url", None)
+                    ),
+                }
+                for action in actions
+            ]
+        else:
+            return [
+                {
+                    "id": module.id,
+                    "name": module.name,
+                    "description": module.meta.description,
+                    "icon_url": module.meta.manifest.get("icon_url", None),
+                }
+            ]
+
+    def get_function_module_by_id(function_id):
+        if function_id in webui_app.state.FUNCTIONS:
+            function_module = webui_app.state.FUNCTIONS[function_id]
+        else:
+            function_module, _, _ = load_function_module_by_id(function_id)
+            webui_app.state.FUNCTIONS[function_id] = function_module
 
 
-        action_ids = action_ids + global_action_ids
-        action_ids = list(set(action_ids))
+    for model in models:
         action_ids = [
         action_ids = [
-            action_id for action_id in action_ids if action_id in enabled_action_ids
+            action_id
+            for action_id in list(set(model.pop("action_ids", []) + global_action_ids))
+            if action_id in enabled_action_ids
         ]
         ]
 
 
         model["actions"] = []
         model["actions"] = []
         for action_id in action_ids:
         for action_id in action_ids:
-            action = Functions.get_function_by_id(action_id)
-            if action is None:
+            action_function = Functions.get_function_by_id(action_id)
+            if action_function is None:
                 raise Exception(f"Action not found: {action_id}")
                 raise Exception(f"Action not found: {action_id}")
 
 
-            if action_id in webui_app.state.FUNCTIONS:
-                function_module = webui_app.state.FUNCTIONS[action_id]
-            else:
-                function_module, _, _ = load_function_module_by_id(action_id)
-                webui_app.state.FUNCTIONS[action_id] = function_module
-
-            __webui__ = False
-            if hasattr(function_module, "__webui__"):
-                __webui__ = function_module.__webui__
-
-            if hasattr(function_module, "actions"):
-                actions = function_module.actions
-                model["actions"].extend(
-                    [
-                        {
-                            "id": f"{action_id}.{_action['id']}",
-                            "name": _action.get(
-                                "name", f"{action.name} ({_action['id']})"
-                            ),
-                            "description": action.meta.description,
-                            "icon_url": _action.get(
-                                "icon_url", action.meta.manifest.get("icon_url", None)
-                            ),
-                            **({"__webui__": __webui__} if __webui__ else {}),
-                        }
-                        for _action in actions
-                    ]
-                )
-            else:
-                model["actions"].append(
-                    {
-                        "id": action_id,
-                        "name": action.name,
-                        "description": action.meta.description,
-                        "icon_url": action.meta.manifest.get("icon_url", None),
-                        **({"__webui__": __webui__} if __webui__ else {}),
-                    }
-                )
-
-    app.state.MODELS = {model["id"]: model for model in models}
-    webui_app.state.MODELS = app.state.MODELS
-
+            function_module = get_function_module_by_id(action_id)
+            model["actions"].extend(get_action_items_from_module(function_module))
     return models
     return models
 
 
 
 
@@ -1081,16 +1100,29 @@ async def get_models(user=Depends(get_verified_user)):
         if "pipeline" not in model or model["pipeline"].get("type", None) != "filter"
         if "pipeline" not in model or model["pipeline"].get("type", None) != "filter"
     ]
     ]
 
 
-    if app.state.config.ENABLE_MODEL_FILTER:
-        if user.role == "user":
-            models = list(
-                filter(
-                    lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
-                    models,
-                )
-            )
-            return {"data": models}
+    # Filter out models that the user does not have access to
+    if user.role == "user":
+        filtered_models = []
+        for model in models:
+            model_info = Models.get_model_by_id(model["id"])
+            if model_info:
+                if has_access(
+                    user.id, type="read", access_control=model_info.access_control
+                ):
+                    filtered_models.append(model)
+            else:
+                filtered_models.append(model)
+        models = filtered_models
+
+    return {"data": models}
+
 
 
+@app.get("/api/models/base")
+async def get_base_models(user=Depends(get_admin_user)):
+    models = await get_all_base_models()
+
+    # Filter out arena models
+    models = [model for model in models if not model.get("arena", False)]
     return {"data": models}
     return {"data": models}
 
 
 
 
@@ -1098,23 +1130,28 @@ async def get_models(user=Depends(get_verified_user)):
 async def generate_chat_completions(
 async def generate_chat_completions(
     form_data: dict, user=Depends(get_verified_user), bypass_filter: bool = False
     form_data: dict, user=Depends(get_verified_user), bypass_filter: bool = False
 ):
 ):
-    model_id = form_data["model"]
+    model_list = await get_all_models()
+    models = {model["id"]: model for model in model_list}
 
 
-    if model_id not in app.state.MODELS:
+    model_id = form_data["model"]
+    if model_id not in models:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             status_code=status.HTTP_404_NOT_FOUND,
             detail="Model not found",
             detail="Model not found",
         )
         )
 
 
-    if not bypass_filter and app.state.config.ENABLE_MODEL_FILTER:
-        if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
+    model = models[model_id]
+    # Check if user has access to the model
+    if user.role == "user":
+        model_info = Models.get_model_by_id(model_id)
+        if not has_access(
+            user.id, type="read", access_control=model_info.access_control
+        ):
             raise HTTPException(
             raise HTTPException(
-                status_code=status.HTTP_403_FORBIDDEN,
+                status_code=403,
                 detail="Model not found",
                 detail="Model not found",
             )
             )
 
 
-    model = app.state.MODELS[model_id]
-
     if model["owned_by"] == "arena":
     if model["owned_by"] == "arena":
         model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
         model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
         filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")
         filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")
@@ -1161,14 +1198,18 @@ async def generate_chat_completions(
                 ),
                 ),
                 "selected_model_id": selected_model_id,
                 "selected_model_id": selected_model_id,
             }
             }
+
     if model.get("pipe"):
     if model.get("pipe"):
-        return await generate_function_chat_completion(form_data, user=user)
+        # Below does not require bypass_filter because this is the only route the uses this function and it is already bypassing the filter
+        return await generate_function_chat_completion(
+            form_data, user=user, models=models
+        )
     if model["owned_by"] == "ollama":
     if model["owned_by"] == "ollama":
         # Using /ollama/api/chat endpoint
         # Using /ollama/api/chat endpoint
         form_data = convert_payload_openai_to_ollama(form_data)
         form_data = convert_payload_openai_to_ollama(form_data)
         form_data = GenerateChatCompletionForm(**form_data)
         form_data = GenerateChatCompletionForm(**form_data)
         response = await generate_ollama_chat_completion(
         response = await generate_ollama_chat_completion(
-            form_data=form_data, user=user, bypass_filter=True
+            form_data=form_data, user=user, bypass_filter=bypass_filter
         )
         )
         if form_data.stream:
         if form_data.stream:
             response.headers["content-type"] = "text/event-stream"
             response.headers["content-type"] = "text/event-stream"
@@ -1179,21 +1220,27 @@ async def generate_chat_completions(
         else:
         else:
             return convert_response_ollama_to_openai(response)
             return convert_response_ollama_to_openai(response)
     else:
     else:
-        return await generate_openai_chat_completion(form_data, user=user)
+        return await generate_openai_chat_completion(
+            form_data, user=user, bypass_filter=bypass_filter
+        )
 
 
 
 
 @app.post("/api/chat/completed")
 @app.post("/api/chat/completed")
 async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
 async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
+
+    model_list = await get_all_models()
+    models = {model["id"]: model for model in model_list}
+
     data = form_data
     data = form_data
     model_id = data["model"]
     model_id = data["model"]
-    if model_id not in app.state.MODELS:
+    if model_id not in models:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             status_code=status.HTTP_404_NOT_FOUND,
             detail="Model not found",
             detail="Model not found",
         )
         )
-    model = app.state.MODELS[model_id]
 
 
-    sorted_filters = get_sorted_filters(model_id)
+    model = models[model_id]
+    sorted_filters = get_sorted_filters(model_id, models)
     if "pipeline" in model:
     if "pipeline" in model:
         sorted_filters = [model] + sorted_filters
         sorted_filters = [model] + sorted_filters
 
 
@@ -1368,14 +1415,18 @@ async def chat_action(action_id: str, form_data: dict, user=Depends(get_verified
             detail="Action not found",
             detail="Action not found",
         )
         )
 
 
+    model_list = await get_all_models()
+    models = {model["id"]: model for model in model_list}
+
     data = form_data
     data = form_data
     model_id = data["model"]
     model_id = data["model"]
-    if model_id not in app.state.MODELS:
+
+    if model_id not in models:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             status_code=status.HTTP_404_NOT_FOUND,
             detail="Model not found",
             detail="Model not found",
         )
         )
-    model = app.state.MODELS[model_id]
+    model = models[model_id]
 
 
     __event_emitter__ = get_event_emitter(
     __event_emitter__ = get_event_emitter(
         {
         {
@@ -1529,8 +1580,11 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
 async def generate_title(form_data: dict, user=Depends(get_verified_user)):
 async def generate_title(form_data: dict, user=Depends(get_verified_user)):
     print("generate_title")
     print("generate_title")
 
 
+    model_list = await get_all_models()
+    models = {model["id"]: model for model in model_list}
+
     model_id = form_data["model"]
     model_id = form_data["model"]
-    if model_id not in app.state.MODELS:
+    if model_id not in models:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             status_code=status.HTTP_404_NOT_FOUND,
             detail="Model not found",
             detail="Model not found",
@@ -1538,10 +1592,16 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
 
 
     # Check if the user has a custom task model
     # Check if the user has a custom task model
     # If the user has a custom task model, use that model
     # If the user has a custom task model, use that model
-    task_model_id = get_task_model_id(model_id)
+    task_model_id = get_task_model_id(
+        model_id,
+        app.state.config.TASK_MODEL,
+        app.state.config.TASK_MODEL_EXTERNAL,
+        models,
+    )
+
     print(task_model_id)
     print(task_model_id)
 
 
-    model = app.state.MODELS[task_model_id]
+    model = models[task_model_id]
 
 
     if app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "":
     if app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "":
         template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
         template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
@@ -1575,7 +1635,7 @@ Artificial Intelligence in Healthcare
         "stream": False,
         "stream": False,
         **(
         **(
             {"max_tokens": 50}
             {"max_tokens": 50}
-            if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
+            if models[task_model_id]["owned_by"] == "ollama"
             else {
             else {
                 "max_completion_tokens": 50,
                 "max_completion_tokens": 50,
             }
             }
@@ -1587,7 +1647,7 @@ Artificial Intelligence in Healthcare
 
 
     # Handle pipeline filters
     # Handle pipeline filters
     try:
     try:
-        payload = filter_pipeline(payload, user)
+        payload = filter_pipeline(payload, user, models)
     except Exception as e:
     except Exception as e:
         if len(e.args) > 1:
         if len(e.args) > 1:
             return JSONResponse(
             return JSONResponse(
@@ -1614,8 +1674,11 @@ async def generate_chat_tags(form_data: dict, user=Depends(get_verified_user)):
             content={"detail": "Tags generation is disabled"},
             content={"detail": "Tags generation is disabled"},
         )
         )
 
 
+    model_list = await get_all_models()
+    models = {model["id"]: model for model in model_list}
+
     model_id = form_data["model"]
     model_id = form_data["model"]
-    if model_id not in app.state.MODELS:
+    if model_id not in models:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             status_code=status.HTTP_404_NOT_FOUND,
             detail="Model not found",
             detail="Model not found",
@@ -1623,7 +1686,12 @@ async def generate_chat_tags(form_data: dict, user=Depends(get_verified_user)):
 
 
     # Check if the user has a custom task model
     # Check if the user has a custom task model
     # If the user has a custom task model, use that model
     # If the user has a custom task model, use that model
-    task_model_id = get_task_model_id(model_id)
+    task_model_id = get_task_model_id(
+        model_id,
+        app.state.config.TASK_MODEL,
+        app.state.config.TASK_MODEL_EXTERNAL,
+        models,
+    )
     print(task_model_id)
     print(task_model_id)
 
 
     if app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "":
     if app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "":
@@ -1661,7 +1729,7 @@ JSON format: { "tags": ["tag1", "tag2", "tag3"] }
 
 
     # Handle pipeline filters
     # Handle pipeline filters
     try:
     try:
-        payload = filter_pipeline(payload, user)
+        payload = filter_pipeline(payload, user, models)
     except Exception as e:
     except Exception as e:
         if len(e.args) > 1:
         if len(e.args) > 1:
             return JSONResponse(
             return JSONResponse(
@@ -1688,8 +1756,11 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
             detail=f"Search query generation is disabled",
             detail=f"Search query generation is disabled",
         )
         )
 
 
+    model_list = await get_all_models()
+    models = {model["id"]: model for model in model_list}
+
     model_id = form_data["model"]
     model_id = form_data["model"]
-    if model_id not in app.state.MODELS:
+    if model_id not in models:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             status_code=status.HTTP_404_NOT_FOUND,
             detail="Model not found",
             detail="Model not found",
@@ -1697,10 +1768,15 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
 
 
     # Check if the user has a custom task model
     # Check if the user has a custom task model
     # If the user has a custom task model, use that model
     # If the user has a custom task model, use that model
-    task_model_id = get_task_model_id(model_id)
+    task_model_id = get_task_model_id(
+        model_id,
+        app.state.config.TASK_MODEL,
+        app.state.config.TASK_MODEL_EXTERNAL,
+        models,
+    )
     print(task_model_id)
     print(task_model_id)
 
 
-    model = app.state.MODELS[task_model_id]
+    model = models[task_model_id]
 
 
     if app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE != "":
     if app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE != "":
         template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
         template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
@@ -1727,7 +1803,7 @@ Search Query:"""
         "stream": False,
         "stream": False,
         **(
         **(
             {"max_tokens": 30}
             {"max_tokens": 30}
-            if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
+            if models[task_model_id]["owned_by"] == "ollama"
             else {
             else {
                 "max_completion_tokens": 30,
                 "max_completion_tokens": 30,
             }
             }
@@ -1738,7 +1814,7 @@ Search Query:"""
 
 
     # Handle pipeline filters
     # Handle pipeline filters
     try:
     try:
-        payload = filter_pipeline(payload, user)
+        payload = filter_pipeline(payload, user, models)
     except Exception as e:
     except Exception as e:
         if len(e.args) > 1:
         if len(e.args) > 1:
             return JSONResponse(
             return JSONResponse(
@@ -1760,8 +1836,11 @@ Search Query:"""
 async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
 async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
     print("generate_emoji")
     print("generate_emoji")
 
 
+    model_list = await get_all_models()
+    models = {model["id"]: model for model in model_list}
+
     model_id = form_data["model"]
     model_id = form_data["model"]
-    if model_id not in app.state.MODELS:
+    if model_id not in models:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             status_code=status.HTTP_404_NOT_FOUND,
             detail="Model not found",
             detail="Model not found",
@@ -1769,10 +1848,15 @@ async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
 
 
     # Check if the user has a custom task model
     # Check if the user has a custom task model
     # If the user has a custom task model, use that model
     # If the user has a custom task model, use that model
-    task_model_id = get_task_model_id(model_id)
+    task_model_id = get_task_model_id(
+        model_id,
+        app.state.config.TASK_MODEL,
+        app.state.config.TASK_MODEL_EXTERNAL,
+        models,
+    )
     print(task_model_id)
     print(task_model_id)
 
 
-    model = app.state.MODELS[task_model_id]
+    model = models[task_model_id]
 
 
     template = '''
     template = '''
 Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
 Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
@@ -1794,7 +1878,7 @@ Message: """{{prompt}}"""
         "stream": False,
         "stream": False,
         **(
         **(
             {"max_tokens": 4}
             {"max_tokens": 4}
-            if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
+            if models[task_model_id]["owned_by"] == "ollama"
             else {
             else {
                 "max_completion_tokens": 4,
                 "max_completion_tokens": 4,
             }
             }
@@ -1806,7 +1890,7 @@ Message: """{{prompt}}"""
 
 
     # Handle pipeline filters
     # Handle pipeline filters
     try:
     try:
-        payload = filter_pipeline(payload, user)
+        payload = filter_pipeline(payload, user, models)
     except Exception as e:
     except Exception as e:
         if len(e.args) > 1:
         if len(e.args) > 1:
             return JSONResponse(
             return JSONResponse(
@@ -1828,8 +1912,11 @@ Message: """{{prompt}}"""
 async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)):
 async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)):
     print("generate_moa_response")
     print("generate_moa_response")
 
 
+    model_list = await get_all_models()
+    models = {model["id"]: model for model in model_list}
+
     model_id = form_data["model"]
     model_id = form_data["model"]
-    if model_id not in app.state.MODELS:
+    if model_id not in models:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             status_code=status.HTTP_404_NOT_FOUND,
             detail="Model not found",
             detail="Model not found",
@@ -1837,10 +1924,15 @@ async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)
 
 
     # Check if the user has a custom task model
     # Check if the user has a custom task model
     # If the user has a custom task model, use that model
     # If the user has a custom task model, use that model
-    task_model_id = get_task_model_id(model_id)
+    task_model_id = get_task_model_id(
+        model_id,
+        app.state.config.TASK_MODEL,
+        app.state.config.TASK_MODEL_EXTERNAL,
+        models,
+    )
     print(task_model_id)
     print(task_model_id)
 
 
-    model = app.state.MODELS[task_model_id]
+    model = models[task_model_id]
 
 
     template = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
     template = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
 
 
@@ -1867,7 +1959,7 @@ Responses from models: {{responses}}"""
     log.debug(payload)
     log.debug(payload)
 
 
     try:
     try:
-        payload = filter_pipeline(payload, user)
+        payload = filter_pipeline(payload, user, models)
     except Exception as e:
     except Exception as e:
         if len(e.args) > 1:
         if len(e.args) > 1:
             return JSONResponse(
             return JSONResponse(
@@ -1897,7 +1989,7 @@ Responses from models: {{responses}}"""
 
 
 @app.get("/api/pipelines/list")
 @app.get("/api/pipelines/list")
 async def get_pipelines_list(user=Depends(get_admin_user)):
 async def get_pipelines_list(user=Depends(get_admin_user)):
-    responses = await get_openai_models(raw=True)
+    responses = await get_openai_models_responses()
 
 
     print(responses)
     print(responses)
     urlIdxs = [
     urlIdxs = [
@@ -2297,32 +2389,6 @@ async def get_app_config(request: Request):
     }
     }
 
 
 
 
-@app.get("/api/config/model/filter")
-async def get_model_filter_config(user=Depends(get_admin_user)):
-    return {
-        "enabled": app.state.config.ENABLE_MODEL_FILTER,
-        "models": app.state.config.MODEL_FILTER_LIST,
-    }
-
-
-class ModelFilterConfigForm(BaseModel):
-    enabled: bool
-    models: list[str]
-
-
-@app.post("/api/config/model/filter")
-async def update_model_filter_config(
-    form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
-):
-    app.state.config.ENABLE_MODEL_FILTER = form_data.enabled
-    app.state.config.MODEL_FILTER_LIST = form_data.models
-
-    return {
-        "enabled": app.state.config.ENABLE_MODEL_FILTER,
-        "models": app.state.config.MODEL_FILTER_LIST,
-    }
-
-
 # TODO: webhook endpoint should be under config endpoints
 # TODO: webhook endpoint should be under config endpoints
 
 
 
 

+ 85 - 0
backend/open_webui/migrations/versions/922e7a387820_add_group_table.py

@@ -0,0 +1,85 @@
+"""Add group table
+
+Revision ID: 922e7a387820
+Revises: 4ace53fd72c8
+Create Date: 2024-11-14 03:00:00.000000
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+revision = "922e7a387820"
+down_revision = "4ace53fd72c8"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.create_table(
+        "group",
+        sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True),
+        sa.Column("user_id", sa.Text(), nullable=True),
+        sa.Column("name", sa.Text(), nullable=True),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column("data", sa.JSON(), nullable=True),
+        sa.Column("meta", sa.JSON(), nullable=True),
+        sa.Column("permissions", sa.JSON(), nullable=True),
+        sa.Column("user_ids", sa.JSON(), nullable=True),
+        sa.Column("created_at", sa.BigInteger(), nullable=True),
+        sa.Column("updated_at", sa.BigInteger(), nullable=True),
+    )
+
+    # Add 'access_control' column to 'model' table
+    op.add_column(
+        "model",
+        sa.Column("access_control", sa.JSON(), nullable=True),
+    )
+
+    # Add 'is_active' column to 'model' table
+    op.add_column(
+        "model",
+        sa.Column(
+            "is_active",
+            sa.Boolean(),
+            nullable=False,
+            server_default=sa.sql.expression.true(),
+        ),
+    )
+
+    # Add 'access_control' column to 'knowledge' table
+    op.add_column(
+        "knowledge",
+        sa.Column("access_control", sa.JSON(), nullable=True),
+    )
+
+    # Add 'access_control' column to 'prompt' table
+    op.add_column(
+        "prompt",
+        sa.Column("access_control", sa.JSON(), nullable=True),
+    )
+
+    # Add 'access_control' column to 'tools' table
+    op.add_column(
+        "tool",
+        sa.Column("access_control", sa.JSON(), nullable=True),
+    )
+
+
+def downgrade():
+    op.drop_table("group")
+
+    # Drop 'access_control' column from 'model' table
+    op.drop_column("model", "access_control")
+
+    # Drop 'is_active' column from 'model' table
+    op.drop_column("model", "is_active")
+
+    # Drop 'access_control' column from 'knowledge' table
+    op.drop_column("knowledge", "access_control")
+
+    # Drop 'access_control' column from 'prompt' table
+    op.drop_column("prompt", "access_control")
+
+    # Drop 'access_control' column from 'tools' table
+    op.drop_column("tool", "access_control")

+ 95 - 0
backend/open_webui/utils/access_control.py

@@ -0,0 +1,95 @@
+from typing import Optional, Union, List, Dict, Any
+from open_webui.apps.webui.models.groups import Groups
+import json
+
+
+def get_permissions(
+    user_id: str,
+    default_permissions: Dict[str, Any],
+) -> Dict[str, Any]:
+    """
+    Get all permissions for a user by combining the permissions of all groups the user is a member of.
+    If a permission is defined in multiple groups, the most permissive value is used (True > False).
+    Permissions are nested in a dict with the permission key as the key and a boolean as the value.
+    """
+
+    def combine_permissions(
+        permissions: Dict[str, Any], group_permissions: Dict[str, Any]
+    ) -> Dict[str, Any]:
+        """Combine permissions from multiple groups by taking the most permissive value."""
+        for key, value in group_permissions.items():
+            if isinstance(value, dict):
+                if key not in permissions:
+                    permissions[key] = {}
+                permissions[key] = combine_permissions(permissions[key], value)
+            else:
+                if key not in permissions:
+                    permissions[key] = value
+                else:
+                    permissions[key] = permissions[key] or value
+        return permissions
+
+    user_groups = Groups.get_groups_by_member_id(user_id)
+
+    # deep copy default permissions to avoid modifying the original dict
+    permissions = json.loads(json.dumps(default_permissions))
+
+    for group in user_groups:
+        group_permissions = group.permissions
+        permissions = combine_permissions(permissions, group_permissions)
+
+    return permissions
+
+
+def has_permission(
+    user_id: str,
+    permission_key: str,
+    default_permissions: Dict[str, bool] = {},
+) -> bool:
+    """
+    Check if a user has a specific permission by checking the group permissions
+    and falls back to default permissions if not found in any group.
+
+    Permission keys can be hierarchical and separated by dots ('.').
+    """
+
+    def get_permission(permissions: Dict[str, bool], keys: List[str]) -> bool:
+        """Traverse permissions dict using a list of keys (from dot-split permission_key)."""
+        for key in keys:
+            if key not in permissions:
+                return False  # If any part of the hierarchy is missing, deny access
+            permissions = permissions[key]  # Go one level deeper
+
+        return bool(permissions)  # Return the boolean at the final level
+
+    permission_hierarchy = permission_key.split(".")
+
+    # Retrieve user group permissions
+    user_groups = Groups.get_groups_by_member_id(user_id)
+
+    for group in user_groups:
+        group_permissions = group.permissions
+        if get_permission(group_permissions, permission_hierarchy):
+            return True
+
+    # Check default permissions afterwards if the group permissions don't allow it
+    return get_permission(default_permissions, permission_hierarchy)
+
+
+def has_access(
+    user_id: str,
+    type: str = "write",
+    access_control: Optional[dict] = None,
+) -> bool:
+    if access_control is None:
+        return type == "read"
+
+    user_groups = Groups.get_groups_by_member_id(user_id)
+    user_group_ids = [group.id for group in user_groups]
+    permission_access = access_control.get(type, {})
+    permitted_group_ids = permission_access.get("group_ids", [])
+    permitted_user_ids = permission_access.get("user_ids", [])
+
+    return user_id in permitted_user_ids or any(
+        group_id in permitted_group_ids for group_id in user_group_ids
+    )

+ 22 - 12
backend/open_webui/utils/tools.py

@@ -4,7 +4,7 @@ from typing import Awaitable, Callable, get_type_hints
 
 
 from open_webui.apps.webui.models.tools import Tools
 from open_webui.apps.webui.models.tools import Tools
 from open_webui.apps.webui.models.users import UserModel
 from open_webui.apps.webui.models.users import UserModel
-from open_webui.apps.webui.utils import load_toolkit_module_by_id
+from open_webui.apps.webui.utils import load_tools_module_by_id
 from open_webui.utils.schemas import json_schema_to_model
 from open_webui.utils.schemas import json_schema_to_model
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
@@ -32,15 +32,16 @@ def apply_extra_params_to_tool_function(
 def get_tools(
 def get_tools(
     webui_app, tool_ids: list[str], user: UserModel, extra_params: dict
     webui_app, tool_ids: list[str], user: UserModel, extra_params: dict
 ) -> dict[str, dict]:
 ) -> dict[str, dict]:
-    tools = {}
+    tools_dict = {}
+
     for tool_id in tool_ids:
     for tool_id in tool_ids:
-        toolkit = Tools.get_tool_by_id(tool_id)
-        if toolkit is None:
+        tools = Tools.get_tool_by_id(tool_id)
+        if tools is None:
             continue
             continue
 
 
         module = webui_app.state.TOOLS.get(tool_id, None)
         module = webui_app.state.TOOLS.get(tool_id, None)
         if module is None:
         if module is None:
-            module, _ = load_toolkit_module_by_id(tool_id)
+            module, _ = load_tools_module_by_id(tool_id)
             webui_app.state.TOOLS[tool_id] = module
             webui_app.state.TOOLS[tool_id] = module
 
 
         extra_params["__id__"] = tool_id
         extra_params["__id__"] = tool_id
@@ -53,11 +54,19 @@ def get_tools(
                 **Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
                 **Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
             )
             )
 
 
-        for spec in toolkit.specs:
+        for spec in tools.specs:
             # TODO: Fix hack for OpenAI API
             # TODO: Fix hack for OpenAI API
             for val in spec.get("parameters", {}).get("properties", {}).values():
             for val in spec.get("parameters", {}).get("properties", {}).values():
                 if val["type"] == "str":
                 if val["type"] == "str":
                     val["type"] = "string"
                     val["type"] = "string"
+
+            # Remove internal parameters
+            spec["parameters"]["properties"] = {
+                key: val
+                for key, val in spec["parameters"]["properties"].items()
+                if not key.startswith("__")
+            }
+
             function_name = spec["name"]
             function_name = spec["name"]
 
 
             # convert to function that takes only model params and inserts custom params
             # convert to function that takes only model params and inserts custom params
@@ -77,13 +86,14 @@ def get_tools(
             }
             }
 
 
             # TODO: if collision, prepend toolkit name
             # TODO: if collision, prepend toolkit name
-            if function_name in tools:
-                log.warning(f"Tool {function_name} already exists in another toolkit!")
-                log.warning(f"Collision between {toolkit} and {tool_id}.")
-                log.warning(f"Discarding {toolkit}.{function_name}")
+            if function_name in tools_dict:
+                log.warning(f"Tool {function_name} already exists in another tools!")
+                log.warning(f"Collision between {tools} and {tool_id}.")
+                log.warning(f"Discarding {tools}.{function_name}")
             else:
             else:
-                tools[function_name] = tool_dict
-    return tools
+                tools_dict[function_name] = tool_dict
+
+    return tools_dict
 
 
 
 
 def doc_to_dict(docstring):
 def doc_to_dict(docstring):

+ 7 - 2
backend/open_webui/utils/utils.py

@@ -1,12 +1,17 @@
 import logging
 import logging
 import uuid
 import uuid
+import jwt
+
 from datetime import UTC, datetime, timedelta
 from datetime import UTC, datetime, timedelta
-from typing import Optional, Union
+from typing import Optional, Union, List, Dict
+
 
 
-import jwt
 from open_webui.apps.webui.models.users import Users
 from open_webui.apps.webui.models.users import Users
+
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.env import WEBUI_SECRET_KEY
 from open_webui.env import WEBUI_SECRET_KEY
+
+
 from fastapi import Depends, HTTPException, Request, Response, status
 from fastapi import Depends, HTTPException, Request, Response, status
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from passlib.context import CryptContext
 from passlib.context import CryptContext

+ 1 - 0
backend/requirements.txt

@@ -13,6 +13,7 @@ passlib[bcrypt]==1.7.4
 requests==2.32.3
 requests==2.32.3
 aiohttp==3.10.8
 aiohttp==3.10.8
 async-timeout
 async-timeout
+aiocache
 
 
 sqlalchemy==2.0.32
 sqlalchemy==2.0.32
 alembic==1.13.2
 alembic==1.13.2

+ 1 - 0
pyproject.toml

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

+ 163 - 0
src/lib/apis/groups/index.ts

@@ -0,0 +1,163 @@
+import { WEBUI_API_BASE_URL } from '$lib/constants';
+
+export const createNewGroup = async (token: string, group: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/groups/create`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			...group
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getGroups = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/groups/`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+
+export const getGroupById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const updateGroupById = async (token: string, id: string, group: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/update`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			...group
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const deleteGroupById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/delete`, {
+		method: 'DELETE',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};

+ 6 - 20
src/lib/apis/index.ts

@@ -1,9 +1,10 @@
 import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
 import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
 
 
-export const getModels = async (token: string = '') => {
+export const getModels = async (token: string = '', base: boolean = false) => {
 	let error = null;
 	let error = null;
-
-	const res = await fetch(`${WEBUI_BASE_URL}/api/models`, {
+	const res = await fetch(`${WEBUI_BASE_URL}/api/models${
+		base ? '/base' : ''
+	}`, {
 		method: 'GET',
 		method: 'GET',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
@@ -16,36 +17,21 @@ export const getModels = async (token: string = '') => {
 			return res.json();
 			return res.json();
 		})
 		})
 		.catch((err) => {
 		.catch((err) => {
-			console.log(err);
 			error = err;
 			error = err;
+			console.log(err);
 			return null;
 			return null;
 		});
 		});
+	
 
 
 	if (error) {
 	if (error) {
 		throw error;
 		throw error;
 	}
 	}
 
 
 	let models = res?.data ?? [];
 	let models = res?.data ?? [];
-
 	models = models
 	models = models
 		.filter((models) => models)
 		.filter((models) => models)
 		// Sort the models
 		// Sort the models
 		.sort((a, b) => {
 		.sort((a, b) => {
-			// Check if models have position property
-			const aHasPosition = a.info?.meta?.position !== undefined;
-			const bHasPosition = b.info?.meta?.position !== undefined;
-
-			// If both a and b have the position property
-			if (aHasPosition && bHasPosition) {
-				return a.info.meta.position - b.info.meta.position;
-			}
-
-			// If only a has the position property, it should come first
-			if (aHasPosition) return -1;
-
-			// If only b has the position property, it should come first
-			if (bHasPosition) return 1;
-
 			// Compare case-insensitively by name for models without position property
 			// Compare case-insensitively by name for models without position property
 			const lowerA = a.name.toLowerCase();
 			const lowerA = a.name.toLowerCase();
 			const lowerB = b.name.toLowerCase();
 			const lowerB = b.name.toLowerCase();

+ 38 - 4
src/lib/apis/knowledge/index.ts

@@ -1,6 +1,6 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 
 
-export const createNewKnowledge = async (token: string, name: string, description: string) => {
+export const createNewKnowledge = async (token: string, name: string, description: string, accessControl: null|object) => {
 	let error = null;
 	let error = null;
 
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/create`, {
 	const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/create`, {
@@ -12,7 +12,8 @@ export const createNewKnowledge = async (token: string, name: string, descriptio
 		},
 		},
 		body: JSON.stringify({
 		body: JSON.stringify({
 			name: name,
 			name: name,
-			description: description
+			description: description,
+			access_control: accessControl
 		})
 		})
 	})
 	})
 		.then(async (res) => {
 		.then(async (res) => {
@@ -32,7 +33,7 @@ export const createNewKnowledge = async (token: string, name: string, descriptio
 	return res;
 	return res;
 };
 };
 
 
-export const getKnowledgeItems = async (token: string = '') => {
+export const getKnowledgeBases = async (token: string = '') => {
 	let error = null;
 	let error = null;
 
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/`, {
 	const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/`, {
@@ -63,6 +64,37 @@ export const getKnowledgeItems = async (token: string = '') => {
 	return res;
 	return res;
 };
 };
 
 
+export const getKnowledgeBaseList = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/list`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const getKnowledgeById = async (token: string, id: string) => {
 export const getKnowledgeById = async (token: string, id: string) => {
 	let error = null;
 	let error = null;
 
 
@@ -99,6 +131,7 @@ type KnowledgeUpdateForm = {
 	name?: string;
 	name?: string;
 	description?: string;
 	description?: string;
 	data?: object;
 	data?: object;
+	access_control?: null|object;
 };
 };
 
 
 export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
 export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
@@ -114,7 +147,8 @@ export const updateKnowledgeById = async (token: string, id: string, form: Knowl
 		body: JSON.stringify({
 		body: JSON.stringify({
 			name: form?.name ? form.name : undefined,
 			name: form?.name ? form.name : undefined,
 			description: form?.description ? form.description : undefined,
 			description: form?.description ? form.description : undefined,
-			data: form?.data ? form.data : undefined
+			data: form?.data ? form.data : undefined,
+			access_control: form.access_control
 		})
 		})
 	})
 	})
 		.then(async (res) => {
 		.then(async (res) => {

+ 84 - 9
src/lib/apis/models/index.ts

@@ -1,9 +1,76 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 
 
-export const addNewModel = async (token: string, model: object) => {
+
+export const getModels = async (token: string = '') => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models/add`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+
+
+export const getBaseModels = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/base`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+
+
+export const createNewModel = async (token: string, model: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/create`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
@@ -29,10 +96,15 @@ export const addNewModel = async (token: string, model: object) => {
 	return res;
 	return res;
 };
 };
 
 
-export const getModelInfos = async (token: string = '') => {
+
+
+export const getModelById = async (token: string, id: string) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models`, {
+	const searchParams = new URLSearchParams();
+	searchParams.append('id', id);
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}`, {
 		method: 'GET',
 		method: 'GET',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
@@ -49,6 +121,7 @@ export const getModelInfos = async (token: string = '') => {
 		})
 		})
 		.catch((err) => {
 		.catch((err) => {
 			error = err;
 			error = err;
+
 			console.log(err);
 			console.log(err);
 			return null;
 			return null;
 		});
 		});
@@ -60,14 +133,15 @@ export const getModelInfos = async (token: string = '') => {
 	return res;
 	return res;
 };
 };
 
 
-export const getModelById = async (token: string, id: string) => {
+
+export const toggleModelById = async (token: string, id: string) => {
 	let error = null;
 	let error = null;
 
 
 	const searchParams = new URLSearchParams();
 	const searchParams = new URLSearchParams();
 	searchParams.append('id', id);
 	searchParams.append('id', id);
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models?${searchParams.toString()}`, {
-		method: 'GET',
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}/toggle`, {
+		method: 'POST',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
 			'Content-Type': 'application/json',
@@ -95,13 +169,14 @@ export const getModelById = async (token: string, id: string) => {
 	return res;
 	return res;
 };
 };
 
 
+
 export const updateModelById = async (token: string, id: string, model: object) => {
 export const updateModelById = async (token: string, id: string, model: object) => {
 	let error = null;
 	let error = null;
 
 
 	const searchParams = new URLSearchParams();
 	const searchParams = new URLSearchParams();
 	searchParams.append('id', id);
 	searchParams.append('id', id);
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models/update?${searchParams.toString()}`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}/update`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
@@ -137,7 +212,7 @@ export const deleteModelById = async (token: string, id: string) => {
 	const searchParams = new URLSearchParams();
 	const searchParams = new URLSearchParams();
 	searchParams.append('id', id);
 	searchParams.append('id', id);
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models/delete?${searchParams.toString()}`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}/delete`, {
 		method: 'DELETE',
 		method: 'DELETE',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',

+ 4 - 2
src/lib/apis/ollama/index.ts

@@ -211,10 +211,12 @@ export const getOllamaVersion = async (token: string, urlIdx?: number) => {
 	return res?.version ?? false;
 	return res?.version ?? false;
 };
 };
 
 
-export const getOllamaModels = async (token: string = '') => {
+export const getOllamaModels = async (token: string = '', urlIdx: null|number = null) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, {
+	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags${
+		urlIdx !== null ? `/${urlIdx}` : ''
+	}`, {
 		method: 'GET',
 		method: 'GET',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',

+ 52 - 13
src/lib/apis/prompts/index.ts

@@ -1,10 +1,18 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 
 
+
+type PromptItem = {
+	command: string;
+	title: string;
+	content: string;
+	access_control: null|object;
+}
+
+
+
 export const createNewPrompt = async (
 export const createNewPrompt = async (
 	token: string,
 	token: string,
-	command: string,
-	title: string,
-	content: string
+	prompt: PromptItem
 ) => {
 ) => {
 	let error = null;
 	let error = null;
 
 
@@ -16,9 +24,8 @@ export const createNewPrompt = async (
 			authorization: `Bearer ${token}`
 			authorization: `Bearer ${token}`
 		},
 		},
 		body: JSON.stringify({
 		body: JSON.stringify({
-			command: `/${command}`,
-			title: title,
-			content: content
+			...prompt,
+			command: `/${prompt.command}`,
 		})
 		})
 	})
 	})
 		.then(async (res) => {
 		.then(async (res) => {
@@ -69,6 +76,39 @@ export const getPrompts = async (token: string = '') => {
 	return res;
 	return res;
 };
 };
 
 
+
+export const getPromptList = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/list`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+
 export const getPromptByCommand = async (token: string, command: string) => {
 export const getPromptByCommand = async (token: string, command: string) => {
 	let error = null;
 	let error = null;
 
 
@@ -101,15 +141,15 @@ export const getPromptByCommand = async (token: string, command: string) => {
 	return res;
 	return res;
 };
 };
 
 
+
+
 export const updatePromptByCommand = async (
 export const updatePromptByCommand = async (
 	token: string,
 	token: string,
-	command: string,
-	title: string,
-	content: string
+	prompt: PromptItem
 ) => {
 ) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/update`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${prompt.command}/update`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
@@ -117,9 +157,8 @@ export const updatePromptByCommand = async (
 			authorization: `Bearer ${token}`
 			authorization: `Bearer ${token}`
 		},
 		},
 		body: JSON.stringify({
 		body: JSON.stringify({
-			command: `/${command}`,
-			title: title,
-			content: content
+			...prompt,
+			command: `/${prompt.command}`,
 		})
 		})
 	})
 	})
 		.then(async (res) => {
 		.then(async (res) => {

+ 33 - 0
src/lib/apis/tools/index.ts

@@ -62,6 +62,39 @@ export const getTools = async (token: string = '') => {
 	return res;
 	return res;
 };
 };
 
 
+
+export const getToolList = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/tools/list`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+
 export const exportTools = async (token: string = '') => {
 export const exportTools = async (token: string = '') => {
 	let error = null;
 	let error = null;
 
 

+ 34 - 4
src/lib/apis/users/index.ts

@@ -1,10 +1,40 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 import { getUserPosition } from '$lib/utils';
 import { getUserPosition } from '$lib/utils';
 
 
-export const getUserPermissions = async (token: string) => {
+
+export const getUserGroups = async (token: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/users/groups`, {
+		method: 'GET',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+
+
+export const getUserDefaultPermissions = async (token: string) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions/user`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/users/default/permissions`, {
 		method: 'GET',
 		method: 'GET',
 		headers: {
 		headers: {
 			'Content-Type': 'application/json',
 			'Content-Type': 'application/json',
@@ -28,10 +58,10 @@ export const getUserPermissions = async (token: string) => {
 	return res;
 	return res;
 };
 };
 
 
-export const updateUserPermissions = async (token: string, permissions: object) => {
+export const updateUserDefaultPermissions = async (token: string, permissions: object) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions/user`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/users/default/permissions`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
 			'Content-Type': 'application/json',
 			'Content-Type': 'application/json',

+ 15 - 28
src/lib/components/workspace/Functions.svelte → src/lib/components/admin/Functions.svelte

@@ -5,7 +5,6 @@
 
 
 	import { WEBUI_NAME, config, functions, models } from '$lib/stores';
 	import { WEBUI_NAME, config, functions, models } from '$lib/stores';
 	import { onMount, getContext, tick } from 'svelte';
 	import { onMount, getContext, tick } from 'svelte';
-	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
 
 
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 	import {
 	import {
@@ -25,13 +24,14 @@
 	import FunctionMenu from './Functions/FunctionMenu.svelte';
 	import FunctionMenu from './Functions/FunctionMenu.svelte';
 	import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
 	import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
 	import Switch from '../common/Switch.svelte';
 	import Switch from '../common/Switch.svelte';
-	import ValvesModal from './common/ValvesModal.svelte';
-	import ManifestModal from './common/ManifestModal.svelte';
+	import ValvesModal from '../workspace/common/ValvesModal.svelte';
+	import ManifestModal from '../workspace/common/ManifestModal.svelte';
 	import Heart from '../icons/Heart.svelte';
 	import Heart from '../icons/Heart.svelte';
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import GarbageBin from '../icons/GarbageBin.svelte';
 	import GarbageBin from '../icons/GarbageBin.svelte';
 	import Search from '../icons/Search.svelte';
 	import Search from '../icons/Search.svelte';
 	import Plus from '../icons/Plus.svelte';
 	import Plus from '../icons/Plus.svelte';
+	import ChevronRight from '../icons/ChevronRight.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -98,7 +98,7 @@
 				id: `${_function.id}_clone`,
 				id: `${_function.id}_clone`,
 				name: `${_function.name} (Clone)`
 				name: `${_function.name} (Clone)`
 			});
 			});
-			goto('/workspace/functions/create');
+			goto('/admin/functions/create');
 		}
 		}
 	};
 	};
 
 
@@ -210,7 +210,7 @@
 		<div>
 		<div>
 			<a
 			<a
 				class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
 				class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
-				href="/workspace/functions/create"
+				href="/admin/functions/create"
 			>
 			>
 				<Plus className="size-3.5" />
 				<Plus className="size-3.5" />
 			</a>
 			</a>
@@ -225,7 +225,7 @@
 		>
 		>
 			<a
 			<a
 				class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
 				class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
-				href={`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`}
+				href={`/admin/functions/edit?id=${encodeURIComponent(func.id)}`}
 			>
 			>
 				<div class="flex items-center text-left">
 				<div class="flex items-center text-left">
 					<div class=" flex-1 self-center pl-1">
 					<div class=" flex-1 self-center pl-1">
@@ -322,7 +322,7 @@
 					<FunctionMenu
 					<FunctionMenu
 						{func}
 						{func}
 						editHandler={() => {
 						editHandler={() => {
-							goto(`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`);
+							goto(`/admin/functions/edit?id=${encodeURIComponent(func.id)}`);
 						}}
 						}}
 						shareHandler={() => {
 						shareHandler={() => {
 							shareHandler(func);
 							shareHandler(func);
@@ -452,40 +452,27 @@
 
 
 {#if $config?.features.enable_community_sharing}
 {#if $config?.features.enable_community_sharing}
 	<div class=" my-16">
 	<div class=" my-16">
-		<div class=" text-lg font-semibold mb-3 line-clamp-1">
+		<div class=" text-lg font-semibold mb-0.5 line-clamp-1">
 			{$i18n.t('Made by OpenWebUI Community')}
 			{$i18n.t('Made by OpenWebUI Community')}
 		</div>
 		</div>
 
 
 		<a
 		<a
-			class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
+			class=" flex cursor-pointer items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-850 w-full mb-2 px-3.5 py-1.5 rounded-xl transition"
 			href="https://openwebui.com/#open-webui-community"
 			href="https://openwebui.com/#open-webui-community"
 			target="_blank"
 			target="_blank"
 		>
 		>
-			<div class=" self-center w-10 flex-shrink-0">
-				<div
-					class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
-				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 24 24"
-						fill="currentColor"
-						class="w-6"
-					>
-						<path
-							fill-rule="evenodd"
-							d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-							clip-rule="evenodd"
-						/>
-					</svg>
-				</div>
-			</div>
-
 			<div class=" self-center">
 			<div class=" self-center">
 				<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
 				<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
 				<div class=" text-sm line-clamp-1">
 				<div class=" text-sm line-clamp-1">
 					{$i18n.t('Discover, download, and explore custom functions')}
 					{$i18n.t('Discover, download, and explore custom functions')}
 				</div>
 				</div>
 			</div>
 			</div>
+
+			<div>
+				<div>
+					<ChevronRight />
+				</div>
+			</div>
 		</a>
 		</a>
 	</div>
 	</div>
 {/if}
 {/if}

+ 1 - 1
src/lib/components/workspace/Functions/FunctionEditor.svelte → src/lib/components/admin/Functions/FunctionEditor.svelte

@@ -305,7 +305,7 @@ class Pipe:
 								<button
 								<button
 									class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
 									class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
 									on:click={() => {
 									on:click={() => {
-										goto('/workspace/functions');
+										goto('/admin/functions');
 									}}
 									}}
 									type="button"
 									type="button"
 								>
 								>

+ 0 - 0
src/lib/components/workspace/Functions/FunctionMenu.svelte → src/lib/components/admin/Functions/FunctionMenu.svelte


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

@@ -327,7 +327,7 @@
 		</button>
 		</button>
 	</div>
 	</div>
 
 
-	<div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll">
+	<div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll pr-1 scrollbar-hidden">
 		{#if selectedTab === 'general'}
 		{#if selectedTab === 'general'}
 			<General
 			<General
 				saveHandler={async () => {
 				saveHandler={async () => {

+ 1 - 0
src/lib/components/admin/Settings/Connections.svelte

@@ -302,6 +302,7 @@
 									<OllamaConnection
 									<OllamaConnection
 										bind:url
 										bind:url
 										bind:config={OLLAMA_API_CONFIGS[url]}
 										bind:config={OLLAMA_API_CONFIGS[url]}
+										{idx}
 										onSubmit={() => {
 										onSubmit={() => {
 											updateOllamaHandler();
 											updateOllamaHandler();
 										}}
 										}}

+ 1019 - 0
src/lib/components/admin/Settings/Connections/ManageOllamaModal.svelte

@@ -0,0 +1,1019 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import { getContext, onMount } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
+	import { splitStream } from '$lib/utils';
+
+	import {
+		createModel,
+		deleteModel,
+		downloadModel,
+		getOllamaUrls,
+		getOllamaVersion,
+		pullModel,
+		uploadModel,
+		getOllamaConfig,
+		getOllamaModels
+	} from '$lib/apis/ollama';
+	import { getModels } from '$lib/apis';
+
+	import Modal from '$lib/components/common/Modal.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import ModelDeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+	import Spinner from '$lib/components/common/Spinner.svelte';
+
+	export let show = false;
+
+	let modelUploadInputElement: HTMLInputElement;
+	let showModelDeleteConfirm = false;
+
+	let loading = true;
+
+	// Models
+	export let urlIdx: number | null = null;
+
+	let ollamaModels = [];
+
+	let updateModelId = null;
+	let updateProgress = null;
+	let showExperimentalOllama = false;
+
+	const MAX_PARALLEL_DOWNLOADS = 3;
+
+	let modelTransferring = false;
+	let modelTag = '';
+
+	let createModelLoading = false;
+	let createModelTag = '';
+	let createModelContent = '';
+	let createModelDigest = '';
+	let createModelPullProgress = null;
+
+	let digest = '';
+	let pullProgress = null;
+
+	let modelUploadMode = 'file';
+	let modelInputFile: File[] | null = null;
+	let modelFileUrl = '';
+	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
+	let modelFileDigest = '';
+
+	let uploadProgress = null;
+	let uploadMessage = '';
+
+	let deleteModelTag = '';
+
+	const updateModelsHandler = async () => {
+		for (const model of ollamaModels) {
+			console.log(model);
+
+			updateModelId = model.id;
+			const [res, controller] = await pullModel(localStorage.token, model.id, urlIdx).catch(
+				(error) => {
+					toast.error(error);
+					return null;
+				}
+			);
+
+			if (res) {
+				const reader = res.body
+					.pipeThrough(new TextDecoderStream())
+					.pipeThrough(splitStream('\n'))
+					.getReader();
+
+				while (true) {
+					try {
+						const { value, done } = await reader.read();
+						if (done) break;
+
+						let lines = value.split('\n');
+
+						for (const line of lines) {
+							if (line !== '') {
+								let data = JSON.parse(line);
+
+								console.log(data);
+								if (data.error) {
+									throw data.error;
+								}
+								if (data.detail) {
+									throw data.detail;
+								}
+								if (data.status) {
+									if (data.digest) {
+										updateProgress = 0;
+										if (data.completed) {
+											updateProgress = Math.round((data.completed / data.total) * 1000) / 10;
+										} else {
+											updateProgress = 100;
+										}
+									} else {
+										toast.success(data.status);
+									}
+								}
+							}
+						}
+					} catch (error) {
+						console.log(error);
+					}
+				}
+			}
+		}
+
+		updateModelId = null;
+		updateProgress = null;
+	};
+
+	const pullModelHandler = async () => {
+		const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, '');
+		console.log($MODEL_DOWNLOAD_POOL);
+		if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
+			toast.error(
+				$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
+					modelTag: sanitizedModelTag
+				})
+			);
+			return;
+		}
+		if (Object.keys($MODEL_DOWNLOAD_POOL).length === MAX_PARALLEL_DOWNLOADS) {
+			toast.error(
+				$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
+			);
+			return;
+		}
+
+		const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, urlIdx).catch(
+			(error) => {
+				toast.error(error);
+				return null;
+			}
+		);
+
+		if (res) {
+			const reader = res.body
+				.pipeThrough(new TextDecoderStream())
+				.pipeThrough(splitStream('\n'))
+				.getReader();
+
+			MODEL_DOWNLOAD_POOL.set({
+				...$MODEL_DOWNLOAD_POOL,
+				[sanitizedModelTag]: {
+					...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
+					abortController: controller,
+					reader,
+					done: false
+				}
+			});
+
+			while (true) {
+				try {
+					const { value, done } = await reader.read();
+					if (done) break;
+
+					let lines = value.split('\n');
+
+					for (const line of lines) {
+						if (line !== '') {
+							let data = JSON.parse(line);
+							console.log(data);
+							if (data.error) {
+								throw data.error;
+							}
+							if (data.detail) {
+								throw data.detail;
+							}
+
+							if (data.status) {
+								if (data.digest) {
+									let downloadProgress = 0;
+									if (data.completed) {
+										downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
+									} else {
+										downloadProgress = 100;
+									}
+
+									MODEL_DOWNLOAD_POOL.set({
+										...$MODEL_DOWNLOAD_POOL,
+										[sanitizedModelTag]: {
+											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
+											pullProgress: downloadProgress,
+											digest: data.digest
+										}
+									});
+								} else {
+									toast.success(data.status);
+
+									MODEL_DOWNLOAD_POOL.set({
+										...$MODEL_DOWNLOAD_POOL,
+										[sanitizedModelTag]: {
+											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
+											done: data.status === 'success'
+										}
+									});
+								}
+							}
+						}
+					}
+				} catch (error) {
+					console.log(error);
+					if (typeof error !== 'string') {
+						error = error.message;
+					}
+
+					toast.error(error);
+					// opts.callback({ success: false, error, modelName: opts.modelName });
+				}
+			}
+
+			console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]);
+
+			if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
+				toast.success(
+					$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
+						modelName: sanitizedModelTag
+					})
+				);
+
+				models.set(await getModels(localStorage.token));
+			} else {
+				toast.error($i18n.t('Download canceled'));
+			}
+
+			delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
+
+			MODEL_DOWNLOAD_POOL.set({
+				...$MODEL_DOWNLOAD_POOL
+			});
+		}
+
+		modelTag = '';
+		modelTransferring = false;
+	};
+
+	const uploadModelHandler = async () => {
+		modelTransferring = true;
+
+		let uploaded = false;
+		let fileResponse = null;
+		let name = '';
+
+		if (modelUploadMode === 'file') {
+			const file = modelInputFile ? modelInputFile[0] : null;
+
+			if (file) {
+				uploadMessage = 'Uploading...';
+
+				fileResponse = await uploadModel(localStorage.token, file, urlIdx).catch((error) => {
+					toast.error(error);
+					return null;
+				});
+			}
+		} else {
+			uploadProgress = 0;
+			fileResponse = await downloadModel(localStorage.token, modelFileUrl, urlIdx).catch(
+				(error) => {
+					toast.error(error);
+					return null;
+				}
+			);
+		}
+
+		if (fileResponse && fileResponse.ok) {
+			const reader = fileResponse.body
+				.pipeThrough(new TextDecoderStream())
+				.pipeThrough(splitStream('\n'))
+				.getReader();
+
+			while (true) {
+				const { value, done } = await reader.read();
+				if (done) break;
+
+				try {
+					let lines = value.split('\n');
+
+					for (const line of lines) {
+						if (line !== '') {
+							let data = JSON.parse(line.replace(/^data: /, ''));
+
+							if (data.progress) {
+								if (uploadMessage) {
+									uploadMessage = '';
+								}
+								uploadProgress = data.progress;
+							}
+
+							if (data.error) {
+								throw data.error;
+							}
+
+							if (data.done) {
+								modelFileDigest = data.blob;
+								name = data.name;
+								uploaded = true;
+							}
+						}
+					}
+				} catch (error) {
+					console.log(error);
+				}
+			}
+		} else {
+			const error = await fileResponse?.json();
+			toast.error(error?.detail ?? error);
+		}
+
+		if (uploaded) {
+			const res = await createModel(
+				localStorage.token,
+				`${name}:latest`,
+				`FROM @${modelFileDigest}\n${modelFileContent}`
+			);
+
+			if (res && res.ok) {
+				const reader = res.body
+					.pipeThrough(new TextDecoderStream())
+					.pipeThrough(splitStream('\n'))
+					.getReader();
+
+				while (true) {
+					const { value, done } = await reader.read();
+					if (done) break;
+
+					try {
+						let lines = value.split('\n');
+
+						for (const line of lines) {
+							if (line !== '') {
+								console.log(line);
+								let data = JSON.parse(line);
+								console.log(data);
+
+								if (data.error) {
+									throw data.error;
+								}
+								if (data.detail) {
+									throw data.detail;
+								}
+
+								if (data.status) {
+									if (
+										!data.digest &&
+										!data.status.includes('writing') &&
+										!data.status.includes('sha256')
+									) {
+										toast.success(data.status);
+									} else {
+										if (data.digest) {
+											digest = data.digest;
+
+											if (data.completed) {
+												pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
+											} else {
+												pullProgress = 100;
+											}
+										}
+									}
+								}
+							}
+						}
+					} catch (error) {
+						console.log(error);
+						toast.error(error);
+					}
+				}
+			}
+		}
+
+		modelFileUrl = '';
+
+		if (modelUploadInputElement) {
+			modelUploadInputElement.value = '';
+		}
+		modelInputFile = null;
+		modelTransferring = false;
+		uploadProgress = null;
+
+		models.set(await getModels(localStorage.token));
+	};
+
+	const deleteModelHandler = async () => {
+		const res = await deleteModel(localStorage.token, deleteModelTag, urlIdx).catch((error) => {
+			toast.error(error);
+		});
+
+		if (res) {
+			toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
+		}
+
+		deleteModelTag = '';
+		models.set(await getModels(localStorage.token));
+	};
+
+	const cancelModelPullHandler = async (model: string) => {
+		const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
+		if (abortController) {
+			abortController.abort();
+		}
+		if (reader) {
+			await reader.cancel();
+			delete $MODEL_DOWNLOAD_POOL[model];
+			MODEL_DOWNLOAD_POOL.set({
+				...$MODEL_DOWNLOAD_POOL
+			});
+			await deleteModel(localStorage.token, model);
+			toast.success(`${model} download has been canceled`);
+		}
+	};
+
+	const createModelHandler = async () => {
+		createModelLoading = true;
+		const res = await createModel(
+			localStorage.token,
+			createModelTag,
+			createModelContent,
+			urlIdx
+		).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res && res.ok) {
+			const reader = res.body
+				.pipeThrough(new TextDecoderStream())
+				.pipeThrough(splitStream('\n'))
+				.getReader();
+
+			while (true) {
+				const { value, done } = await reader.read();
+				if (done) break;
+
+				try {
+					let lines = value.split('\n');
+
+					for (const line of lines) {
+						if (line !== '') {
+							console.log(line);
+							let data = JSON.parse(line);
+							console.log(data);
+
+							if (data.error) {
+								throw data.error;
+							}
+							if (data.detail) {
+								throw data.detail;
+							}
+
+							if (data.status) {
+								if (
+									!data.digest &&
+									!data.status.includes('writing') &&
+									!data.status.includes('sha256')
+								) {
+									toast.success(data.status);
+								} else {
+									if (data.digest) {
+										createModelDigest = data.digest;
+
+										if (data.completed) {
+											createModelPullProgress =
+												Math.round((data.completed / data.total) * 1000) / 10;
+										} else {
+											createModelPullProgress = 100;
+										}
+									}
+								}
+							}
+						}
+					}
+				} catch (error) {
+					console.log(error);
+					toast.error(error);
+				}
+			}
+		}
+
+		models.set(await getModels(localStorage.token));
+
+		createModelLoading = false;
+
+		createModelTag = '';
+		createModelContent = '';
+		createModelDigest = '';
+		createModelPullProgress = null;
+	};
+
+	const init = async () => {
+		loading = true;
+		ollamaModels = await getOllamaModels(localStorage.token, urlIdx);
+
+		console.log(ollamaModels);
+		loading = false;
+	};
+
+	$: if (show) {
+		init();
+	}
+</script>
+
+<ModelDeleteConfirmDialog
+	bind:show={showModelDeleteConfirm}
+	on:confirm={() => {
+		deleteModelHandler();
+	}}
+/>
+
+<Modal size="sm" bind:show>
+	<div>
+		<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
+			<div class=" text-lg font-medium self-center font-primary">
+				{$i18n.t('Manage Ollama')}
+			</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
+			{#if !loading}
+				<div class=" flex flex-col w-full">
+					<div>
+						<div class="space-y-2">
+							<div>
+								<div class=" mb-2 text-sm font-medium">
+									{$i18n.t('Pull a model from Ollama.com')}
+								</div>
+								<div class="flex w-full">
+									<div class="flex-1 mr-2">
+										<input
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
+												modelTag: 'mistral:7b'
+											})}
+											bind:value={modelTag}
+										/>
+									</div>
+									<button
+										class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+										on:click={() => {
+											pullModelHandler();
+										}}
+										disabled={modelTransferring}
+									>
+										{#if modelTransferring}
+											<div class="self-center">
+												<svg
+													class=" w-4 h-4"
+													viewBox="0 0 24 24"
+													fill="currentColor"
+													xmlns="http://www.w3.org/2000/svg"
+												>
+													<style>
+														.spinner_ajPY {
+															transform-origin: center;
+															animation: spinner_AtaB 0.75s infinite linear;
+														}
+
+														@keyframes spinner_AtaB {
+															100% {
+																transform: rotate(360deg);
+															}
+														}
+													</style>
+													<path
+														d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+														opacity=".25"
+													/>
+													<path
+														d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+														class="spinner_ajPY"
+													/>
+												</svg>
+											</div>
+										{:else}
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 16 16"
+												fill="currentColor"
+												class="w-4 h-4"
+											>
+												<path
+													d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
+												/>
+												<path
+													d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
+												/>
+											</svg>
+										{/if}
+									</button>
+								</div>
+
+								<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
+									{$i18n.t('To access the available model names for downloading,')}
+									<a
+										class=" text-gray-500 dark:text-gray-300 font-medium underline"
+										href="https://ollama.com/library"
+										target="_blank">{$i18n.t('click here.')}</a
+									>
+								</div>
+
+								{#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
+									{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
+										{#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
+											<div class="flex flex-col">
+												<div class="font-medium mb-1">{model}</div>
+												<div class="">
+													<div class="flex flex-row justify-between space-x-4 pr-2">
+														<div class=" flex-1">
+															<div
+																class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
+																style="width: {Math.max(
+																	15,
+																	$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
+																)}%"
+															>
+																{$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
+															</div>
+														</div>
+
+														<Tooltip content={$i18n.t('Cancel')}>
+															<button
+																class="text-gray-800 dark:text-gray-100"
+																on:click={() => {
+																	cancelModelPullHandler(model);
+																}}
+															>
+																<svg
+																	class="w-4 h-4 text-gray-800 dark:text-white"
+																	aria-hidden="true"
+																	xmlns="http://www.w3.org/2000/svg"
+																	width="24"
+																	height="24"
+																	fill="currentColor"
+																	viewBox="0 0 24 24"
+																>
+																	<path
+																		stroke="currentColor"
+																		stroke-linecap="round"
+																		stroke-linejoin="round"
+																		stroke-width="2"
+																		d="M6 18 17.94 6M18 18 6.06 6"
+																	/>
+																</svg>
+															</button>
+														</Tooltip>
+													</div>
+													{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
+														<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
+															{$MODEL_DOWNLOAD_POOL[model].digest}
+														</div>
+													{/if}
+												</div>
+											</div>
+										{/if}
+									{/each}
+								{/if}
+							</div>
+
+							<div>
+								<div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
+								<div class="flex w-full">
+									<div class="flex-1 mr-2">
+										<select
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											bind:value={deleteModelTag}
+											placeholder={$i18n.t('Select a model')}
+										>
+											{#if !deleteModelTag}
+												<option value="" disabled selected>{$i18n.t('Select a model')}</option>
+											{/if}
+											{#each ollamaModels as model}
+												<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
+													>{model.name +
+														' (' +
+														(model.size / 1024 ** 3).toFixed(1) +
+														' GB)'}</option
+												>
+											{/each}
+										</select>
+									</div>
+									<button
+										class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+										on:click={() => {
+											showModelDeleteConfirm = true;
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 16 16"
+											fill="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												fill-rule="evenodd"
+												d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
+												clip-rule="evenodd"
+											/>
+										</svg>
+									</button>
+								</div>
+							</div>
+
+							<div>
+								<div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div>
+								<div class="flex w-full">
+									<div class="flex-1 mr-2 flex flex-col gap-2">
+										<input
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
+												modelTag: 'my-modelfile'
+											})}
+											bind:value={createModelTag}
+											disabled={createModelLoading}
+										/>
+
+										<textarea
+											bind:value={createModelContent}
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
+											rows="6"
+											placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
+											disabled={createModelLoading}
+										/>
+									</div>
+
+									<div class="flex self-start">
+										<button
+											class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
+											on:click={() => {
+												createModelHandler();
+											}}
+											disabled={createModelLoading}
+										>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 16 16"
+												fill="currentColor"
+												class="size-4"
+											>
+												<path
+													d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
+												/>
+												<path
+													d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
+												/>
+											</svg>
+										</button>
+									</div>
+								</div>
+
+								{#if createModelDigest !== ''}
+									<div class="flex flex-col mt-1">
+										<div class="font-medium mb-1">{createModelTag}</div>
+										<div class="">
+											<div class="flex flex-row justify-between space-x-4 pr-2">
+												<div class=" flex-1">
+													<div
+														class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
+														style="width: {Math.max(15, createModelPullProgress ?? 0)}%"
+													>
+														{createModelPullProgress ?? 0}%
+													</div>
+												</div>
+											</div>
+											{#if createModelDigest}
+												<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
+													{createModelDigest}
+												</div>
+											{/if}
+										</div>
+									</div>
+								{/if}
+							</div>
+
+							<div class="pt-1">
+								<div class="flex justify-between items-center text-xs">
+									<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
+									<button
+										class=" text-xs font-medium text-gray-500"
+										type="button"
+										on:click={() => {
+											showExperimentalOllama = !showExperimentalOllama;
+										}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
+									>
+								</div>
+							</div>
+
+							{#if showExperimentalOllama}
+								<form
+									on:submit|preventDefault={() => {
+										uploadModelHandler();
+									}}
+								>
+									<div class=" mb-2 flex w-full justify-between">
+										<div class="  text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
+
+										<button
+											class="p-1 px-3 text-xs flex rounded transition"
+											on:click={() => {
+												if (modelUploadMode === 'file') {
+													modelUploadMode = 'url';
+												} else {
+													modelUploadMode = 'file';
+												}
+											}}
+											type="button"
+										>
+											{#if modelUploadMode === 'file'}
+												<span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
+											{:else}
+												<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
+											{/if}
+										</button>
+									</div>
+
+									<div class="flex w-full mb-1.5">
+										<div class="flex flex-col w-full">
+											{#if modelUploadMode === 'file'}
+												<div
+													class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"
+												>
+													<input
+														id="model-upload-input"
+														bind:this={modelUploadInputElement}
+														type="file"
+														bind:files={modelInputFile}
+														on:change={() => {
+															console.log(modelInputFile);
+														}}
+														accept=".gguf,.safetensors"
+														required
+														hidden
+													/>
+
+													<button
+														type="button"
+														class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
+														on:click={() => {
+															modelUploadInputElement.click();
+														}}
+													>
+														{#if modelInputFile && modelInputFile.length > 0}
+															{modelInputFile[0].name}
+														{:else}
+															{$i18n.t('Click here to select')}
+														{/if}
+													</button>
+												</div>
+											{:else}
+												<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
+													<input
+														class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
+														''
+															? 'mr-2'
+															: ''}"
+														type="url"
+														required
+														bind:value={modelFileUrl}
+														placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
+													/>
+												</div>
+											{/if}
+										</div>
+
+										{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
+											<button
+												class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
+												type="submit"
+												disabled={modelTransferring}
+											>
+												{#if modelTransferring}
+													<div class="self-center">
+														<svg
+															class=" w-4 h-4"
+															viewBox="0 0 24 24"
+															fill="currentColor"
+															xmlns="http://www.w3.org/2000/svg"
+														>
+															<style>
+																.spinner_ajPY {
+																	transform-origin: center;
+																	animation: spinner_AtaB 0.75s infinite linear;
+																}
+
+																@keyframes spinner_AtaB {
+																	100% {
+																		transform: rotate(360deg);
+																	}
+																}
+															</style>
+															<path
+																d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+																opacity=".25"
+															/>
+															<path
+																d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+																class="spinner_ajPY"
+															/>
+														</svg>
+													</div>
+												{:else}
+													<svg
+														xmlns="http://www.w3.org/2000/svg"
+														viewBox="0 0 16 16"
+														fill="currentColor"
+														class="w-4 h-4"
+													>
+														<path
+															d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
+														/>
+														<path
+															d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
+														/>
+													</svg>
+												{/if}
+											</button>
+										{/if}
+									</div>
+
+									{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
+										<div>
+											<div>
+												<div class=" my-2.5 text-sm font-medium">
+													{$i18n.t('Modelfile Content')}
+												</div>
+												<textarea
+													bind:value={modelFileContent}
+													class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
+													rows="6"
+												/>
+											</div>
+										</div>
+									{/if}
+									<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
+										{$i18n.t('To access the GGUF models available for downloading,')}
+										<a
+											class=" text-gray-500 dark:text-gray-300 font-medium underline"
+											href="https://huggingface.co/models?search=gguf"
+											target="_blank">{$i18n.t('click here.')}</a
+										>
+									</div>
+
+									{#if uploadMessage}
+										<div class="mt-2">
+											<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
+
+											<div class="w-full rounded-full dark:bg-gray-800">
+												<div
+													class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
+													style="width: 100%"
+												>
+													{uploadMessage}
+												</div>
+											</div>
+											<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
+												{modelFileDigest}
+											</div>
+										</div>
+									{:else if uploadProgress !== null}
+										<div class="mt-2">
+											<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
+
+											<div class="w-full rounded-full dark:bg-gray-800">
+												<div
+													class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
+													style="width: {Math.max(15, uploadProgress ?? 0)}%"
+												>
+													{uploadProgress ?? 0}%
+												</div>
+											</div>
+											<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
+												{modelFileDigest}
+											</div>
+										</div>
+									{/if}
+								</form>
+							{/if}
+						</div>
+					</div>
+				</div>
+			{:else}
+				<Spinner />
+			{/if}
+		</div>
+	</div>
+</Modal>

+ 20 - 1
src/lib/components/admin/Settings/Connections/OllamaConnection.svelte

@@ -4,15 +4,20 @@
 
 
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
-	import Cog6 from '$lib/components/icons/Cog6.svelte';
 	import AddConnectionModal from './AddConnectionModal.svelte';
 	import AddConnectionModal from './AddConnectionModal.svelte';
 
 
+	import Cog6 from '$lib/components/icons/Cog6.svelte';
+	import Wrench from '$lib/components/icons/Wrench.svelte';
+	import ManageOllamaModal from './ManageOllamaModal.svelte';
+
 	export let onDelete = () => {};
 	export let onDelete = () => {};
 	export let onSubmit = () => {};
 	export let onSubmit = () => {};
 
 
 	export let url = '';
 	export let url = '';
+	export let idx = 0;
 	export let config = {};
 	export let config = {};
 
 
+	let showManageModal = false;
 	let showConfigModal = false;
 	let showConfigModal = false;
 </script>
 </script>
 
 
@@ -33,6 +38,8 @@
 	}}
 	}}
 />
 />
 
 
+<ManageOllamaModal bind:show={showManageModal} urlIdx={idx} />
+
 <div class="flex gap-1.5">
 <div class="flex gap-1.5">
 	<Tooltip
 	<Tooltip
 		className="w-full relative"
 		className="w-full relative"
@@ -55,6 +62,18 @@
 	</Tooltip>
 	</Tooltip>
 
 
 	<div class="flex gap-1">
 	<div class="flex gap-1">
+		<Tooltip content={$i18n.t('Manage')} className="self-start">
+			<button
+				class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
+				on:click={() => {
+					showManageModal = true;
+				}}
+				type="button"
+			>
+				<Wrench />
+			</button>
+		</Tooltip>
+
 		<Tooltip content={$i18n.t('Configure')} className="self-start">
 		<Tooltip content={$i18n.t('Configure')} className="self-start">
 			<button
 			<button
 				class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
 				class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"

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

@@ -19,7 +19,7 @@
 	} from '$lib/apis/retrieval';
 	} from '$lib/apis/retrieval';
 
 
 	import { knowledge, models } from '$lib/stores';
 	import { knowledge, models } from '$lib/stores';
-	import { getKnowledgeItems } from '$lib/apis/knowledge';
+	import { getKnowledgeBases } from '$lib/apis/knowledge';
 	import { uploadDir, deleteAllFiles, deleteFileById } from '$lib/apis/files';
 	import { uploadDir, deleteAllFiles, deleteFileById } from '$lib/apis/files';
 
 
 	import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
@@ -312,7 +312,7 @@
 			{#if embeddingEngine === 'openai'}
 			{#if embeddingEngine === 'openai'}
 				<div class="my-0.5 flex gap-2">
 				<div class="my-0.5 flex gap-2">
 					<input
 					<input
-						class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+						class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none"
 						placeholder={$i18n.t('API Base URL')}
 						placeholder={$i18n.t('API Base URL')}
 						bind:value={OpenAIUrl}
 						bind:value={OpenAIUrl}
 						required
 						required
@@ -376,19 +376,12 @@
 			{#if embeddingEngine === 'ollama'}
 			{#if embeddingEngine === 'ollama'}
 				<div class="flex w-full">
 				<div class="flex w-full">
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
-						<select
+						<input
 							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							bind:value={embeddingModel}
 							bind:value={embeddingModel}
-							placeholder={$i18n.t('Select a model')}
+							placeholder={$i18n.t('Set embedding model')}
 							required
 							required
-						>
-							{#if !embeddingModel}
-								<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-							{/if}
-							{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
-								<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
-							{/each}
-						</select>
+						/>
 					</div>
 					</div>
 				</div>
 				</div>
 			{:else}
 			{:else}

+ 279 - 1018
src/lib/components/admin/Settings/Models.svelte

@@ -1,1082 +1,343 @@
 <script lang="ts">
 <script lang="ts">
-	import { toast } from 'svelte-sonner';
-	import { onMount, getContext } from 'svelte';
+	import { marked } from 'marked';
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
 
 
-	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
-	import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
-	import { splitStream } from '$lib/utils';
+	import { onMount, getContext, tick } from 'svelte';
+	const i18n = getContext('i18n');
 
 
+	import { WEBUI_NAME, config, mobile, models as _models, settings, user } from '$lib/stores';
 	import {
 	import {
-		createModel,
-		deleteModel,
-		downloadModel,
-		getOllamaUrls,
-		getOllamaVersion,
-		pullModel,
-		uploadModel,
-		getOllamaConfig
-	} from '$lib/apis/ollama';
-	import { getModels as _getModels } from '$lib/apis';
-
+		createNewModel,
+		getBaseModels,
+		toggleModelById,
+		updateModelById
+	} from '$lib/apis/models';
+
+	import { getModels } from '$lib/apis';
+	import Search from '$lib/components/icons/Search.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Switch from '$lib/components/common/Switch.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
-	import ModelDeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
-
-	const i18n = getContext('i18n');
-
-	const getModels = async () => {
-		return await _getModels(localStorage.token);
-	};
-
-	let modelUploadInputElement: HTMLInputElement;
-
-	let showModelDeleteConfirm = false;
-
-	// Models
-
-	let ollamaEnabled = null;
-
-	let OLLAMA_BASE_URLS = [];
-	let selectedOllamaUrlIdx: number | null = null;
-
-	let updateModelId = null;
-	let updateProgress = null;
-
-	let showExperimentalOllama = false;
 
 
-	let ollamaVersion = null;
-	const MAX_PARALLEL_DOWNLOADS = 3;
+	import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
 
 
-	let modelTransferring = false;
-	let modelTag = '';
+	let importFiles;
+	let modelsImportInputElement: HTMLInputElement;
 
 
-	let createModelLoading = false;
-	let createModelTag = '';
-	let createModelContent = '';
-	let createModelDigest = '';
-	let createModelPullProgress = null;
+	let models = null;
 
 
-	let digest = '';
-	let pullProgress = null;
+	let workspaceModels = null;
+	let baseModels = null;
 
 
-	let modelUploadMode = 'file';
-	let modelInputFile: File[] | null = null;
-	let modelFileUrl = '';
-	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
-	let modelFileDigest = '';
+	let filteredModels = [];
 
 
-	let uploadProgress = null;
-	let uploadMessage = '';
-
-	let deleteModelTag = '';
-
-	const updateModelsHandler = async () => {
-		for (const model of $models.filter(
-			(m) =>
-				!(m?.preset ?? false) &&
-				m.owned_by === 'ollama' &&
-				(selectedOllamaUrlIdx === null
-					? true
-					: (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))
-		)) {
-			console.log(model);
-
-			updateModelId = model.id;
-			const [res, controller] = await pullModel(
-				localStorage.token,
-				model.id,
-				selectedOllamaUrlIdx
-			).catch((error) => {
-				toast.error(error);
-				return null;
-			});
-
-			if (res) {
-				const reader = res.body
-					.pipeThrough(new TextDecoderStream())
-					.pipeThrough(splitStream('\n'))
-					.getReader();
-
-				while (true) {
-					try {
-						const { value, done } = await reader.read();
-						if (done) break;
-
-						let lines = value.split('\n');
-
-						for (const line of lines) {
-							if (line !== '') {
-								let data = JSON.parse(line);
-
-								console.log(data);
-								if (data.error) {
-									throw data.error;
-								}
-								if (data.detail) {
-									throw data.detail;
-								}
-								if (data.status) {
-									if (data.digest) {
-										updateProgress = 0;
-										if (data.completed) {
-											updateProgress = Math.round((data.completed / data.total) * 1000) / 10;
-										} else {
-											updateProgress = 100;
-										}
-									} else {
-										toast.success(data.status);
-									}
-								}
-							}
-						}
-					} catch (error) {
-						console.log(error);
-					}
-				}
-			}
-		}
+	let selectedModelId = null;
 
 
-		updateModelId = null;
-		updateProgress = null;
-	};
+	$: if (models) {
+		filteredModels = models.filter(
+			(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
+		);
+	}
 
 
-	const pullModelHandler = async () => {
-		const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, '');
-		console.log($MODEL_DOWNLOAD_POOL);
-		if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
-			toast.error(
-				$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
-					modelTag: sanitizedModelTag
-				})
-			);
-			return;
-		}
-		if (Object.keys($MODEL_DOWNLOAD_POOL).length === MAX_PARALLEL_DOWNLOADS) {
-			toast.error(
-				$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
-			);
-			return;
-		}
+	let searchValue = '';
 
 
-		const [res, controller] = await pullModel(
-			localStorage.token,
-			sanitizedModelTag,
-			selectedOllamaUrlIdx
-		).catch((error) => {
-			toast.error(error);
-			return null;
+	const downloadModels = async (models) => {
+		let blob = new Blob([JSON.stringify(models)], {
+			type: 'application/json'
 		});
 		});
+		saveAs(blob, `models-export-${Date.now()}.json`);
+	};
 
 
-		if (res) {
-			const reader = res.body
-				.pipeThrough(new TextDecoderStream())
-				.pipeThrough(splitStream('\n'))
-				.getReader();
-
-			MODEL_DOWNLOAD_POOL.set({
-				...$MODEL_DOWNLOAD_POOL,
-				[sanitizedModelTag]: {
-					...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
-					abortController: controller,
-					reader,
-					done: false
-				}
-			});
-
-			while (true) {
-				try {
-					const { value, done } = await reader.read();
-					if (done) break;
-
-					let lines = value.split('\n');
-
-					for (const line of lines) {
-						if (line !== '') {
-							let data = JSON.parse(line);
-							console.log(data);
-							if (data.error) {
-								throw data.error;
-							}
-							if (data.detail) {
-								throw data.detail;
-							}
-
-							if (data.status) {
-								if (data.digest) {
-									let downloadProgress = 0;
-									if (data.completed) {
-										downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
-									} else {
-										downloadProgress = 100;
-									}
-
-									MODEL_DOWNLOAD_POOL.set({
-										...$MODEL_DOWNLOAD_POOL,
-										[sanitizedModelTag]: {
-											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
-											pullProgress: downloadProgress,
-											digest: data.digest
-										}
-									});
-								} else {
-									toast.success(data.status);
-
-									MODEL_DOWNLOAD_POOL.set({
-										...$MODEL_DOWNLOAD_POOL,
-										[sanitizedModelTag]: {
-											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
-											done: data.status === 'success'
-										}
-									});
-								}
-							}
-						}
-					}
-				} catch (error) {
-					console.log(error);
-					if (typeof error !== 'string') {
-						error = error.message;
-					}
-
-					toast.error(error);
-					// opts.callback({ success: false, error, modelName: opts.modelName });
-				}
-			}
-
-			console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]);
+	const init = async () => {
+		workspaceModels = await getBaseModels(localStorage.token);
+		baseModels = await getModels(localStorage.token, true);
 
 
-			if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
-				toast.success(
-					$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
-						modelName: sanitizedModelTag
-					})
-				);
+		models = baseModels.map((m) => {
+			const workspaceModel = workspaceModels.find((wm) => wm.id === m.id);
 
 
-				models.set(await getModels());
+			if (workspaceModel) {
+				return workspaceModel;
 			} else {
 			} else {
-				toast.error($i18n.t('Download canceled'));
+				return {
+					id: m.id,
+					name: m.name,
+					is_active: true
+				};
 			}
 			}
-
-			delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
-
-			MODEL_DOWNLOAD_POOL.set({
-				...$MODEL_DOWNLOAD_POOL
-			});
-		}
-
-		modelTag = '';
-		modelTransferring = false;
+		});
 	};
 	};
 
 
-	const uploadModelHandler = async () => {
-		modelTransferring = true;
-
-		let uploaded = false;
-		let fileResponse = null;
-		let name = '';
-
-		if (modelUploadMode === 'file') {
-			const file = modelInputFile ? modelInputFile[0] : null;
+	const upsertModelHandler = async (model) => {
+		model.base_model_id = null;
 
 
-			if (file) {
-				uploadMessage = 'Uploading...';
-
-				fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch(
-					(error) => {
-						toast.error(error);
-						return null;
-					}
-				);
-			}
-		} else {
-			uploadProgress = 0;
-			fileResponse = await downloadModel(
-				localStorage.token,
-				modelFileUrl,
-				selectedOllamaUrlIdx
-			).catch((error) => {
-				toast.error(error);
+		if (workspaceModels.find((m) => m.id === model.id)) {
+			await updateModelById(localStorage.token, model.id, model).catch((error) => {
 				return null;
 				return null;
 			});
 			});
-		}
-
-		if (fileResponse && fileResponse.ok) {
-			const reader = fileResponse.body
-				.pipeThrough(new TextDecoderStream())
-				.pipeThrough(splitStream('\n'))
-				.getReader();
-
-			while (true) {
-				const { value, done } = await reader.read();
-				if (done) break;
-
-				try {
-					let lines = value.split('\n');
-
-					for (const line of lines) {
-						if (line !== '') {
-							let data = JSON.parse(line.replace(/^data: /, ''));
-
-							if (data.progress) {
-								if (uploadMessage) {
-									uploadMessage = '';
-								}
-								uploadProgress = data.progress;
-							}
-
-							if (data.error) {
-								throw data.error;
-							}
-
-							if (data.done) {
-								modelFileDigest = data.blob;
-								name = data.name;
-								uploaded = true;
-							}
-						}
-					}
-				} catch (error) {
-					console.log(error);
-				}
-			}
 		} else {
 		} else {
-			const error = await fileResponse?.json();
-			toast.error(error?.detail ?? error);
-		}
-
-		if (uploaded) {
-			const res = await createModel(
-				localStorage.token,
-				`${name}:latest`,
-				`FROM @${modelFileDigest}\n${modelFileContent}`
-			);
-
-			if (res && res.ok) {
-				const reader = res.body
-					.pipeThrough(new TextDecoderStream())
-					.pipeThrough(splitStream('\n'))
-					.getReader();
-
-				while (true) {
-					const { value, done } = await reader.read();
-					if (done) break;
-
-					try {
-						let lines = value.split('\n');
-
-						for (const line of lines) {
-							if (line !== '') {
-								console.log(line);
-								let data = JSON.parse(line);
-								console.log(data);
-
-								if (data.error) {
-									throw data.error;
-								}
-								if (data.detail) {
-									throw data.detail;
-								}
-
-								if (data.status) {
-									if (
-										!data.digest &&
-										!data.status.includes('writing') &&
-										!data.status.includes('sha256')
-									) {
-										toast.success(data.status);
-									} else {
-										if (data.digest) {
-											digest = data.digest;
-
-											if (data.completed) {
-												pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
-											} else {
-												pullProgress = 100;
-											}
-										}
-									}
-								}
-							}
-						}
-					} catch (error) {
-						console.log(error);
-						toast.error(error);
-					}
-				}
-			}
-		}
-
-		modelFileUrl = '';
-
-		if (modelUploadInputElement) {
-			modelUploadInputElement.value = '';
-		}
-		modelInputFile = null;
-		modelTransferring = false;
-		uploadProgress = null;
-
-		models.set(await getModels());
-	};
-
-	const deleteModelHandler = async () => {
-		const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch(
-			(error) => {
-				toast.error(error);
-			}
-		);
-
-		if (res) {
-			toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
+			await createNewModel(localStorage.token, model).catch((error) => {
+				return null;
+			});
 		}
 		}
 
 
-		deleteModelTag = '';
-		models.set(await getModels());
+		_models.set(await getModels(localStorage.token));
+		await init();
 	};
 	};
 
 
-	const cancelModelPullHandler = async (model: string) => {
-		const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
-		if (abortController) {
-			abortController.abort();
-		}
-		if (reader) {
-			await reader.cancel();
-			delete $MODEL_DOWNLOAD_POOL[model];
-			MODEL_DOWNLOAD_POOL.set({
-				...$MODEL_DOWNLOAD_POOL
+	const toggleModelHandler = async (model) => {
+		if (!Object.keys(model).includes('base_model_id')) {
+			await createNewModel(localStorage.token, {
+				id: model.id,
+				name: model.name,
+				base_model_id: null,
+				meta: {},
+				params: {},
+				is_active: model.is_active
+			}).catch((error) => {
+				return null;
 			});
 			});
-			await deleteModel(localStorage.token, model);
-			toast.success(`${model} download has been canceled`);
-		}
-	};
-
-	const createModelHandler = async () => {
-		createModelLoading = true;
-		const res = await createModel(
-			localStorage.token,
-			createModelTag,
-			createModelContent,
-			selectedOllamaUrlIdx
-		).catch((error) => {
-			toast.error(error);
-			return null;
-		});
-
-		if (res && res.ok) {
-			const reader = res.body
-				.pipeThrough(new TextDecoderStream())
-				.pipeThrough(splitStream('\n'))
-				.getReader();
-
-			while (true) {
-				const { value, done } = await reader.read();
-				if (done) break;
-
-				try {
-					let lines = value.split('\n');
-
-					for (const line of lines) {
-						if (line !== '') {
-							console.log(line);
-							let data = JSON.parse(line);
-							console.log(data);
-
-							if (data.error) {
-								throw data.error;
-							}
-							if (data.detail) {
-								throw data.detail;
-							}
 
 
-							if (data.status) {
-								if (
-									!data.digest &&
-									!data.status.includes('writing') &&
-									!data.status.includes('sha256')
-								) {
-									toast.success(data.status);
-								} else {
-									if (data.digest) {
-										createModelDigest = data.digest;
-
-										if (data.completed) {
-											createModelPullProgress =
-												Math.round((data.completed / data.total) * 1000) / 10;
-										} else {
-											createModelPullProgress = 100;
-										}
-									}
-								}
-							}
-						}
-					}
-				} catch (error) {
-					console.log(error);
-					toast.error(error);
-				}
-			}
+			await init();
+		} else {
+			await toggleModelById(localStorage.token, model.id);
 		}
 		}
 
 
-		models.set(await getModels());
-
-		createModelLoading = false;
-
-		createModelTag = '';
-		createModelContent = '';
-		createModelDigest = '';
-		createModelPullProgress = null;
+		_models.set(await getModels(localStorage.token));
 	};
 	};
 
 
 	onMount(async () => {
 	onMount(async () => {
-		const ollamaConfig = await getOllamaConfig(localStorage.token);
-
-		if (ollamaConfig.ENABLE_OLLAMA_API) {
-			ollamaEnabled = true;
-
-			OLLAMA_BASE_URLS = ollamaConfig.OLLAMA_BASE_URLS;
-
-			if (OLLAMA_BASE_URLS.length > 0) {
-				selectedOllamaUrlIdx = 0;
-			}
-
-			ollamaVersion = true;
-		} else {
-			ollamaEnabled = false;
-			toast.error($i18n.t('Ollama API is disabled'));
-		}
+		init();
 	});
 	});
 </script>
 </script>
 
 
-<ModelDeleteConfirmDialog
-	bind:show={showModelDeleteConfirm}
-	on:confirm={() => {
-		deleteModelHandler();
-	}}
-/>
-
-<div class="flex flex-col h-full justify-between text-sm">
-	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
-		{#if ollamaEnabled}
-			{#if ollamaVersion !== null}
-				<div class="space-y-2 pr-1.5">
-					<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
+{#if models !== null}
+	{#if selectedModelId === null}
+		<div class="flex flex-col gap-1 mt-1.5 mb-2">
+			<div class="flex justify-between items-center">
+				<div class="flex items-center md:self-center text-xl font-medium px-0.5">
+					{$i18n.t('Models')}
+					<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
+					<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
+						>{filteredModels.length}</span
+					>
+				</div>
+			</div>
 
 
-					{#if OLLAMA_BASE_URLS.length > 0}
-						<div class="flex gap-2">
-							<div class="flex-1 pb-1">
-								<select
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-									bind:value={selectedOllamaUrlIdx}
-									placeholder={$i18n.t('Select an Ollama instance')}
+			<div class=" flex flex-1 items-center w-full space-x-2">
+				<div class="flex flex-1 items-center">
+					<div class=" self-center ml-1 mr-3">
+						<Search className="size-3.5" />
+					</div>
+					<input
+						class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
+						bind:value={searchValue}
+						placeholder={$i18n.t('Search Models')}
+					/>
+				</div>
+			</div>
+		</div>
+
+		<div class=" my-2 mb-5" id="model-list">
+			{#if models.length > 0}
+				{#each filteredModels as model (model.id)}
+					<div
+						class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
+						id="model-item-{model.id}"
+					>
+						<button
+							class=" flex flex-1 text-left space-x-3.5 cursor-pointer w-full"
+							type="button"
+							on:click={() => {
+								selectedModelId = model.id;
+							}}
+						>
+							<div class=" self-center w-8">
+								<div
+									class=" rounded-full object-cover {(model?.is_active ?? true)
+										? ''
+										: 'opacity-50 dark:opacity-50'} "
 								>
 								>
-									{#each OLLAMA_BASE_URLS as url, idx}
-										<option value={idx} class="bg-gray-50 dark:bg-gray-700">{url}</option>
-									{/each}
-								</select>
-							</div>
-
-							<div>
-								<div class="flex w-full justify-end">
-									<Tooltip content="Update All Models" placement="top">
-										<button
-											class="p-2.5 flex gap-2 items-center bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
-											on:click={() => {
-												updateModelsHandler();
-											}}
-										>
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												viewBox="0 0 16 16"
-												fill="currentColor"
-												class="w-4 h-4"
-											>
-												<path
-													d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z"
-												/>
-												<path
-													d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z"
-												/>
-											</svg>
-										</button>
-									</Tooltip>
-								</div>
-							</div>
-						</div>
-
-						{#if updateModelId}
-							Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
-						{/if}
-					{/if}
-
-					<div class="space-y-2">
-						<div>
-							<div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
-							<div class="flex w-full">
-								<div class="flex-1 mr-2">
-									<input
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-										placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
-											modelTag: 'mistral:7b'
-										})}
-										bind:value={modelTag}
+									<img
+										src={model?.meta?.profile_image_url ?? '/static/favicon.png'}
+										alt="modelfile profile"
+										class=" rounded-full w-full h-auto object-cover"
 									/>
 									/>
 								</div>
 								</div>
-								<button
-									class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
-									on:click={() => {
-										pullModelHandler();
-									}}
-									disabled={modelTransferring}
-								>
-									{#if modelTransferring}
-										<div class="self-center">
-											<svg
-												class=" w-4 h-4"
-												viewBox="0 0 24 24"
-												fill="currentColor"
-												xmlns="http://www.w3.org/2000/svg"
-											>
-												<style>
-													.spinner_ajPY {
-														transform-origin: center;
-														animation: spinner_AtaB 0.75s infinite linear;
-													}
-
-													@keyframes spinner_AtaB {
-														100% {
-															transform: rotate(360deg);
-														}
-													}
-												</style>
-												<path
-													d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-													opacity=".25"
-												/>
-												<path
-													d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-													class="spinner_ajPY"
-												/>
-											</svg>
-										</div>
-									{:else}
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 16 16"
-											fill="currentColor"
-											class="w-4 h-4"
-										>
-											<path
-												d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
-											/>
-											<path
-												d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
-											/>
-										</svg>
-									{/if}
-								</button>
 							</div>
 							</div>
 
 
-							<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
-								{$i18n.t('To access the available model names for downloading,')}
-								<a
-									class=" text-gray-500 dark:text-gray-300 font-medium underline"
-									href="https://ollama.com/library"
-									target="_blank">{$i18n.t('click here.')}</a
+							<div class=" flex-1 self-center {(model?.is_active ?? true) ? '' : 'text-gray-500'}">
+								<Tooltip
+									content={marked.parse(model?.meta?.description ?? model.id)}
+									className=" w-fit"
+									placement="top-start"
 								>
 								>
-							</div>
-
-							{#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
-								{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
-									{#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
-										<div class="flex flex-col">
-											<div class="font-medium mb-1">{model}</div>
-											<div class="">
-												<div class="flex flex-row justify-between space-x-4 pr-2">
-													<div class=" flex-1">
-														<div
-															class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
-															style="width: {Math.max(
-																15,
-																$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
-															)}%"
-														>
-															{$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
-														</div>
-													</div>
-
-													<Tooltip content={$i18n.t('Cancel')}>
-														<button
-															class="text-gray-800 dark:text-gray-100"
-															on:click={() => {
-																cancelModelPullHandler(model);
-															}}
-														>
-															<svg
-																class="w-4 h-4 text-gray-800 dark:text-white"
-																aria-hidden="true"
-																xmlns="http://www.w3.org/2000/svg"
-																width="24"
-																height="24"
-																fill="currentColor"
-																viewBox="0 0 24 24"
-															>
-																<path
-																	stroke="currentColor"
-																	stroke-linecap="round"
-																	stroke-linejoin="round"
-																	stroke-width="2"
-																	d="M6 18 17.94 6M18 18 6.06 6"
-																/>
-															</svg>
-														</button>
-													</Tooltip>
-												</div>
-												{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
-													<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-														{$MODEL_DOWNLOAD_POOL[model].digest}
-													</div>
-												{/if}
-											</div>
-										</div>
-									{/if}
-								{/each}
-							{/if}
-						</div>
-
-						<div>
-							<div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
-							<div class="flex w-full">
-								<div class="flex-1 mr-2">
-									<select
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-										bind:value={deleteModelTag}
-										placeholder={$i18n.t('Select a model')}
-									>
-										{#if !deleteModelTag}
-											<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-										{/if}
-										{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
-											<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
-												>{model.name +
-													' (' +
-													(model.ollama.size / 1024 ** 3).toFixed(1) +
-													' GB)'}</option
-											>
-										{/each}
-									</select>
+									<div class="  font-semibold line-clamp-1">{model.name}</div>
+								</Tooltip>
+								<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
+									{model?.meta?.description ?? model.id}
 								</div>
 								</div>
-								<button
-									class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
-									on:click={() => {
-										showModelDeleteConfirm = true;
-									}}
-								>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 16 16"
-										fill="currentColor"
-										class="w-4 h-4"
-									>
-										<path
-											fill-rule="evenodd"
-											d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
-											clip-rule="evenodd"
-										/>
-									</svg>
-								</button>
 							</div>
 							</div>
-						</div>
-
-						<div>
-							<div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div>
-							<div class="flex w-full">
-								<div class="flex-1 mr-2 flex flex-col gap-2">
-									<input
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-										placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
-											modelTag: 'my-modelfile'
-										})}
-										bind:value={createModelTag}
-										disabled={createModelLoading}
-									/>
-
-									<textarea
-										bind:value={createModelContent}
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
-										rows="6"
-										placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
-										disabled={createModelLoading}
+						</button>
+						<div class="flex flex-row gap-0.5 items-center self-center">
+							<button
+								class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+								type="button"
+								on:click={() => {
+									selectedModelId = model.id;
+								}}
+							>
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									fill="none"
+									viewBox="0 0 24 24"
+									stroke-width="1.5"
+									stroke="currentColor"
+									class="w-4 h-4"
+								>
+									<path
+										stroke-linecap="round"
+										stroke-linejoin="round"
+										d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
 									/>
 									/>
-								</div>
+								</svg>
+							</button>
 
 
-								<div class="flex self-start">
-									<button
-										class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
-										on:click={() => {
-											createModelHandler();
-										}}
-										disabled={createModelLoading}
-									>
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 16 16"
-											fill="currentColor"
-											class="size-4"
-										>
-											<path
-												d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
-											/>
-											<path
-												d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
-											/>
-										</svg>
-									</button>
-								</div>
-							</div>
-
-							{#if createModelDigest !== ''}
-								<div class="flex flex-col mt-1">
-									<div class="font-medium mb-1">{createModelTag}</div>
-									<div class="">
-										<div class="flex flex-row justify-between space-x-4 pr-2">
-											<div class=" flex-1">
-												<div
-													class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
-													style="width: {Math.max(15, createModelPullProgress ?? 0)}%"
-												>
-													{createModelPullProgress ?? 0}%
-												</div>
-											</div>
-										</div>
-										{#if createModelDigest}
-											<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-												{createModelDigest}
-											</div>
-										{/if}
-									</div>
-								</div>
-							{/if}
-						</div>
-
-						<div class="pt-1">
-							<div class="flex justify-between items-center text-xs">
-								<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
-								<button
-									class=" text-xs font-medium text-gray-500"
-									type="button"
-									on:click={() => {
-										showExperimentalOllama = !showExperimentalOllama;
-									}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
+							<div class="ml-1">
+								<Tooltip
+									content={(model?.is_active ?? true) ? $i18n.t('Enabled') : $i18n.t('Disabled')}
 								>
 								>
+									<Switch
+										bind:state={model.is_active}
+										on:change={async () => {
+											toggleModelHandler(model);
+										}}
+									/>
+								</Tooltip>
 							</div>
 							</div>
 						</div>
 						</div>
-
-						{#if showExperimentalOllama}
-							<form
-								on:submit|preventDefault={() => {
-									uploadModelHandler();
-								}}
-							>
-								<div class=" mb-2 flex w-full justify-between">
-									<div class="  text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
-
-									<button
-										class="p-1 px-3 text-xs flex rounded transition"
-										on:click={() => {
-											if (modelUploadMode === 'file') {
-												modelUploadMode = 'url';
-											} else {
-												modelUploadMode = 'file';
-											}
-										}}
-										type="button"
-									>
-										{#if modelUploadMode === 'file'}
-											<span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
-										{:else}
-											<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
-										{/if}
-									</button>
-								</div>
-
-								<div class="flex w-full mb-1.5">
-									<div class="flex flex-col w-full">
-										{#if modelUploadMode === 'file'}
-											<div
-												class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"
-											>
-												<input
-													id="model-upload-input"
-													bind:this={modelUploadInputElement}
-													type="file"
-													bind:files={modelInputFile}
-													on:change={() => {
-														console.log(modelInputFile);
-													}}
-													accept=".gguf,.safetensors"
-													required
-													hidden
-												/>
-
-												<button
-													type="button"
-													class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
-													on:click={() => {
-														modelUploadInputElement.click();
-													}}
-												>
-													{#if modelInputFile && modelInputFile.length > 0}
-														{modelInputFile[0].name}
-													{:else}
-														{$i18n.t('Click here to select')}
-													{/if}
-												</button>
-											</div>
-										{:else}
-											<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
-												<input
-													class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
-													''
-														? 'mr-2'
-														: ''}"
-													type="url"
-													required
-													bind:value={modelFileUrl}
-													placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
-												/>
-											</div>
-										{/if}
-									</div>
-
-									{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
-										<button
-											class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
-											type="submit"
-											disabled={modelTransferring}
-										>
-											{#if modelTransferring}
-												<div class="self-center">
-													<svg
-														class=" w-4 h-4"
-														viewBox="0 0 24 24"
-														fill="currentColor"
-														xmlns="http://www.w3.org/2000/svg"
-													>
-														<style>
-															.spinner_ajPY {
-																transform-origin: center;
-																animation: spinner_AtaB 0.75s infinite linear;
-															}
-
-															@keyframes spinner_AtaB {
-																100% {
-																	transform: rotate(360deg);
-																}
-															}
-														</style>
-														<path
-															d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-															opacity=".25"
-														/>
-														<path
-															d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-															class="spinner_ajPY"
-														/>
-													</svg>
-												</div>
-											{:else}
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													viewBox="0 0 16 16"
-													fill="currentColor"
-													class="w-4 h-4"
-												>
-													<path
-														d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
-													/>
-													<path
-														d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
-													/>
-												</svg>
-											{/if}
-										</button>
-									{/if}
-								</div>
-
-								{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
-									<div>
-										<div>
-											<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
-											<textarea
-												bind:value={modelFileContent}
-												class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
-												rows="6"
-											/>
-										</div>
-									</div>
-								{/if}
-								<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
-									{$i18n.t('To access the GGUF models available for downloading,')}
-									<a
-										class=" text-gray-500 dark:text-gray-300 font-medium underline"
-										href="https://huggingface.co/models?search=gguf"
-										target="_blank">{$i18n.t('click here.')}</a
-									>
-								</div>
-
-								{#if uploadMessage}
-									<div class="mt-2">
-										<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
-
-										<div class="w-full rounded-full dark:bg-gray-800">
-											<div
-												class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
-												style="width: 100%"
-											>
-												{uploadMessage}
-											</div>
-										</div>
-										<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-											{modelFileDigest}
-										</div>
-									</div>
-								{:else if uploadProgress !== null}
-									<div class="mt-2">
-										<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
-
-										<div class="w-full rounded-full dark:bg-gray-800">
-											<div
-												class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
-												style="width: {Math.max(15, uploadProgress ?? 0)}%"
-											>
-												{uploadProgress ?? 0}%
-											</div>
-										</div>
-										<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-											{modelFileDigest}
-										</div>
-									</div>
-								{/if}
-							</form>
-						{/if}
 					</div>
 					</div>
-				</div>
-			{:else if ollamaVersion === false}
-				<div>Ollama Not Detected</div>
+				{/each}
 			{:else}
 			{:else}
-				<div class="flex h-full justify-center">
-					<div class="my-auto">
-						<Spinner className="size-6" />
+				<div class="flex flex-col items-center justify-center w-full h-20">
+					<div class="text-gray-500 dark:text-gray-400 text-xs">
+						{$i18n.t('No models found')}
 					</div>
 					</div>
 				</div>
 				</div>
 			{/if}
 			{/if}
-		{:else if ollamaEnabled === false}
-			<div>{$i18n.t('Ollama API is disabled')}</div>
-		{:else}
-			<div class="flex h-full justify-center">
-				<div class="my-auto">
-					<Spinner className="size-6" />
+		</div>
+
+		{#if $user?.role === 'admin'}
+			<div class=" flex justify-end w-full mb-3">
+				<div class="flex space-x-1">
+					<input
+						id="models-import-input"
+						bind:this={modelsImportInputElement}
+						bind:files={importFiles}
+						type="file"
+						accept=".json"
+						hidden
+						on:change={() => {
+							console.log(importFiles);
+
+							let reader = new FileReader();
+							reader.onload = async (event) => {
+								let savedModels = JSON.parse(event.target.result);
+								console.log(savedModels);
+
+								for (const model of savedModels) {
+									if (Object.keys(model).includes('base_model_id')) {
+										if (model.base_model_id === null) {
+											upsertModelHandler(model);
+										}
+									} else {
+										if (model?.info ?? false) {
+											if (model.info.base_model_id === null) {
+												upsertModelHandler(model.info);
+											}
+										}
+									}
+								}
+
+								await _models.set(await getModels(localStorage.token));
+								init();
+							};
+
+							reader.readAsText(importFiles[0]);
+						}}
+					/>
+
+					<button
+						class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+						on:click={() => {
+							modelsImportInputElement.click();
+						}}
+					>
+						<div class=" self-center mr-2 font-medium line-clamp-1">
+							{$i18n.t('Import Presets')}
+						</div>
+
+						<div class=" self-center">
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 16 16"
+								fill="currentColor"
+								class="w-3.5 h-3.5"
+							>
+								<path
+									fill-rule="evenodd"
+									d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
+									clip-rule="evenodd"
+								/>
+							</svg>
+						</div>
+					</button>
+
+					<button
+						class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+						on:click={async () => {
+							downloadModels(models);
+						}}
+					>
+						<div class=" self-center mr-2 font-medium line-clamp-1">
+							{$i18n.t('Export Presets')}
+						</div>
+
+						<div class=" self-center">
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 16 16"
+								fill="currentColor"
+								class="w-3.5 h-3.5"
+							>
+								<path
+									fill-rule="evenodd"
+									d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
+									clip-rule="evenodd"
+								/>
+							</svg>
+						</div>
+					</button>
 				</div>
 				</div>
 			</div>
 			</div>
 		{/if}
 		{/if}
+	{:else}
+		<ModelEditor
+			edit
+			model={models.find((m) => m.id === selectedModelId)}
+			preset={false}
+			onSubmit={(model) => {
+				console.log(model);
+				upsertModelHandler(model);
+				selectedModelId = null;
+			}}
+			onBack={() => {
+				selectedModelId = null;
+			}}
+		/>
+	{/if}
+{:else}
+	<div class=" h-full w-full flex justify-center items-center">
+		<Spinner />
 	</div>
 	</div>
-</div>
+{/if}

+ 0 - 214
src/lib/components/admin/Settings/Users.svelte

@@ -1,214 +0,0 @@
-<script lang="ts">
-	import { getBackendConfig, getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
-	import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
-	import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
-
-	import { onMount, getContext } from 'svelte';
-	import { models, config } from '$lib/stores';
-	import Switch from '$lib/components/common/Switch.svelte';
-	import { setDefaultModels } from '$lib/apis/configs';
-
-	const i18n = getContext('i18n');
-
-	export let saveHandler: Function;
-
-	let defaultModelId = '';
-
-	let whitelistEnabled = false;
-	let whitelistModels = [''];
-	let permissions = {
-		chat: {
-			deletion: true,
-			edit: true,
-			temporary: true
-		}
-	};
-
-	let chatDeletion = true;
-	let chatEdit = true;
-	let chatTemporary = true;
-
-	onMount(async () => {
-		permissions = await getUserPermissions(localStorage.token);
-
-		chatDeletion = permissions?.chat?.deletion ?? true;
-		chatEdit = permissions?.chat?.editing ?? true;
-		chatTemporary = permissions?.chat?.temporary ?? true;
-
-		const res = await getModelFilterConfig(localStorage.token);
-		if (res) {
-			whitelistEnabled = res.enabled;
-			whitelistModels = res.models.length > 0 ? res.models : [''];
-		}
-
-		defaultModelId = $config.default_models ? $config?.default_models.split(',')[0] : '';
-	});
-</script>
-
-<form
-	class="flex flex-col h-full justify-between space-y-3 text-sm"
-	on:submit|preventDefault={async () => {
-		// console.log('submit');
-
-		await setDefaultModels(localStorage.token, defaultModelId);
-		await updateUserPermissions(localStorage.token, {
-			chat: {
-				deletion: chatDeletion,
-				editing: chatEdit,
-				temporary: chatTemporary
-			}
-		});
-		await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
-		saveHandler();
-
-		await config.set(await getBackendConfig());
-	}}
->
-	<div class=" space-y-3 overflow-y-scroll max-h-full pr-1.5">
-		<div>
-			<div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
-
-			<div class="  flex w-full justify-between my-2 pr-2">
-				<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div>
-
-				<Switch bind:state={chatDeletion} />
-			</div>
-
-			<div class="  flex w-full justify-between my-2 pr-2">
-				<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Editing')}</div>
-
-				<Switch bind:state={chatEdit} />
-			</div>
-
-			<div class="  flex w-full justify-between my-2 pr-2">
-				<div class=" self-center text-xs font-medium">{$i18n.t('Allow Temporary Chat')}</div>
-
-				<Switch bind:state={chatTemporary} />
-			</div>
-		</div>
-
-		<!-- <hr class=" border-gray-50 dark:border-gray-850 my-2" />
-
-		<div class="mt-2 space-y-3">
-			<div>
-				<div class="mb-2">
-					<div class="flex justify-between items-center text-xs">
-						<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
-					</div>
-				</div>
-				<div class=" space-y-1 mb-3">
-					<div class="mb-2">
-						<div class="flex justify-between items-center text-xs">
-							<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
-						</div>
-					</div>
-
-					<div class="flex-1 mr-2">
-						<select
-							class="w-full bg-transparent outline-none py-0.5"
-							bind:value={defaultModelId}
-							placeholder="Select a model"
-						>
-							<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-							{#each $models.filter((model) => model.id) as model}
-								<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
-							{/each}
-						</select>
-					</div>
-				</div>
-
-				<div class=" space-y-1">
-					<div class="mb-2">
-						<div class="flex justify-between items-center text-xs my-3 pr-2">
-							<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
-
-							<Switch bind:state={whitelistEnabled} />
-						</div>
-					</div>
-
-					{#if whitelistEnabled}
-						<div>
-							<div class=" space-y-1.5">
-								{#each whitelistModels as modelId, modelIdx}
-									<div class="flex w-full">
-										<div class="flex-1 mr-2">
-											<select
-												class="w-full bg-transparent outline-none py-0.5"
-												bind:value={modelId}
-												placeholder="Select a model"
-											>
-												<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-												{#each $models.filter((model) => model.id) as model}
-													<option value={model.id} class="bg-gray-100 dark:bg-gray-700"
-														>{model.name}</option
-													>
-												{/each}
-											</select>
-										</div>
-
-										{#if modelIdx === 0}
-											<button
-												class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
-												type="button"
-												on:click={() => {
-													if (whitelistModels.at(-1) !== '') {
-														whitelistModels = [...whitelistModels, ''];
-													}
-												}}
-											>
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													viewBox="0 0 16 16"
-													fill="currentColor"
-													class="w-4 h-4"
-												>
-													<path
-														d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
-													/>
-												</svg>
-											</button>
-										{:else}
-											<button
-												class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
-												type="button"
-												on:click={() => {
-													whitelistModels.splice(modelIdx, 1);
-													whitelistModels = whitelistModels;
-												}}
-											>
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													viewBox="0 0 16 16"
-													fill="currentColor"
-													class="w-4 h-4"
-												>
-													<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
-												</svg>
-											</button>
-										{/if}
-									</div>
-								{/each}
-							</div>
-
-							<div class="flex justify-end items-center text-xs mt-1.5 text-right">
-								<div class=" text-xs font-medium">
-									{whitelistModels.length}
-									{$i18n.t('Model(s) Whitelisted')}
-								</div>
-							</div>
-						</div>
-					{/if}
-				</div>
-			</div>
-		</div> -->
-	</div>
-
-	<div class="flex justify-end pt-3 text-sm font-medium">
-		<button
-			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"
-			type="submit"
-		>
-			{$i18n.t('Save')}
-		</button>
-	</div>
-</form>

+ 28 - 4
src/lib/components/admin/Users.svelte

@@ -1,14 +1,38 @@
 <script>
 <script>
 	import { getContext, tick, onMount } from 'svelte';
 	import { getContext, tick, onMount } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
+
+	import { goto } from '$app/navigation';
+	import { user } from '$lib/stores';
+
+	import { getUsers } from '$lib/apis/users';
+
 	import UserList from './Users/UserList.svelte';
 	import UserList from './Users/UserList.svelte';
 	import Groups from './Users/Groups.svelte';
 	import Groups from './Users/Groups.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
+	let users = [];
+
 	let selectedTab = 'overview';
 	let selectedTab = 'overview';
+	let loaded = false;
+
+	$: if (selectedTab) {
+		getUsersHandler();
+	}
+
+	const getUsersHandler = async () => {
+		users = await getUsers(localStorage.token);
+	};
+
+	onMount(async () => {
+		if ($user?.role !== 'admin') {
+			await goto('/');
+		} else {
+			users = await getUsers(localStorage.token);
+		}
+		loaded = true;
 
 
-	onMount(() => {
 		const containerElement = document.getElementById('users-tabs-container');
 		const containerElement = document.getElementById('users-tabs-container');
 
 
 		if (containerElement) {
 		if (containerElement) {
@@ -25,7 +49,7 @@
 <div class="flex flex-col lg:flex-row w-full h-full -mt-0.5 pb-2 lg:space-x-4">
 <div class="flex flex-col lg:flex-row w-full h-full -mt-0.5 pb-2 lg:space-x-4">
 	<div
 	<div
 		id="users-tabs-container"
 		id="users-tabs-container"
-		class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
+		class=" flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
 	>
 	>
 		<button
 		<button
 			class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===
 			class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===
@@ -78,9 +102,9 @@
 
 
 	<div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll">
 	<div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll">
 		{#if selectedTab === 'overview'}
 		{#if selectedTab === 'overview'}
-			<UserList />
+			<UserList {users} />
 		{:else if selectedTab === 'groups'}
 		{:else if selectedTab === 'groups'}
-			<Groups />
+			<Groups {users} />
 		{/if}
 		{/if}
 	</div>
 	</div>
 </div>
 </div>

+ 117 - 3
src/lib/components/admin/Users/Groups.svelte

@@ -7,16 +7,30 @@
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 
 
-	import { WEBUI_NAME, config, user, showSidebar } from '$lib/stores';
+	import { WEBUI_NAME, config, user, showSidebar, knowledge } from '$lib/stores';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
 
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
+	import Badge from '$lib/components/common/Badge.svelte';
+	import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
+	import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
+	import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
+	import User from '$lib/components/icons/User.svelte';
+	import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
+	import GroupModal from './Groups/EditGroupModal.svelte';
+	import Pencil from '$lib/components/icons/Pencil.svelte';
+	import GroupItem from './Groups/GroupItem.svelte';
+	import AddGroupModal from './Groups/AddGroupModal.svelte';
+	import { createNewGroup, getGroups } from '$lib/apis/groups';
+	import { getUserDefaultPermissions, updateUserDefaultPermissions } from '$lib/apis/users';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	let loaded = false;
 	let loaded = false;
 
 
+	export let users = [];
+
 	let groups = [];
 	let groups = [];
 	let filteredGroups;
 	let filteredGroups;
 
 
@@ -31,20 +45,69 @@
 	});
 	});
 
 
 	let search = '';
 	let search = '';
+	let defaultPermissions = {
+		workspace: {
+			models: false,
+			knowledge: false,
+			prompts: false,
+			tools: false
+		},
+		chat: {
+			file_upload: true,
+			delete: true,
+			edit: true,
+			temporary: true
+		}
+	};
 
 
 	let showCreateGroupModal = false;
 	let showCreateGroupModal = false;
+	let showDefaultPermissionsModal = false;
+
+	const setGroups = async () => {
+		groups = await getGroups(localStorage.token);
+	};
+
+	const addGroupHandler = async (group) => {
+		const res = await createNewGroup(localStorage.token, group).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			toast.success($i18n.t('Group created successfully'));
+			groups = await getGroups(localStorage.token);
+		}
+	};
+
+	const updateDefaultPermissionsHandler = async (group) => {
+		console.log(group.permissions);
+
+		const res = await updateUserDefaultPermissions(localStorage.token, group.permissions).catch(
+			(error) => {
+				toast.error(error);
+				return null;
+			}
+		);
+
+		if (res) {
+			toast.success($i18n.t('Default permissions updated successfully'));
+			defaultPermissions = await getUserDefaultPermissions(localStorage.token);
+		}
+	};
 
 
 	onMount(async () => {
 	onMount(async () => {
 		if ($user?.role !== 'admin') {
 		if ($user?.role !== 'admin') {
 			await goto('/');
 			await goto('/');
 		} else {
 		} else {
-			groups = [];
+			await setGroups();
+			defaultPermissions = await getUserDefaultPermissions(localStorage.token);
 		}
 		}
 		loaded = true;
 		loaded = true;
 	});
 	});
 </script>
 </script>
 
 
 {#if loaded}
 {#if loaded}
+	<AddGroupModal bind:show={showCreateGroupModal} onSubmit={addGroupHandler} />
 	<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
 	<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
 		<div class="flex md:self-center text-lg font-medium px-0.5">
 		<div class="flex md:self-center text-lg font-medium px-0.5">
 			{$i18n.t('Groups')}
 			{$i18n.t('Groups')}
@@ -117,7 +180,58 @@
 				</div>
 				</div>
 			</div>
 			</div>
 		{:else}
 		{:else}
-			<div></div>
+			<div>
+				<div class=" flex items-center gap-3 justify-between text-xs uppercase px-1 font-bold">
+					<div class="w-full">Group</div>
+
+					<div class="w-full">Users</div>
+
+					<div class="w-full"></div>
+				</div>
+
+				<hr class="mt-1.5 border-gray-50 dark:border-gray-850" />
+
+				{#each filteredGroups as group}
+					<div class="my-2">
+						<GroupItem {group} {users} {setGroups} />
+					</div>
+				{/each}
+			</div>
 		{/if}
 		{/if}
+
+		<hr class="mb-2 border-gray-50 dark:border-gray-850" />
+
+		<GroupModal
+			bind:show={showDefaultPermissionsModal}
+			tabs={['permissions']}
+			bind:permissions={defaultPermissions}
+			custom={false}
+			onSubmit={updateDefaultPermissionsHandler}
+		/>
+
+		<button
+			class="flex items-center justify-between rounded-lg w-full transition pt-1"
+			on:click={() => {
+				showDefaultPermissionsModal = true;
+			}}
+		>
+			<div class="flex items-center gap-2.5">
+				<div class="p-1.5 bg-black/5 dark:bg-white/10 rounded-full">
+					<UsersSolid className="size-4" />
+				</div>
+
+				<div class="text-left">
+					<div class=" text-sm font-medium">{$i18n.t('Default permissions')}</div>
+
+					<div class="flex text-xs mt-0.5">
+						{$i18n.t('applies to all users with the "user" role')}
+					</div>
+				</div>
+			</div>
+
+			<div>
+				<ChevronRight strokeWidth="2.5" />
+			</div>
+		</button>
 	</div>
 	</div>
 {/if}
 {/if}

+ 149 - 0
src/lib/components/admin/Users/Groups/AddGroupModal.svelte

@@ -0,0 +1,149 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import { getContext, onMount } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import Modal from '$lib/components/common/Modal.svelte';
+	import Textarea from '$lib/components/common/Textarea.svelte';
+	export let onSubmit: Function = () => {};
+	export let show = false;
+
+	let name = '';
+	let description = '';
+	let userIds = [];
+
+	let loading = false;
+
+	const submitHandler = async () => {
+		loading = true;
+
+		const group = {
+			name,
+			description
+		};
+
+		await onSubmit(group);
+
+		loading = false;
+		show = false;
+
+		name = '';
+		description = '';
+		userIds = [];
+	};
+
+	onMount(() => {
+		console.log('mounted');
+	});
+</script>
+
+<Modal size="sm" bind:show>
+	<div>
+		<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
+			<div class=" text-lg font-medium self-center font-primary">
+				{$i18n.t('Add User Group')}
+			</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
+			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
+				<form
+					class="flex flex-col w-full"
+					on:submit={(e) => {
+						e.preventDefault();
+						submitHandler();
+					}}
+				>
+					<div class="px-1 flex flex-col w-full">
+						<div class="flex gap-2">
+							<div class="flex flex-col w-full">
+								<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Name')}</div>
+
+								<div class="flex-1">
+									<input
+										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+										type="text"
+										bind:value={name}
+										placeholder={$i18n.t('Group Name')}
+										autocomplete="off"
+										required
+									/>
+								</div>
+							</div>
+						</div>
+
+						<div class="flex flex-col w-full mt-2">
+							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Description')}</div>
+
+							<div class="flex-1">
+								<Textarea
+									className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
+									rows={2}
+									bind:value={description}
+									placeholder={$i18n.t('Group Description')}
+								/>
+							</div>
+						</div>
+					</div>
+
+					<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
+						<button
+							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 flex flex-row space-x-1 items-center {loading
+								? ' cursor-not-allowed'
+								: ''}"
+							type="submit"
+							disabled={loading}
+						>
+							{$i18n.t('Create')}
+
+							{#if loading}
+								<div class="ml-2 self-center">
+									<svg
+										class=" w-4 h-4"
+										viewBox="0 0 24 24"
+										fill="currentColor"
+										xmlns="http://www.w3.org/2000/svg"
+										><style>
+											.spinner_ajPY {
+												transform-origin: center;
+												animation: spinner_AtaB 0.75s infinite linear;
+											}
+											@keyframes spinner_AtaB {
+												100% {
+													transform: rotate(360deg);
+												}
+											}
+										</style><path
+											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+											opacity=".25"
+										/><path
+											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+											class="spinner_ajPY"
+										/></svg
+									>
+								</div>
+							{/if}
+						</button>
+					</div>
+				</form>
+			</div>
+		</div>
+	</div>
+</Modal>

+ 61 - 0
src/lib/components/admin/Users/Groups/Display.svelte

@@ -0,0 +1,61 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	import Textarea from '$lib/components/common/Textarea.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let name = '';
+	export let color = '';
+	export let description = '';
+</script>
+
+<div class="flex gap-2">
+	<div class="flex flex-col w-full">
+		<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Name')}</div>
+
+		<div class="flex-1">
+			<input
+				class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+				type="text"
+				bind:value={name}
+				placeholder={$i18n.t('Group Name')}
+				autocomplete="off"
+				required
+			/>
+		</div>
+	</div>
+</div>
+
+<!-- <div class="flex flex-col w-full mt-2">
+	<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Color')}</div>
+
+	<div class="flex-1">
+		<Tooltip content={$i18n.t('Hex Color - Leave empty for default color')} placement="top-start">
+			<div class="flex gap-0.5">
+				<div class="text-gray-500">#</div>
+
+				<input
+					class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+					type="text"
+					bind:value={color}
+					placeholder={$i18n.t('Hex Color')}
+					autocomplete="off"
+				/>
+			</div>
+		</Tooltip>
+	</div>
+</div> -->
+
+<div class="flex flex-col w-full mt-2">
+	<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Description')}</div>
+
+	<div class="flex-1">
+		<Textarea
+			className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
+			rows={4}
+			bind:value={description}
+			placeholder={$i18n.t('Group Description')}
+		/>
+	</div>
+</div>

+ 328 - 0
src/lib/components/admin/Users/Groups/EditGroupModal.svelte

@@ -0,0 +1,328 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import { getContext, onMount } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import Modal from '$lib/components/common/Modal.svelte';
+	import Display from './Display.svelte';
+	import Permissions from './Permissions.svelte';
+	import Users from './Users.svelte';
+	import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
+	import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
+
+	export let onSubmit: Function = () => {};
+	export let onDelete: Function = () => {};
+
+	export let show = false;
+	export let edit = false;
+
+	export let users = [];
+	export let group = null;
+
+	export let custom = true;
+
+	export let tabs = ['general', 'permissions', 'users'];
+
+	let selectedTab = 'general';
+	let loading = false;
+
+	export let name = '';
+	export let description = '';
+
+	export let permissions = {
+		workspace: {
+			models: false,
+			knowledge: false,
+			prompts: false,
+			tools: false
+		},
+		chat: {
+			file_upload: true,
+			delete: true,
+			edit: true,
+			temporary: true
+		}
+	};
+	export let userIds = [];
+
+	const submitHandler = async () => {
+		loading = true;
+
+		const group = {
+			name,
+			description,
+			permissions,
+			user_ids: userIds
+		};
+
+		await onSubmit(group);
+
+		loading = false;
+		show = false;
+	};
+
+	const init = () => {
+		if (group) {
+			name = group.name;
+			description = group.description;
+			permissions = group?.permissions ?? {
+				workspace: {
+					models: false,
+					knowledge: false,
+					prompts: false,
+					tools: false
+				},
+				chat: {
+					file_upload: true,
+					delete: true,
+					edit: true,
+					temporary: true
+				}
+			};
+			userIds = group?.user_ids ?? [];
+		}
+	};
+
+	$: if (show) {
+		init();
+	}
+
+	onMount(() => {
+		console.log(tabs);
+		selectedTab = tabs[0];
+		init();
+	});
+</script>
+
+<Modal size="md" bind:show>
+	<div>
+		<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
+			<div class=" text-lg font-medium self-center font-primary">
+				{#if custom}
+					{#if edit}
+						{$i18n.t('Edit User Group')}
+					{:else}
+						{$i18n.t('Add User Group')}
+					{/if}
+				{:else}
+					{$i18n.t('Edit Default Permissions')}
+				{/if}
+			</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
+			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
+				<form
+					class="flex flex-col w-full"
+					on:submit={(e) => {
+						e.preventDefault();
+						submitHandler();
+					}}
+				>
+					<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
+						<div
+							id="admin-settings-tabs-container"
+							class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
+						>
+							{#if tabs.includes('general')}
+								<button
+									class="px-0.5 py-1 max-w-fit w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
+									'general'
+										? ''
+										: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+									on:click={() => {
+										selectedTab = 'general';
+									}}
+									type="button"
+								>
+									<div class=" self-center mr-2">
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 16 16"
+											fill="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												fill-rule="evenodd"
+												d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
+												clip-rule="evenodd"
+											/>
+										</svg>
+									</div>
+									<div class=" self-center">{$i18n.t('General')}</div>
+								</button>
+							{/if}
+
+							{#if tabs.includes('permissions')}
+								<button
+									class="px-0.5 py-1 max-w-fit w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
+									'permissions'
+										? ''
+										: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+									on:click={() => {
+										selectedTab = 'permissions';
+									}}
+									type="button"
+								>
+									<div class=" self-center mr-2">
+										<WrenchSolid />
+									</div>
+									<div class=" self-center">{$i18n.t('Permissions')}</div>
+								</button>
+							{/if}
+
+							{#if tabs.includes('users')}
+								<button
+									class="px-0.5 py-1 max-w-fit w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
+									'users'
+										? ''
+										: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+									on:click={() => {
+										selectedTab = 'users';
+									}}
+									type="button"
+								>
+									<div class=" self-center mr-2">
+										<UserPlusSolid />
+									</div>
+									<div class=" self-center">{$i18n.t('Users')} ({userIds.length})</div>
+								</button>
+							{/if}
+						</div>
+
+						<div
+							class="flex-1 mt-1 lg:mt-1 lg:h-[22rem] lg:max-h-[22rem] overflow-y-auto scrollbar-hidden"
+						>
+							{#if selectedTab == 'general'}
+								<Display bind:name bind:description />
+							{:else if selectedTab == 'permissions'}
+								<Permissions bind:permissions />
+							{:else if selectedTab == 'users'}
+								<Users bind:userIds {users} />
+							{/if}
+						</div>
+					</div>
+
+					<!-- <div
+						class=" tabs flex flex-row overflow-x-auto gap-2.5 text-sm font-medium border-b border-b-gray-800 scrollbar-hidden"
+					>
+						{#if tabs.includes('display')}
+							<button
+								class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
+								'display'
+									? ' dark:border-white'
+									: 'border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+								on:click={() => {
+									selectedTab = 'display';
+								}}
+								type="button"
+							>
+								{$i18n.t('Display')}
+							</button>
+						{/if}
+
+						{#if tabs.includes('permissions')}
+							<button
+								class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
+								'permissions'
+									? '  dark:border-white'
+									: 'border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+								on:click={() => {
+									selectedTab = 'permissions';
+								}}
+								type="button"
+							>
+								{$i18n.t('Permissions')}
+							</button>
+						{/if}
+
+						{#if tabs.includes('users')}
+							<button
+								class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
+								'users'
+									? ' dark:border-white'
+									: ' border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+								on:click={() => {
+									selectedTab = 'users';
+								}}
+								type="button"
+							>
+								{$i18n.t('Users')} ({userIds.length})
+							</button>
+						{/if}
+					</div> -->
+
+					<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
+						{#if edit}
+							<button
+								class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
+								type="button"
+								on:click={() => {
+									onDelete();
+									show = false;
+								}}
+							>
+								{$i18n.t('Delete')}
+							</button>
+						{/if}
+
+						<button
+							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 flex flex-row space-x-1 items-center {loading
+								? ' cursor-not-allowed'
+								: ''}"
+							type="submit"
+							disabled={loading}
+						>
+							{$i18n.t('Save')}
+
+							{#if loading}
+								<div class="ml-2 self-center">
+									<svg
+										class=" w-4 h-4"
+										viewBox="0 0 24 24"
+										fill="currentColor"
+										xmlns="http://www.w3.org/2000/svg"
+										><style>
+											.spinner_ajPY {
+												transform-origin: center;
+												animation: spinner_AtaB 0.75s infinite linear;
+											}
+											@keyframes spinner_AtaB {
+												100% {
+													transform: rotate(360deg);
+												}
+											}
+										</style><path
+											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+											opacity=".25"
+										/><path
+											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+											class="spinner_ajPY"
+										/></svg
+									>
+								</div>
+							{/if}
+						</button>
+					</div>
+				</form>
+			</div>
+		</div>
+	</div>
+</Modal>

+ 84 - 0
src/lib/components/admin/Users/Groups/GroupItem.svelte

@@ -0,0 +1,84 @@
+<script>
+	import { toast } from 'svelte-sonner';
+	import { getContext } from 'svelte';
+
+	const i18n = getContext('i18n');
+
+	import { deleteGroupById, updateGroupById } from '$lib/apis/groups';
+
+	import Pencil from '$lib/components/icons/Pencil.svelte';
+	import User from '$lib/components/icons/User.svelte';
+	import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
+	import GroupModal from './EditGroupModal.svelte';
+
+	export let users = [];
+	export let group = {
+		name: 'Admins',
+		user_ids: [1, 2, 3]
+	};
+
+	export let setGroups = () => {};
+
+	let showEdit = false;
+
+	const updateHandler = async (_group) => {
+		const res = await updateGroupById(localStorage.token, group.id, _group).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			toast.success($i18n.t('Group updated successfully'));
+			setGroups();
+		}
+	};
+
+	const deleteHandler = async () => {
+		const res = await deleteGroupById(localStorage.token, group.id).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			toast.success($i18n.t('Group deleted successfully'));
+			setGroups();
+		}
+	};
+</script>
+
+<GroupModal
+	bind:show={showEdit}
+	edit
+	{users}
+	{group}
+	onSubmit={updateHandler}
+	onDelete={deleteHandler}
+/>
+
+<div class="flex items-center gap-3 justify-between px-1 text-xs w-full transition">
+	<div class="flex items-center gap-1.5 w-full font-medium">
+		<div>
+			<UserCircleSolid className="size-4" />
+		</div>
+		{group.name}
+	</div>
+
+	<div class="flex items-center gap-1.5 w-full font-medium">
+		{group.user_ids.length}
+
+		<div>
+			<User className="size-3.5" />
+		</div>
+	</div>
+
+	<div class="w-full flex justify-end">
+		<button
+			class=" rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+			on:click={() => {
+				showEdit = true;
+			}}
+		>
+			<Pencil className="size-3.5" />
+		</button>
+	</div>
+</div>

+ 204 - 0
src/lib/components/admin/Users/Groups/Permissions.svelte

@@ -0,0 +1,204 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import Switch from '$lib/components/common/Switch.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+
+	export let permissions = {
+		workspace: {
+			models: false,
+			knowledge: false,
+			prompts: false,
+			tools: false
+		},
+		chat: {
+			delete: true,
+			edit: true,
+			temporary: true,
+			file_upload: true
+		}
+	};
+</script>
+
+<div>
+	<!-- <div>
+		<div class=" mb-2 text-sm font-medium">{$i18n.t('Model Permissions')}</div>
+
+		<div class="mb-2">
+			<div class="flex justify-between items-center text-xs pr-2">
+				<div class=" text-xs font-medium">{$i18n.t('Model Filtering')}</div>
+
+				<Switch bind:state={permissions.model.filter} />
+			</div>
+		</div>
+
+		{#if permissions.model.filter}
+			<div class="mb-2">
+				<div class=" space-y-1.5">
+					<div class="flex flex-col w-full">
+						<div class="mb-1 flex justify-between">
+							<div class="text-xs text-gray-500">{$i18n.t('Model IDs')}</div>
+						</div>
+
+						{#if model_ids.length > 0}
+							<div class="flex flex-col">
+								{#each model_ids as modelId, modelIdx}
+									<div class=" flex gap-2 w-full justify-between items-center">
+										<div class=" text-sm flex-1 rounded-lg">
+											{modelId}
+										</div>
+										<div class="flex-shrink-0">
+											<button
+												type="button"
+												on:click={() => {
+													model_ids = model_ids.filter((_, idx) => idx !== modelIdx);
+												}}
+											>
+												<Minus strokeWidth="2" className="size-3.5" />
+											</button>
+										</div>
+									</div>
+								{/each}
+							</div>
+						{:else}
+							<div class="text-gray-500 text-xs text-center py-2 px-10">
+								{$i18n.t('No model IDs')}
+							</div>
+						{/if}
+					</div>
+				</div>
+				<hr class=" border-gray-100 dark:border-gray-700/10 mt-2.5 mb-1 w-full" />
+
+				<div class="flex items-center">
+					<select
+						class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
+							? ''
+							: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+						bind:value={selectedModelId}
+					>
+						<option value="">{$i18n.t('Select a model')}</option>
+						{#each $models.filter((m) => m?.owned_by !== 'arena') as model}
+							<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
+						{/each}
+					</select>
+
+					<div>
+						<button
+							type="button"
+							on:click={() => {
+								if (selectedModelId && !permissions.model.model_ids.includes(selectedModelId)) {
+									permissions.model.model_ids = [...permissions.model.model_ids, selectedModelId];
+									selectedModelId = '';
+								}
+							}}
+						>
+							<Plus className="size-3.5" strokeWidth="2" />
+						</button>
+					</div>
+				</div>
+			</div>
+		{/if}
+
+		<div class=" space-y-1 mb-3">
+			<div class="">
+				<div class="flex justify-between items-center text-xs">
+					<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
+				</div>
+			</div>
+
+			<div class="flex-1 mr-2">
+				<select
+					class="w-full bg-transparent outline-none py-0.5 text-sm"
+					bind:value={permissions.model.default_id}
+					placeholder="Select a model"
+				>
+					<option value="" disabled selected>{$i18n.t('Select a model')}</option>
+					{#each permissions.model.filter ? $models.filter( (model) => filterModelIds.includes(model.id) ) : $models.filter((model) => model.id) as model}
+						<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
+					{/each}
+				</select>
+			</div>
+		</div>
+	</div>
+
+	<hr class=" border-gray-50 dark:border-gray-850 my-2" /> -->
+
+	<div>
+		<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Models Access')}
+			</div>
+			<Switch bind:state={permissions.workspace.models} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Knowledge Access')}
+			</div>
+			<Switch bind:state={permissions.workspace.knowledge} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Prompts Access')}
+			</div>
+			<Switch bind:state={permissions.workspace.prompts} />
+		</div>
+
+		<div class=" ">
+			<Tooltip
+				className=" flex w-full justify-between my-2 pr-2"
+				content={$i18n.t(
+					'Warning: Enabling this will allow users to upload arbitrary code on the server.'
+				)}
+				placement="top-start"
+			>
+				<div class=" self-center text-xs font-medium">
+					{$i18n.t('Tools Access')}
+				</div>
+				<Switch bind:state={permissions.workspace.tools} />
+			</Tooltip>
+		</div>
+	</div>
+
+	<hr class=" border-gray-50 dark:border-gray-850 my-2" />
+
+	<div>
+		<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Allow File Upload')}
+			</div>
+
+			<Switch bind:state={permissions.chat.file_upload} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Allow Chat Delete')}
+			</div>
+
+			<Switch bind:state={permissions.chat.delete} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Allow Chat Edit')}
+			</div>
+
+			<Switch bind:state={permissions.chat.edit} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Allow Temporary Chat')}
+			</div>
+
+			<Switch bind:state={permissions.chat.temporary} />
+		</div>
+	</div>
+</div>

+ 122 - 0
src/lib/components/admin/Users/Groups/Users.svelte

@@ -0,0 +1,122 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Plus from '$lib/components/icons/Plus.svelte';
+	import { WEBUI_BASE_URL } from '$lib/constants';
+	import Checkbox from '$lib/components/common/Checkbox.svelte';
+	import Badge from '$lib/components/common/Badge.svelte';
+
+	export let users = [];
+	export let userIds = [];
+
+	let filteredUsers = [];
+
+	$: filteredUsers = users
+		.filter((user) => {
+			if (user?.role === 'admin') {
+				return false;
+			}
+
+			if (query === '') {
+				return true;
+			}
+
+			return (
+				user.name.toLowerCase().includes(query.toLowerCase()) ||
+				user.email.toLowerCase().includes(query.toLowerCase())
+			);
+		})
+		.sort((a, b) => {
+			const aUserIndex = userIds.indexOf(a.id);
+			const bUserIndex = userIds.indexOf(b.id);
+
+			// Compare based on userIds or fall back to alphabetical order
+			if (aUserIndex !== -1 && bUserIndex === -1) return -1; // 'a' has valid userId -> prioritize
+			if (bUserIndex !== -1 && aUserIndex === -1) return 1; // 'b' has valid userId -> prioritize
+
+			// Both a and b are either in the userIds array or not, so we'll sort them by their indices
+			if (aUserIndex !== -1 && bUserIndex !== -1) return aUserIndex - bUserIndex;
+
+			// If both are not in the userIds, fallback to alphabetical sorting by name
+			return a.name.localeCompare(b.name);
+		});
+
+	let query = '';
+</script>
+
+<div>
+	<div class="flex w-full">
+		<div class="flex flex-1">
+			<div class=" self-center mr-3">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+			<input
+				class=" w-full text-sm pr-4 rounded-r-xl outline-none bg-transparent"
+				bind:value={query}
+				placeholder={$i18n.t('Search')}
+			/>
+		</div>
+	</div>
+
+	<div class="mt-3 max-h-[22rem] overflow-y-auto scrollbar-hidden">
+		<div class="flex flex-col gap-2.5">
+			{#if filteredUsers.length > 0}
+				{#each filteredUsers as user, userIdx (user.id)}
+					<div class="flex flex-row items-center gap-3 w-full text-sm">
+						<div class="flex items-center">
+							<Checkbox
+								state={userIds.includes(user.id) ? 'checked' : 'unchecked'}
+								on:change={(e) => {
+									if (e.detail === 'checked') {
+										userIds = [...userIds, user.id];
+									} else {
+										userIds = userIds.filter((id) => id !== user.id);
+									}
+								}}
+							/>
+						</div>
+
+						<div class="flex w-full items-center justify-between">
+							<Tooltip content={user.email} placement="top-start">
+								<div class="flex">
+									<img
+										class=" rounded-full size-5 object-cover mr-2.5"
+										src={user.profile_image_url.startsWith(WEBUI_BASE_URL) ||
+										user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
+										user.profile_image_url.startsWith('data:')
+											? user.profile_image_url
+											: `/user.png`}
+										alt="user"
+									/>
+
+									<div class=" font-medium self-center">{user.name}</div>
+								</div>
+							</Tooltip>
+
+							{#if userIds.includes(user.id)}
+								<Badge type="success" content="member" />
+							{/if}
+						</div>
+					</div>
+				{/each}
+			{:else}
+				<div class="text-gray-500 text-xs text-center py-2 px-10">
+					{$i18n.t('No users were found.')}
+				</div>
+			{/if}
+		</div>
+	</div>
+</div>

+ 287 - 301
src/lib/components/admin/Users/UserList.svelte

@@ -29,9 +29,7 @@
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
-	let loaded = false;
-	let tab = '';
-	let users = [];
+	export let users = [];
 
 
 	let search = '';
 	let search = '';
 	let selectedUser = null;
 	let selectedUser = null;
@@ -65,14 +63,6 @@
 		}
 		}
 	};
 	};
 
 
-	onMount(async () => {
-		if ($user?.role !== 'admin') {
-			await goto('/');
-		} else {
-			users = await getUsers(localStorage.token);
-		}
-		loaded = true;
-	});
 	let sortKey = 'created_at'; // default sort key
 	let sortKey = 'created_at'; // default sort key
 	let sortOrder = 'asc'; // default sort order
 	let sortOrder = 'asc'; // default sort order
 
 
@@ -131,278 +121,301 @@
 />
 />
 <UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
 <UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
 
 
-{#if loaded}
-	<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
-		<div class="flex md:self-center text-lg font-medium px-0.5">
-			{$i18n.t('Users')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
+<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
+	<div class="flex md:self-center text-lg font-medium px-0.5">
+		{$i18n.t('Users')}
+		<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
 
 
-			<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
-		</div>
+		<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
+	</div>
 
 
-		<div class="flex gap-1">
-			<div class=" flex w-full space-x-2">
-				<div class="flex flex-1">
-					<div class=" self-center ml-1 mr-3">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 20 20"
-							fill="currentColor"
-							class="w-4 h-4"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
-								clip-rule="evenodd"
-							/>
-						</svg>
-					</div>
-					<input
-						class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
-						bind:value={search}
-						placeholder={$i18n.t('Search')}
-					/>
+	<div class="flex gap-1">
+		<div class=" flex w-full space-x-2">
+			<div class="flex flex-1">
+				<div class=" self-center ml-1 mr-3">
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 20 20"
+						fill="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							fill-rule="evenodd"
+							d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
+							clip-rule="evenodd"
+						/>
+					</svg>
 				</div>
 				</div>
+				<input
+					class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+					bind:value={search}
+					placeholder={$i18n.t('Search')}
+				/>
+			</div>
 
 
-				<div>
-					<Tooltip content={$i18n.t('Add User')}>
-						<button
-							class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
-							on:click={() => {
-								showAddUserModal = !showAddUserModal;
-							}}
-						>
-							<Plus className="size-3.5" />
-						</button>
-					</Tooltip>
-				</div>
+			<div>
+				<Tooltip content={$i18n.t('Add User')}>
+					<button
+						class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
+						on:click={() => {
+							showAddUserModal = !showAddUserModal;
+						}}
+					>
+						<Plus className="size-3.5" />
+					</button>
+				</Tooltip>
 			</div>
 			</div>
 		</div>
 		</div>
 	</div>
 	</div>
+</div>
 
 
-	<div
-		class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5"
+<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
+	<table
+		class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
 	>
 	>
-		<table
-			class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
+		<thead
+			class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
 		>
 		>
-			<thead
-				class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
-			>
-				<tr class="">
-					<th
-						scope="col"
-						class="px-3 py-1.5 cursor-pointer select-none"
-						on:click={() => setSortKey('role')}
-					>
-						<div class="flex gap-1.5 items-center">
-							{$i18n.t('Role')}
-
-							{#if sortKey === 'role'}
-								<span class="font-normal"
-									>{#if sortOrder === 'asc'}
-										<ChevronUp className="size-2" />
-									{:else}
-										<ChevronDown className="size-2" />
-									{/if}
-								</span>
-							{:else}
-								<span class="invisible">
+			<tr class="">
+				<th
+					scope="col"
+					class="px-3 py-1.5 cursor-pointer select-none"
+					on:click={() => setSortKey('role')}
+				>
+					<div class="flex gap-1.5 items-center">
+						{$i18n.t('Role')}
+
+						{#if sortKey === 'role'}
+							<span class="font-normal"
+								>{#if sortOrder === 'asc'}
 									<ChevronUp className="size-2" />
 									<ChevronUp className="size-2" />
-								</span>
-							{/if}
-						</div>
-					</th>
-					<th
-						scope="col"
-						class="px-3 py-1.5 cursor-pointer select-none"
-						on:click={() => setSortKey('name')}
-					>
-						<div class="flex gap-1.5 items-center">
-							{$i18n.t('Name')}
-
-							{#if sortKey === 'name'}
-								<span class="font-normal"
-									>{#if sortOrder === 'asc'}
-										<ChevronUp className="size-2" />
-									{:else}
-										<ChevronDown className="size-2" />
-									{/if}
-								</span>
-							{:else}
-								<span class="invisible">
+								{:else}
+									<ChevronDown className="size-2" />
+								{/if}
+							</span>
+						{:else}
+							<span class="invisible">
+								<ChevronUp className="size-2" />
+							</span>
+						{/if}
+					</div>
+				</th>
+				<th
+					scope="col"
+					class="px-3 py-1.5 cursor-pointer select-none"
+					on:click={() => setSortKey('name')}
+				>
+					<div class="flex gap-1.5 items-center">
+						{$i18n.t('Name')}
+
+						{#if sortKey === 'name'}
+							<span class="font-normal"
+								>{#if sortOrder === 'asc'}
 									<ChevronUp className="size-2" />
 									<ChevronUp className="size-2" />
-								</span>
-							{/if}
-						</div>
-					</th>
-					<th
-						scope="col"
-						class="px-3 py-1.5 cursor-pointer select-none"
-						on:click={() => setSortKey('email')}
-					>
-						<div class="flex gap-1.5 items-center">
-							{$i18n.t('Email')}
-
-							{#if sortKey === 'email'}
-								<span class="font-normal"
-									>{#if sortOrder === 'asc'}
-										<ChevronUp className="size-2" />
-									{:else}
-										<ChevronDown className="size-2" />
-									{/if}
-								</span>
-							{:else}
-								<span class="invisible">
+								{:else}
+									<ChevronDown className="size-2" />
+								{/if}
+							</span>
+						{:else}
+							<span class="invisible">
+								<ChevronUp className="size-2" />
+							</span>
+						{/if}
+					</div>
+				</th>
+				<th
+					scope="col"
+					class="px-3 py-1.5 cursor-pointer select-none"
+					on:click={() => setSortKey('email')}
+				>
+					<div class="flex gap-1.5 items-center">
+						{$i18n.t('Email')}
+
+						{#if sortKey === 'email'}
+							<span class="font-normal"
+								>{#if sortOrder === 'asc'}
 									<ChevronUp className="size-2" />
 									<ChevronUp className="size-2" />
-								</span>
-							{/if}
-						</div>
-					</th>
-
-					<th
-						scope="col"
-						class="px-3 py-1.5 cursor-pointer select-none"
-						on:click={() => setSortKey('last_active_at')}
-					>
-						<div class="flex gap-1.5 items-center">
-							{$i18n.t('Last Active')}
-
-							{#if sortKey === 'last_active_at'}
-								<span class="font-normal"
-									>{#if sortOrder === 'asc'}
-										<ChevronUp className="size-2" />
-									{:else}
-										<ChevronDown className="size-2" />
-									{/if}
-								</span>
-							{:else}
-								<span class="invisible">
+								{:else}
+									<ChevronDown className="size-2" />
+								{/if}
+							</span>
+						{:else}
+							<span class="invisible">
+								<ChevronUp className="size-2" />
+							</span>
+						{/if}
+					</div>
+				</th>
+
+				<th
+					scope="col"
+					class="px-3 py-1.5 cursor-pointer select-none"
+					on:click={() => setSortKey('last_active_at')}
+				>
+					<div class="flex gap-1.5 items-center">
+						{$i18n.t('Last Active')}
+
+						{#if sortKey === 'last_active_at'}
+							<span class="font-normal"
+								>{#if sortOrder === 'asc'}
 									<ChevronUp className="size-2" />
 									<ChevronUp className="size-2" />
-								</span>
-							{/if}
-						</div>
-					</th>
-					<th
-						scope="col"
-						class="px-3 py-1.5 cursor-pointer select-none"
-						on:click={() => setSortKey('created_at')}
-					>
-						<div class="flex gap-1.5 items-center">
-							{$i18n.t('Created at')}
-							{#if sortKey === 'created_at'}
-								<span class="font-normal"
-									>{#if sortOrder === 'asc'}
-										<ChevronUp className="size-2" />
-									{:else}
-										<ChevronDown className="size-2" />
-									{/if}
-								</span>
-							{:else}
-								<span class="invisible">
+								{:else}
+									<ChevronDown className="size-2" />
+								{/if}
+							</span>
+						{:else}
+							<span class="invisible">
+								<ChevronUp className="size-2" />
+							</span>
+						{/if}
+					</div>
+				</th>
+				<th
+					scope="col"
+					class="px-3 py-1.5 cursor-pointer select-none"
+					on:click={() => setSortKey('created_at')}
+				>
+					<div class="flex gap-1.5 items-center">
+						{$i18n.t('Created at')}
+						{#if sortKey === 'created_at'}
+							<span class="font-normal"
+								>{#if sortOrder === 'asc'}
 									<ChevronUp className="size-2" />
 									<ChevronUp className="size-2" />
-								</span>
-							{/if}
-						</div>
-					</th>
-
-					<th
-						scope="col"
-						class="px-3 py-1.5 cursor-pointer select-none"
-						on:click={() => setSortKey('oauth_sub')}
-					>
-						<div class="flex gap-1.5 items-center">
-							{$i18n.t('OAuth ID')}
-
-							{#if sortKey === 'oauth_sub'}
-								<span class="font-normal"
-									>{#if sortOrder === 'asc'}
-										<ChevronUp className="size-2" />
-									{:else}
-										<ChevronDown className="size-2" />
-									{/if}
-								</span>
-							{:else}
-								<span class="invisible">
+								{:else}
+									<ChevronDown className="size-2" />
+								{/if}
+							</span>
+						{:else}
+							<span class="invisible">
+								<ChevronUp className="size-2" />
+							</span>
+						{/if}
+					</div>
+				</th>
+
+				<th
+					scope="col"
+					class="px-3 py-1.5 cursor-pointer select-none"
+					on:click={() => setSortKey('oauth_sub')}
+				>
+					<div class="flex gap-1.5 items-center">
+						{$i18n.t('OAuth ID')}
+
+						{#if sortKey === 'oauth_sub'}
+							<span class="font-normal"
+								>{#if sortOrder === 'asc'}
 									<ChevronUp className="size-2" />
 									<ChevronUp className="size-2" />
-								</span>
-							{/if}
+								{:else}
+									<ChevronDown className="size-2" />
+								{/if}
+							</span>
+						{:else}
+							<span class="invisible">
+								<ChevronUp className="size-2" />
+							</span>
+						{/if}
+					</div>
+				</th>
+
+				<th scope="col" class="px-3 py-2 text-right" />
+			</tr>
+		</thead>
+		<tbody class="">
+			{#each filteredUsers as user, userIdx}
+				<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
+					<td class="px-3 py-1 min-w-[7rem] w-28">
+						<button
+							class=" translate-y-0.5"
+							on:click={() => {
+								if (user.role === 'user') {
+									updateRoleHandler(user.id, 'admin');
+								} else if (user.role === 'pending') {
+									updateRoleHandler(user.id, 'user');
+								} else {
+									updateRoleHandler(user.id, 'pending');
+								}
+							}}
+						>
+							<Badge
+								type={user.role === 'admin' ? 'info' : user.role === 'user' ? 'success' : 'muted'}
+								content={$i18n.t(user.role)}
+							/>
+						</button>
+					</td>
+					<td class="px-3 py-1 font-medium text-gray-900 dark:text-white w-max">
+						<div class="flex flex-row w-max">
+							<img
+								class=" rounded-full w-6 h-6 object-cover mr-2.5"
+								src={user.profile_image_url.startsWith(WEBUI_BASE_URL) ||
+								user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
+								user.profile_image_url.startsWith('data:')
+									? user.profile_image_url
+									: `/user.png`}
+								alt="user"
+							/>
+
+							<div class=" font-medium self-center">{user.name}</div>
 						</div>
 						</div>
-					</th>
+					</td>
+					<td class=" px-3 py-1"> {user.email} </td>
 
 
-					<th scope="col" class="px-3 py-2 text-right" />
-				</tr>
-			</thead>
-			<tbody class="">
-				{#each filteredUsers as user, userIdx}
-					<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
-						<td class="px-3 py-1 min-w-[7rem] w-28">
-							<button
-								class=" translate-y-0.5"
-								on:click={() => {
-									if (user.role === 'user') {
-										updateRoleHandler(user.id, 'admin');
-									} else if (user.role === 'pending') {
-										updateRoleHandler(user.id, 'user');
-									} else {
-										updateRoleHandler(user.id, 'pending');
-									}
-								}}
-							>
-								<Badge
-									type={user.role === 'admin' ? 'info' : user.role === 'user' ? 'success' : 'muted'}
-									content={$i18n.t(user.role)}
-								/>
-							</button>
-						</td>
-						<td class="px-3 py-1 font-medium text-gray-900 dark:text-white w-max">
-							<div class="flex flex-row w-max">
-								<img
-									class=" rounded-full w-6 h-6 object-cover mr-2.5"
-									src={user.profile_image_url.startsWith(WEBUI_BASE_URL) ||
-									user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
-									user.profile_image_url.startsWith('data:')
-										? user.profile_image_url
-										: `/user.png`}
-									alt="user"
-								/>
-
-								<div class=" font-medium self-center">{user.name}</div>
-							</div>
-						</td>
-						<td class=" px-3 py-1"> {user.email} </td>
-
-						<td class=" px-3 py-1">
-							{dayjs(user.last_active_at * 1000).fromNow()}
-						</td>
-
-						<td class=" px-3 py-1">
-							{dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
-						</td>
-
-						<td class=" px-3 py-1"> {user.oauth_sub ?? ''} </td>
-
-						<td class="px-3 py-1 text-right">
-							<div class="flex justify-end w-full">
-								{#if $config.features.enable_admin_chat_access && user.role !== 'admin'}
-									<Tooltip content={$i18n.t('Chats')}>
-										<button
-											class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-											on:click={async () => {
-												showUserChatsModal = !showUserChatsModal;
-												selectedUser = user;
-											}}
-										>
-											<ChatBubbles />
-										</button>
-									</Tooltip>
-								{/if}
+					<td class=" px-3 py-1">
+						{dayjs(user.last_active_at * 1000).fromNow()}
+					</td>
+
+					<td class=" px-3 py-1">
+						{dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
+					</td>
 
 
-								<Tooltip content={$i18n.t('Edit User')}>
+					<td class=" px-3 py-1"> {user.oauth_sub ?? ''} </td>
+
+					<td class="px-3 py-1 text-right">
+						<div class="flex justify-end w-full">
+							{#if $config.features.enable_admin_chat_access && user.role !== 'admin'}
+								<Tooltip content={$i18n.t('Chats')}>
+									<button
+										class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+										on:click={async () => {
+											showUserChatsModal = !showUserChatsModal;
+											selectedUser = user;
+										}}
+									>
+										<ChatBubbles />
+									</button>
+								</Tooltip>
+							{/if}
+
+							<Tooltip content={$i18n.t('Edit User')}>
+								<button
+									class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+									on:click={async () => {
+										showEditUserModal = !showEditUserModal;
+										selectedUser = user;
+									}}
+								>
+									<svg
+										xmlns="http://www.w3.org/2000/svg"
+										fill="none"
+										viewBox="0 0 24 24"
+										stroke-width="1.5"
+										stroke="currentColor"
+										class="w-4 h-4"
+									>
+										<path
+											stroke-linecap="round"
+											stroke-linejoin="round"
+											d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
+										/>
+									</svg>
+								</button>
+							</Tooltip>
+
+							{#if user.role !== 'admin'}
+								<Tooltip content={$i18n.t('Delete User')}>
 									<button
 									<button
 										class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 										class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 										on:click={async () => {
 										on:click={async () => {
-											showEditUserModal = !showEditUserModal;
+											showDeleteConfirmDialog = true;
 											selectedUser = user;
 											selectedUser = user;
 										}}
 										}}
 									>
 									>
@@ -417,49 +430,22 @@
 											<path
 											<path
 												stroke-linecap="round"
 												stroke-linecap="round"
 												stroke-linejoin="round"
 												stroke-linejoin="round"
-												d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
+												d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
 											/>
 											/>
 										</svg>
 										</svg>
 									</button>
 									</button>
 								</Tooltip>
 								</Tooltip>
+							{/if}
+						</div>
+					</td>
+				</tr>
+			{/each}
+		</tbody>
+	</table>
+</div>
 
 
-								{#if user.role !== 'admin'}
-									<Tooltip content={$i18n.t('Delete User')}>
-										<button
-											class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-											on:click={async () => {
-												showDeleteConfirmDialog = true;
-												selectedUser = user;
-											}}
-										>
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												fill="none"
-												viewBox="0 0 24 24"
-												stroke-width="1.5"
-												stroke="currentColor"
-												class="w-4 h-4"
-											>
-												<path
-													stroke-linecap="round"
-													stroke-linejoin="round"
-													d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
-												/>
-											</svg>
-										</button>
-									</Tooltip>
-								{/if}
-							</div>
-						</td>
-					</tr>
-				{/each}
-			</tbody>
-		</table>
-	</div>
-
-	<div class=" text-gray-500 text-xs mt-1.5 text-right">
-		ⓘ {$i18n.t("Click on the user role button to change a user's role.")}
-	</div>
+<div class=" text-gray-500 text-xs mt-1.5 text-right">
+	ⓘ {$i18n.t("Click on the user role button to change a user's role.")}
+</div>
 
 
-	<Pagination bind:page count={users.length} />
-{/if}
+<Pagination bind:page count={users.length} />

+ 27 - 18
src/lib/components/chat/Chat.svelte

@@ -34,7 +34,8 @@
 		mobile,
 		mobile,
 		showOverview,
 		showOverview,
 		chatTitle,
 		chatTitle,
-		showArtifacts
+		showArtifacts,
+		tools
 	} from '$lib/stores';
 	} from '$lib/stores';
 	import {
 	import {
 		convertMessagesToHistory,
 		convertMessagesToHistory,
@@ -78,6 +79,7 @@
 	import ChatControls from './ChatControls.svelte';
 	import ChatControls from './ChatControls.svelte';
 	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
 	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
 	import Placeholder from './Placeholder.svelte';
 	import Placeholder from './Placeholder.svelte';
+	import { getTools } from '$lib/apis/tools';
 
 
 	export let chatIdProp = '';
 	export let chatIdProp = '';
 
 
@@ -153,6 +155,26 @@
 		console.log('saveSessionSelectedModels', selectedModels, sessionStorage.selectedModels);
 		console.log('saveSessionSelectedModels', selectedModels, sessionStorage.selectedModels);
 	};
 	};
 
 
+	$: if (selectedModels) {
+		setToolIds();
+	}
+
+	const setToolIds = async () => {
+		if (!$tools) {
+			tools.set(await getTools(localStorage.token));
+		}
+
+		if (selectedModels.length !== 1) {
+			return;
+		}
+		const model = $models.find((m) => m.id === selectedModels[0]);
+		if (model) {
+			selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) =>
+				$tools.find((t) => t.id === id)
+			);
+		}
+	};
+
 	const showMessage = async (message) => {
 	const showMessage = async (message) => {
 		const _chatId = JSON.parse(JSON.stringify($chatId));
 		const _chatId = JSON.parse(JSON.stringify($chatId));
 		let _messageId = JSON.parse(JSON.stringify(message.id));
 		let _messageId = JSON.parse(JSON.stringify(message.id));
@@ -480,8 +502,6 @@
 			}
 			}
 		}
 		}
 
 
-		console.log(selectedModels);
-
 		await showControls.set(false);
 		await showControls.set(false);
 		await showCallOverlay.set(false);
 		await showCallOverlay.set(false);
 		await showOverview.set(false);
 		await showOverview.set(false);
@@ -815,9 +835,12 @@
 		console.log('submitPrompt', userPrompt, $chatId);
 		console.log('submitPrompt', userPrompt, $chatId);
 
 
 		const messages = createMessagesList(history.currentId);
 		const messages = createMessagesList(history.currentId);
-		selectedModels = selectedModels.map((modelId) =>
+		const _selectedModels = selectedModels.map((modelId) =>
 			$models.map((m) => m.id).includes(modelId) ? modelId : ''
 			$models.map((m) => m.id).includes(modelId) ? modelId : ''
 		);
 		);
+		if (JSON.stringify(selectedModels) !== JSON.stringify(_selectedModels)) {
+			selectedModels = _selectedModels;
+		}
 
 
 		if (userPrompt === '') {
 		if (userPrompt === '') {
 			toast.error($i18n.t('Please enter a prompt'));
 			toast.error($i18n.t('Please enter a prompt'));
@@ -2267,13 +2290,6 @@
 								bind:selectedToolIds
 								bind:selectedToolIds
 								bind:webSearchEnabled
 								bind:webSearchEnabled
 								bind:atSelectedModel
 								bind:atSelectedModel
-								availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
-									const model = $models.find((m) => m.id === e);
-									if (model?.info?.meta?.toolIds ?? false) {
-										return [...new Set([...a, ...model.info.meta.toolIds])];
-									}
-									return a;
-								}, [])}
 								transparentBackground={$settings?.backgroundImageUrl ?? false}
 								transparentBackground={$settings?.backgroundImageUrl ?? false}
 								{stopResponse}
 								{stopResponse}
 								{createMessagePair}
 								{createMessagePair}
@@ -2311,13 +2327,6 @@
 								bind:selectedToolIds
 								bind:selectedToolIds
 								bind:webSearchEnabled
 								bind:webSearchEnabled
 								bind:atSelectedModel
 								bind:atSelectedModel
-								availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
-									const model = $models.find((m) => m.id === e);
-									if (model?.info?.meta?.toolIds ?? false) {
-										return [...new Set([...a, ...model.info.meta.toolIds])];
-									}
-									return a;
-								}, [])}
 								transparentBackground={$settings?.backgroundImageUrl ?? false}
 								transparentBackground={$settings?.backgroundImageUrl ?? false}
 								{stopResponse}
 								{stopResponse}
 								{createMessagePair}
 								{createMessagePair}

文件差异内容过多而无法显示
+ 550 - 349
src/lib/components/chat/MessageInput.svelte


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

@@ -8,7 +8,7 @@
 
 
 	import { removeLastWordFromString } from '$lib/utils';
 	import { removeLastWordFromString } from '$lib/utils';
 	import { getPrompts } from '$lib/apis/prompts';
 	import { getPrompts } from '$lib/apis/prompts';
-	import { getKnowledgeItems } from '$lib/apis/knowledge';
+	import { getKnowledgeBases } from '$lib/apis/knowledge';
 
 
 	import Prompts from './Commands/Prompts.svelte';
 	import Prompts from './Commands/Prompts.svelte';
 	import Knowledge from './Commands/Knowledge.svelte';
 	import Knowledge from './Commands/Knowledge.svelte';
@@ -46,7 +46,7 @@
 				prompts.set(await getPrompts(localStorage.token));
 				prompts.set(await getPrompts(localStorage.token));
 			})(),
 			})(),
 			(async () => {
 			(async () => {
-				knowledge.set(await getKnowledgeItems(localStorage.token));
+				knowledge.set(await getKnowledgeBases(localStorage.token));
 			})()
 			})()
 		]);
 		]);
 		loading = false;
 		loading = false;

+ 19 - 18
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -1,9 +1,10 @@
 <script lang="ts">
 <script lang="ts">
 	import { DropdownMenu } from 'bits-ui';
 	import { DropdownMenu } from 'bits-ui';
 	import { flyAndScale } from '$lib/utils/transitions';
 	import { flyAndScale } from '$lib/utils/transitions';
-	import { getContext, onMount } from 'svelte';
+	import { getContext, onMount, tick } from 'svelte';
 
 
 	import { config, user, tools as _tools } from '$lib/stores';
 	import { config, user, tools as _tools } from '$lib/stores';
+	import { getTools } from '$lib/apis/tools';
 
 
 	import Dropdown from '$lib/components/common/Dropdown.svelte';
 	import Dropdown from '$lib/components/common/Dropdown.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -11,17 +12,13 @@
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
 	import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
 	import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
 	import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
-	import { getTools } from '$lib/apis/tools';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	export let uploadFilesHandler: Function;
 	export let uploadFilesHandler: Function;
-
-	export let availableToolIds: string[] = [];
 	export let selectedToolIds: string[] = [];
 	export let selectedToolIds: string[] = [];
 
 
 	export let webSearchEnabled: boolean;
 	export let webSearchEnabled: boolean;
-
 	export let onClose: Function;
 	export let onClose: Function;
 
 
 	let tools = {};
 	let tools = {};
@@ -31,24 +28,17 @@
 		init();
 		init();
 	}
 	}
 
 
-	$: if (tools) {
-		selectedToolIds = Object.keys(tools).filter((toolId) => tools[toolId]?.enabled ?? false);
-	}
-
 	const init = async () => {
 	const init = async () => {
-		console.log('init');
 		if ($_tools === null) {
 		if ($_tools === null) {
 			await _tools.set(await getTools(localStorage.token));
 			await _tools.set(await getTools(localStorage.token));
 		}
 		}
 
 
 		tools = $_tools.reduce((a, tool, i, arr) => {
 		tools = $_tools.reduce((a, tool, i, arr) => {
-			if (availableToolIds.includes(tool.id) || ($user?.role ?? 'user') === 'admin') {
-				a[tool.id] = {
-					name: tool.name,
-					description: tool.meta.description,
-					enabled: selectedToolIds.includes(tool.id)
-				};
-			}
+			a[tool.id] = {
+				name: tool.name,
+				description: tool.meta.description,
+				enabled: selectedToolIds.includes(tool.id)
+			};
 			return a;
 			return a;
 		}, {});
 		}, {});
 	};
 	};
@@ -97,7 +87,18 @@
 							</div>
 							</div>
 
 
 							<div class=" flex-shrink-0">
 							<div class=" flex-shrink-0">
-								<Switch state={tools[toolId].enabled} />
+								<Switch
+									state={tools[toolId].enabled}
+									on:change={async (e) => {
+										const state = e.detail;
+										await tick();
+										if (state) {
+											selectedToolIds = [...selectedToolIds, toolId];
+										} else {
+											selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
+										}
+									}}
+								/>
 							</div>
 							</div>
 						</button>
 						</button>
 					{/each}
 					{/each}

+ 10 - 12
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -729,7 +729,7 @@
 
 
 							{#if message.done}
 							{#if message.done}
 								{#if !readOnly}
 								{#if !readOnly}
-									{#if $user.role === 'user' ? ($config?.permissions?.chat?.editing ?? true) : true}
+									{#if $user.role === 'user' ? ($user?.permissions?.chat?.edit ?? true) : true}
 										<Tooltip content={$i18n.t('Edit')} placement="bottom">
 										<Tooltip content={$i18n.t('Edit')} placement="bottom">
 											<button
 											<button
 												class="{isLastMessage
 												class="{isLastMessage
@@ -1125,19 +1125,17 @@
 												showRateComment = false;
 												showRateComment = false;
 												regenerateResponse(message);
 												regenerateResponse(message);
 
 
-												(model?.actions ?? [])
-													.filter((action) => action?.__webui__ ?? false)
-													.forEach((action) => {
-														dispatch('action', {
-															id: action.id,
-															event: {
-																id: 'regenerate-response',
-																data: {
-																	messageId: message.id
-																}
+												(model?.actions ?? []).forEach((action) => {
+													dispatch('action', {
+														id: action.id,
+														event: {
+															id: 'regenerate-response',
+															data: {
+																messageId: message.id
 															}
 															}
-														});
+														}
 													});
 													});
+												});
 											}}
 											}}
 										>
 										>
 											<svg
 											<svg

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

@@ -48,7 +48,7 @@
 							model: model
 							model: model
 						}))}
 						}))}
 						showTemporaryChatControl={$user.role === 'user'
 						showTemporaryChatControl={$user.role === 'user'
-							? ($config?.permissions?.chat?.temporary ?? true)
+							? ($user?.permissions?.chat?.temporary ?? true)
 							: true}
 							: true}
 						bind:value={selectedModel}
 						bind:value={selectedModel}
 					/>
 					/>

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

@@ -55,17 +55,15 @@
 	let selectedModelIdx = 0;
 	let selectedModelIdx = 0;
 
 
 	const fuse = new Fuse(
 	const fuse = new Fuse(
-		items
-			.filter((item) => !item.model?.info?.meta?.hidden)
-			.map((item) => {
-				const _item = {
-					...item,
-					modelName: item.model?.name,
-					tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
-					desc: item.model?.info?.meta?.description
-				};
-				return _item;
-			}),
+		items.map((item) => {
+			const _item = {
+				...item,
+				modelName: item.model?.name,
+				tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
+				desc: item.model?.info?.meta?.description
+			};
+			return _item;
+		}),
 		{
 		{
 			keys: ['value', 'tags', 'modelName'],
 			keys: ['value', 'tags', 'modelName'],
 			threshold: 0.3
 			threshold: 0.3
@@ -76,7 +74,7 @@
 		? fuse.search(searchValue).map((e) => {
 		? fuse.search(searchValue).map((e) => {
 				return e.item;
 				return e.item;
 			})
 			})
-		: items.filter((item) => !item.model?.info?.meta?.hidden);
+		: items;
 
 
 	const pullModelHandler = async () => {
 	const pullModelHandler = async () => {
 		const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
 		const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
@@ -583,14 +581,3 @@
 		</slot>
 		</slot>
 	</DropdownMenu.Content>
 	</DropdownMenu.Content>
 </DropdownMenu.Root>
 </DropdownMenu.Root>
-
-<style>
-	.scrollbar-hidden:active::-webkit-scrollbar-thumb,
-	.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
-	.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
-		visibility: visible;
-	}
-	.scrollbar-hidden::-webkit-scrollbar-thumb {
-		visibility: hidden;
-	}
-</style>

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

@@ -32,7 +32,7 @@
 
 
 	export let prompt = '';
 	export let prompt = '';
 	export let files = [];
 	export let files = [];
-	export let availableToolIds = [];
+
 	export let selectedToolIds = [];
 	export let selectedToolIds = [];
 	export let webSearchEnabled = false;
 	export let webSearchEnabled = false;
 
 
@@ -200,7 +200,6 @@
 						bind:selectedToolIds
 						bind:selectedToolIds
 						bind:webSearchEnabled
 						bind:webSearchEnabled
 						bind:atSelectedModel
 						bind:atSelectedModel
-						{availableToolIds}
 						{transparentBackground}
 						{transparentBackground}
 						{stopResponse}
 						{stopResponse}
 						{createMessagePair}
 						{createMessagePair}

+ 2 - 4
src/lib/components/common/Switch.svelte

@@ -4,14 +4,12 @@
 	export let state = true;
 	export let state = true;
 
 
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
+
+	$: dispatch('change', state);
 </script>
 </script>
 
 
 <Switch.Root
 <Switch.Root
 	bind:checked={state}
 	bind:checked={state}
-	onCheckedChange={async (e) => {
-		await tick();
-		dispatch('change', e);
-	}}
 	class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] mx-[1px] transition  {state
 	class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] mx-[1px] transition  {state
 		? ' bg-emerald-600'
 		? ' bg-emerald-600'
 		: 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800"
 		: 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800"

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

@@ -0,0 +1,19 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"
+	/>
+</svg>

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

@@ -0,0 +1,11 @@
+<script lang="ts">
+	export let className = 'size-4';
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class={className}>
+	<path
+		fill-rule="evenodd"
+		d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z"
+		clip-rule="evenodd"
+	/>
+</svg>

+ 9 - 0
src/lib/components/icons/UserPlusSolid.svelte

@@ -0,0 +1,9 @@
+<script lang="ts">
+	export let className = 'size-4';
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class={className}>
+	<path
+		d="M10 5a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM1.615 16.428a1.224 1.224 0 0 1-.569-1.175 6.002 6.002 0 0 1 11.908 0c.058.467-.172.92-.57 1.174A9.953 9.953 0 0 1 7 18a9.953 9.953 0 0 1-5.385-1.572ZM16.25 5.75a.75.75 0 0 0-1.5 0v2h-2a.75.75 0 0 0 0 1.5h2v2a.75.75 0 0 0 1.5 0v-2h2a.75.75 0 0 0 0-1.5h-2v-2Z"
+	/>
+</svg>

+ 9 - 0
src/lib/components/icons/UsersSolid.svelte

@@ -0,0 +1,9 @@
+<script lang="ts">
+	export let className = 'size-4';
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
+	<path
+		d="M4.5 6.375a4.125 4.125 0 1 1 8.25 0 4.125 4.125 0 0 1-8.25 0ZM14.25 8.625a3.375 3.375 0 1 1 6.75 0 3.375 3.375 0 0 1-6.75 0ZM1.5 19.125a7.125 7.125 0 0 1 14.25 0v.003l-.001.119a.75.75 0 0 1-.363.63 13.067 13.067 0 0 1-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 0 1-.364-.63l-.001-.122ZM17.25 19.128l-.001.144a2.25 2.25 0 0 1-.233.96 10.088 10.088 0 0 0 5.06-1.01.75.75 0 0 0 .42-.643 4.875 4.875 0 0 0-6.957-4.611 8.586 8.586 0 0 1 1.71 5.157v.003Z"
+	/>
+</svg>

+ 20 - 0
src/lib/components/icons/Wrench.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	export let className = 'w-4 h-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="M21.75 6.75a4.5 4.5 0 0 1-4.884 4.484c-1.076-.091-2.264.071-2.95.904l-7.152 8.684a2.548 2.548 0 1 1-3.586-3.586l8.684-7.152c.833-.686.995-1.874.904-2.95a4.5 4.5 0 0 1 6.336-4.486l-3.276 3.276a3.004 3.004 0 0 0 2.25 2.25l3.276-3.276c.256.565.398 1.192.398 1.852Z"
+	/>
+	<path stroke-linecap="round" stroke-linejoin="round" d="M4.867 19.125h.008v.008h-.008v-.008Z" />
+</svg>

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

@@ -470,7 +470,7 @@
 			</button>
 			</button>
 		</div>
 		</div>
 
 
-		{#if $user?.role === 'admin'}
+		{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
 			<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
 			<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
 				<a
 				<a
 					class="flex-grow flex space-x-3 rounded-lg px-2 py-[7px] hover:bg-gray-100 dark:hover:bg-gray-900 transition"
 					class="flex-grow flex space-x-3 rounded-lg px-2 py-[7px] hover:bg-gray-100 dark:hover:bg-gray-900 transition"

+ 113 - 101
src/lib/components/workspace/Knowledge.svelte

@@ -10,20 +10,22 @@
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	import { WEBUI_NAME, knowledge } from '$lib/stores';
 	import { WEBUI_NAME, knowledge } from '$lib/stores';
-
-	import { getKnowledgeItems, deleteKnowledgeById } from '$lib/apis/knowledge';
-
-	import { blobToFile, transformFileName } from '$lib/utils';
+	import {
+		getKnowledgeBases,
+		deleteKnowledgeById,
+		getKnowledgeBaseList
+	} from '$lib/apis/knowledge';
 
 
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
-	import Tooltip from '../common/Tooltip.svelte';
-	import GarbageBin from '../icons/GarbageBin.svelte';
-	import Pencil from '../icons/Pencil.svelte';
+
 	import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
 	import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
 	import ItemMenu from './Knowledge/ItemMenu.svelte';
 	import ItemMenu from './Knowledge/ItemMenu.svelte';
 	import Badge from '../common/Badge.svelte';
 	import Badge from '../common/Badge.svelte';
 	import Search from '../icons/Search.svelte';
 	import Search from '../icons/Search.svelte';
 	import Plus from '../icons/Plus.svelte';
 	import Plus from '../icons/Plus.svelte';
+	import Spinner from '../common/Spinner.svelte';
+
+	let loaded = false;
 
 
 	let query = '';
 	let query = '';
 	let selectedItem = null;
 	let selectedItem = null;
@@ -31,13 +33,21 @@
 
 
 	let fuse = null;
 	let fuse = null;
 
 
+	let knowledgeBases = [];
 	let filteredItems = [];
 	let filteredItems = [];
+
+	$: if (knowledgeBases) {
+		fuse = new Fuse(knowledgeBases, {
+			keys: ['name', 'description']
+		});
+	}
+
 	$: if (fuse) {
 	$: if (fuse) {
 		filteredItems = query
 		filteredItems = query
 			? fuse.search(query).map((e) => {
 			? fuse.search(query).map((e) => {
 					return e.item;
 					return e.item;
 				})
 				})
-			: $knowledge;
+			: knowledgeBases;
 	}
 	}
 
 
 	const deleteHandler = async (item) => {
 	const deleteHandler = async (item) => {
@@ -46,19 +56,15 @@
 		});
 		});
 
 
 		if (res) {
 		if (res) {
-			knowledge.set(await getKnowledgeItems(localStorage.token));
+			knowledgeBases = await getKnowledgeBaseList(localStorage.token);
+			knowledge.set(await getKnowledgeBases(localStorage.token));
 			toast.success($i18n.t('Knowledge deleted successfully.'));
 			toast.success($i18n.t('Knowledge deleted successfully.'));
 		}
 		}
 	};
 	};
 
 
 	onMount(async () => {
 	onMount(async () => {
-		knowledge.set(await getKnowledgeItems(localStorage.token));
-
-		knowledge.subscribe((value) => {
-			fuse = new Fuse(value, {
-				keys: ['name', 'description']
-			});
-		});
+		knowledgeBases = await getKnowledgeBaseList(localStorage.token);
+		loaded = true;
 	});
 	});
 </script>
 </script>
 
 
@@ -68,104 +74,110 @@
 	</title>
 	</title>
 </svelte:head>
 </svelte:head>
 
 
-<DeleteConfirmDialog
-	bind:show={showDeleteConfirm}
-	on:confirm={() => {
-		deleteHandler(selectedItem);
-	}}
-/>
-
-<div class="flex flex-col gap-1 mt-1.5 mb-2">
-	<div class="flex justify-between items-center">
-		<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
-			{$i18n.t('Knowledge')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
-			<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
-				>{filteredItems.length}</span
-			>
+{#if loaded}
+	<DeleteConfirmDialog
+		bind:show={showDeleteConfirm}
+		on:confirm={() => {
+			deleteHandler(selectedItem);
+		}}
+	/>
+
+	<div class="flex flex-col gap-1 mt-1.5 mb-2">
+		<div class="flex justify-between items-center">
+			<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
+				{$i18n.t('Knowledge')}
+				<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
+				<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
+					>{filteredItems.length}</span
+				>
+			</div>
 		</div>
 		</div>
-	</div>
 
 
-	<div class=" flex w-full space-x-2">
-		<div class="flex flex-1">
-			<div class=" self-center ml-1 mr-3">
-				<Search className="size-3.5" />
+		<div class=" flex w-full space-x-2">
+			<div class="flex flex-1">
+				<div class=" self-center ml-1 mr-3">
+					<Search className="size-3.5" />
+				</div>
+				<input
+					class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
+					bind:value={query}
+					placeholder={$i18n.t('Search Knowledge')}
+				/>
+			</div>
+
+			<div>
+				<button
+					class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
+					aria-label={$i18n.t('Create Knowledge')}
+					on:click={() => {
+						goto('/workspace/knowledge/create');
+					}}
+				>
+					<Plus className="size-3.5" />
+				</button>
 			</div>
 			</div>
-			<input
-				class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
-				bind:value={query}
-				placeholder={$i18n.t('Search Knowledge')}
-			/>
 		</div>
 		</div>
+	</div>
 
 
-		<div>
+	<div class="my-3 mb-5 grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
+		{#each filteredItems as item}
 			<button
 			<button
-				class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
-				aria-label={$i18n.t('Create Knowledge')}
+				class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-xl"
 				on:click={() => {
 				on:click={() => {
-					goto('/workspace/knowledge/create');
+					if (item?.meta?.document) {
+						toast.error(
+							$i18n.t(
+								'Only collections can be edited, create a new knowledge base to edit/add documents.'
+							)
+						);
+					} else {
+						goto(`/workspace/knowledge/${item.id}`);
+					}
 				}}
 				}}
 			>
 			>
-				<Plus className="size-3.5" />
-			</button>
-		</div>
-	</div>
-</div>
-
-<div class="my-3 mb-5 grid md:grid-cols-2 lg:grid-cols-3 gap-2">
-	{#each filteredItems as item}
-		<button
-			class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-xl"
-			on:click={() => {
-				if (item?.meta?.document) {
-					toast.error(
-						$i18n.t(
-							'Only collections can be edited, create a new knowledge base to edit/add documents.'
-						)
-					);
-				} else {
-					goto(`/workspace/knowledge/${item.id}`);
-				}
-			}}
-		>
-			<div class=" w-full">
-				<div class="flex items-center justify-between -mt-1">
-					<div class=" font-semibold line-clamp-1 h-fit">{item.name}</div>
-
-					<div class=" flex self-center">
-						<ItemMenu
-							on:delete={() => {
-								selectedItem = item;
-								showDeleteConfirm = true;
-							}}
-						/>
-					</div>
-				</div>
-
-				<div class=" self-center flex-1">
-					<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
-						{item.description}
+				<div class=" w-full">
+					<div class="flex items-center justify-between -mt-1">
+						<div class=" font-semibold line-clamp-1 h-fit">{item.name}</div>
+
+						<div class=" flex self-center">
+							<ItemMenu
+								on:delete={() => {
+									selectedItem = item;
+									showDeleteConfirm = true;
+								}}
+							/>
+						</div>
 					</div>
 					</div>
 
 
-					<div class="mt-5 flex justify-between">
-						<div>
-							{#if item?.meta?.document}
-								<Badge type="muted" content={$i18n.t('Document')} />
-							{:else}
-								<Badge type="success" content={$i18n.t('Collection')} />
-							{/if}
+					<div class=" self-center flex-1">
+						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
+							{item.description}
 						</div>
 						</div>
-						<div class=" text-xs text-gray-500 line-clamp-1">
-							{$i18n.t('Updated')}
-							{dayjs(item.updated_at * 1000).fromNow()}
+
+						<div class="mt-5 flex justify-between">
+							<div>
+								{#if item?.meta?.document}
+									<Badge type="muted" content={$i18n.t('Document')} />
+								{:else}
+									<Badge type="success" content={$i18n.t('Collection')} />
+								{/if}
+							</div>
+							<div class=" text-xs text-gray-500 line-clamp-1">
+								{$i18n.t('Updated')}
+								{dayjs(item.updated_at * 1000).fromNow()}
+							</div>
 						</div>
 						</div>
 					</div>
 					</div>
 				</div>
 				</div>
-			</div>
-		</button>
-	{/each}
-</div>
+			</button>
+		{/each}
+	</div>
 
 
-<div class=" text-gray-500 text-xs mt-1 mb-2">
-	ⓘ {$i18n.t("Use '#' in the prompt input to load and include your knowledge.")}
-</div>
+	<div class=" text-gray-500 text-xs mt-1 mb-2">
+		ⓘ {$i18n.t("Use '#' in the prompt input to load and include your knowledge.")}
+	</div>
+{:else}
+	<div class="w-full h-full flex justify-center items-center">
+		<Spinner />
+	</div>
+{/if}

+ 16 - 3
src/lib/components/workspace/Knowledge/CreateCollection.svelte → src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte

@@ -3,14 +3,16 @@
 	import { getContext } from 'svelte';
 	import { getContext } from 'svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
-	import { createNewKnowledge, getKnowledgeItems } from '$lib/apis/knowledge';
+	import { createNewKnowledge, getKnowledgeBases } from '$lib/apis/knowledge';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { knowledge } from '$lib/stores';
 	import { knowledge } from '$lib/stores';
+	import AccessControl from '../common/AccessControl.svelte';
 
 
 	let loading = false;
 	let loading = false;
 
 
 	let name = '';
 	let name = '';
 	let description = '';
 	let description = '';
+	let accessControl = null;
 
 
 	const submitHandler = async () => {
 	const submitHandler = async () => {
 		loading = true;
 		loading = true;
@@ -23,13 +25,18 @@
 			return;
 			return;
 		}
 		}
 
 
-		const res = await createNewKnowledge(localStorage.token, name, description).catch((e) => {
+		const res = await createNewKnowledge(
+			localStorage.token,
+			name,
+			description,
+			accessControl
+		).catch((e) => {
 			toast.error(e);
 			toast.error(e);
 		});
 		});
 
 
 		if (res) {
 		if (res) {
 			toast.success($i18n.t('Knowledge created successfully.'));
 			toast.success($i18n.t('Knowledge created successfully.'));
-			knowledge.set(await getKnowledgeItems(localStorage.token));
+			knowledge.set(await getKnowledgeBases(localStorage.token));
 			goto(`/workspace/knowledge/${res.id}`);
 			goto(`/workspace/knowledge/${res.id}`);
 		}
 		}
 
 
@@ -103,6 +110,12 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
+		<div class="mt-2">
+			<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
+				<AccessControl bind:accessControl />
+			</div>
+		</div>
+
 		<div class="flex justify-end mt-2">
 		<div class="flex justify-end mt-2">
 			<div>
 			<div>
 				<button
 				<button

+ 78 - 44
src/lib/components/workspace/Knowledge/Collection.svelte → src/lib/components/workspace/Knowledge/KnowledgeBase.svelte

@@ -15,7 +15,7 @@
 	import {
 	import {
 		addFileToKnowledgeById,
 		addFileToKnowledgeById,
 		getKnowledgeById,
 		getKnowledgeById,
-		getKnowledgeItems,
+		getKnowledgeBases,
 		removeFileFromKnowledgeById,
 		removeFileFromKnowledgeById,
 		resetKnowledgeById,
 		resetKnowledgeById,
 		updateFileFromKnowledgeById,
 		updateFileFromKnowledgeById,
@@ -27,18 +27,19 @@
 	import { processFile } from '$lib/apis/retrieval';
 	import { processFile } from '$lib/apis/retrieval';
 
 
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
-	import Files from './Collection/Files.svelte';
+	import Files from './KnowledgeBase/Files.svelte';
 	import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
 	import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
 
 
-	import AddContentMenu from './Collection/AddContentMenu.svelte';
-	import AddTextContentModal from './Collection/AddTextContentModal.svelte';
+	import AddContentMenu from './KnowledgeBase/AddContentMenu.svelte';
+	import AddTextContentModal from './KnowledgeBase/AddTextContentModal.svelte';
 
 
 	import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
 	import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
 	import RichTextInput from '$lib/components/common/RichTextInput.svelte';
 	import RichTextInput from '$lib/components/common/RichTextInput.svelte';
 	import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
 	import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
 	import Drawer from '$lib/components/common/Drawer.svelte';
 	import Drawer from '$lib/components/common/Drawer.svelte';
 	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
 	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
-	import MenuLines from '$lib/components/icons/MenuLines.svelte';
+	import LockClosed from '$lib/components/icons/LockClosed.svelte';
+	import AccessControlModal from '../common/AccessControlModal.svelte';
 
 
 	let largeScreen = true;
 	let largeScreen = true;
 
 
@@ -62,6 +63,7 @@
 
 
 	let showAddTextContentModal = false;
 	let showAddTextContentModal = false;
 	let showSyncConfirmModal = false;
 	let showSyncConfirmModal = false;
+	let showAccessControlModal = false;
 
 
 	let inputFiles = null;
 	let inputFiles = null;
 
 
@@ -420,14 +422,15 @@
 
 
 			const res = await updateKnowledgeById(localStorage.token, id, {
 			const res = await updateKnowledgeById(localStorage.token, id, {
 				name: knowledge.name,
 				name: knowledge.name,
-				description: knowledge.description
+				description: knowledge.description,
+				access_control: knowledge.access_control
 			}).catch((e) => {
 			}).catch((e) => {
 				toast.error(e);
 				toast.error(e);
 			});
 			});
 
 
 			if (res) {
 			if (res) {
 				toast.success($i18n.t('Knowledge updated successfully'));
 				toast.success($i18n.t('Knowledge updated successfully'));
-				_knowledge.set(await getKnowledgeItems(localStorage.token));
+				_knowledge.set(await getKnowledgeBases(localStorage.token));
 			}
 			}
 		}, 1000);
 		}, 1000);
 	};
 	};
@@ -596,8 +599,63 @@
 	}}
 	}}
 />
 />
 
 
-<div class="flex flex-col w-full h-full max-h-[100dvh] mt-1" id="collection-container">
+<div class="flex flex-col w-full h-full max-h-[100dvh] translate-y-1" id="collection-container">
 	{#if id && knowledge}
 	{#if id && knowledge}
+		<AccessControlModal
+			bind:show={showAccessControlModal}
+			bind:accessControl={knowledge.access_control}
+			onChange={() => {
+				changeDebounceHandler();
+			}}
+		/>
+		<div class="w-full mb-2.5">
+			<div class=" flex w-full">
+				<div class="flex-1">
+					<div class="flex items-center justify-between w-full px-0.5 mb-1">
+						<div class="w-full">
+							<input
+								type="text"
+								class="text-left w-full font-semibold text-2xl font-primary bg-transparent outline-none"
+								bind:value={knowledge.name}
+								placeholder="Knowledge Name"
+								on:input={() => {
+									changeDebounceHandler();
+								}}
+							/>
+						</div>
+
+						<div class="self-center">
+							<button
+								class="bg-gray-50 hover:bg-gray-100 text-black transition px-2 py-1 rounded-full flex gap-1 items-center"
+								type="button"
+								on:click={() => {
+									showAccessControlModal = true;
+								}}
+							>
+								<LockClosed strokeWidth="2.5" className="size-3.5" />
+
+								<div class="text-sm font-medium flex-shrink-0">
+									{$i18n.t('Share')}
+								</div>
+							</button>
+						</div>
+					</div>
+
+					<div class="flex w-full px-1">
+						<input
+							type="text"
+							class="text-left text-xs w-full text-gray-500 bg-transparent outline-none"
+							bind:value={knowledge.description}
+							placeholder="Knowledge Description"
+							on:input={() => {
+								changeDebounceHandler();
+							}}
+						/>
+					</div>
+				</div>
+			</div>
+		</div>
+
 		<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
 		<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
 			<PaneGroup direction="horizontal">
 			<PaneGroup direction="horizontal">
 				<Pane
 				<Pane
@@ -687,7 +745,17 @@
 										/>
 										/>
 									</div>
 									</div>
 								{:else}
 								{:else}
-									<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
+									<div
+										class="m-auto flex flex-col justify-center text-center text-gray-500 text-xs"
+									>
+										<div>
+											{$i18n.t('No content found')}
+										</div>
+
+										<div class="mx-12 mt-2 text-center text-gray-200 dark:text-gray-700">
+											{$i18n.t('Drag and drop a file to upload or select a file to view')}
+										</div>
+									</div>
 								{/if}
 								{/if}
 							</div>
 							</div>
 						</div>
 						</div>
@@ -753,41 +821,7 @@
 									</div>
 									</div>
 								</div>
 								</div>
 							{:else}
 							{:else}
-								<div class="m-auto pb-32">
-									<div>
-										<div class=" flex w-full mt-1 mb-3.5">
-											<div class="flex-1">
-												<div class="flex items-center justify-between w-full px-0.5 mb-1">
-													<div class="w-full">
-														<input
-															type="text"
-															class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
-															bind:value={knowledge.name}
-															on:input={() => {
-																changeDebounceHandler();
-															}}
-														/>
-													</div>
-												</div>
-
-												<div class="flex w-full px-1">
-													<input
-														type="text"
-														class="text-center w-full text-gray-500 bg-transparent outline-none"
-														bind:value={knowledge.description}
-														on:input={() => {
-															changeDebounceHandler();
-														}}
-													/>
-												</div>
-											</div>
-										</div>
-									</div>
-
-									<div class=" mt-2 text-center text-sm text-gray-200 dark:text-gray-700 w-full">
-										{$i18n.t('Select a file to view or drag and drop a file to upload')}
-									</div>
-								</div>
+								<div></div>
 							{/if}
 							{/if}
 						</div>
 						</div>
 					</Pane>
 					</Pane>

+ 0 - 0
src/lib/components/workspace/Knowledge/Collection/AddContentMenu.svelte → src/lib/components/workspace/Knowledge/KnowledgeBase/AddContentMenu.svelte


+ 0 - 0
src/lib/components/workspace/Knowledge/Collection/AddTextContentModal.svelte → src/lib/components/workspace/Knowledge/KnowledgeBase/AddTextContentModal.svelte


+ 0 - 0
src/lib/components/workspace/Knowledge/Collection/Files.svelte → src/lib/components/workspace/Knowledge/KnowledgeBase/Files.svelte


+ 288 - 489
src/lib/components/workspace/Models.svelte

@@ -8,12 +8,17 @@
 	const { saveAs } = fileSaver;
 	const { saveAs } = fileSaver;
 
 
 	import { onMount, getContext, tick } from 'svelte';
 	import { onMount, getContext, tick } from 'svelte';
-
-	import { WEBUI_NAME, config, mobile, models, settings, user } from '$lib/stores';
-	import { addNewModel, deleteModelById, getModelInfos, updateModelById } from '$lib/apis/models';
-
-	import { deleteModel } from '$lib/apis/ollama';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
+	const i18n = getContext('i18n');
+
+	import { WEBUI_NAME, config, mobile, models as _models, settings, user } from '$lib/stores';
+	import {
+		createNewModel,
+		deleteModelById,
+		getModels as getWorkspaceModels,
+		toggleModelById,
+		updateModelById
+	} from '$lib/apis/models';
 
 
 	import { getModels } from '$lib/apis';
 	import { getModels } from '$lib/apis';
 
 
@@ -24,67 +29,52 @@
 	import GarbageBin from '../icons/GarbageBin.svelte';
 	import GarbageBin from '../icons/GarbageBin.svelte';
 	import Search from '../icons/Search.svelte';
 	import Search from '../icons/Search.svelte';
 	import Plus from '../icons/Plus.svelte';
 	import Plus from '../icons/Plus.svelte';
-
-	const i18n = getContext('i18n');
+	import ChevronRight from '../icons/ChevronRight.svelte';
+	import Switch from '../common/Switch.svelte';
+	import Spinner from '../common/Spinner.svelte';
 
 
 	let shiftKey = false;
 	let shiftKey = false;
 
 
-	let showModelDeleteConfirm = false;
-
-	let localModelfiles = [];
-
 	let importFiles;
 	let importFiles;
 	let modelsImportInputElement: HTMLInputElement;
 	let modelsImportInputElement: HTMLInputElement;
+	let loaded = false;
 
 
-	let _models = [];
+	let models = [];
 
 
 	let filteredModels = [];
 	let filteredModels = [];
 	let selectedModel = null;
 	let selectedModel = null;
 
 
-	$: if (_models) {
-		filteredModels = _models
-			.filter((m) => m?.owned_by !== 'arena')
-			.filter(
-				(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
-			);
+	let showModelDeleteConfirm = false;
+
+	$: if (models) {
+		filteredModels = models.filter(
+			(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
+		);
 	}
 	}
 
 
-	let sortable = null;
 	let searchValue = '';
 	let searchValue = '';
 
 
 	const deleteModelHandler = async (model) => {
 	const deleteModelHandler = async (model) => {
-		console.log(model.info);
-		if (!model?.info) {
-			toast.error(
-				$i18n.t('{{ owner }}: You cannot delete a base model', {
-					owner: model.owned_by.toUpperCase()
-				})
-			);
+		const res = await deleteModelById(localStorage.token, model.id).catch((e) => {
+			toast.error(e);
 			return null;
 			return null;
-		}
-
-		const res = await deleteModelById(localStorage.token, model.id);
+		});
 
 
 		if (res) {
 		if (res) {
 			toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
 			toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
 		}
 		}
 
 
-		await models.set(await getModels(localStorage.token));
-		_models = $models;
+		await _models.set(await getModels(localStorage.token));
+		models = await getWorkspaceModels(localStorage.token);
 	};
 	};
 
 
 	const cloneModelHandler = async (model) => {
 	const cloneModelHandler = async (model) => {
-		if ((model?.info?.base_model_id ?? null) === null) {
-			toast.error($i18n.t('You cannot clone a base model'));
-			return;
-		} else {
-			sessionStorage.model = JSON.stringify({
-				...model,
-				id: `${model.id}-clone`,
-				name: `${model.name} (Clone)`
-			});
-			goto('/workspace/models/create');
-		}
+		sessionStorage.model = JSON.stringify({
+			...model,
+			id: `${model.id}-clone`,
+			name: `${model.name} (Clone)`
+		});
+		goto('/workspace/models/create');
 	};
 	};
 
 
 	const shareModelHandler = async (model) => {
 	const shareModelHandler = async (model) => {
@@ -108,58 +98,6 @@
 		window.addEventListener('message', messageHandler, false);
 		window.addEventListener('message', messageHandler, false);
 	};
 	};
 
 
-	const moveToTopHandler = async (model) => {
-		// find models with position 0 and set them to 1
-		const topModels = _models.filter((m) => m.info?.meta?.position === 0);
-		for (const m of topModels) {
-			let info = m.info;
-			if (!info) {
-				info = {
-					id: m.id,
-					name: m.name,
-					meta: {
-						position: 1
-					},
-					params: {}
-				};
-			}
-
-			info.meta = {
-				...info.meta,
-				position: 1
-			};
-
-			await updateModelById(localStorage.token, info.id, info);
-		}
-
-		let info = model.info;
-
-		if (!info) {
-			info = {
-				id: model.id,
-				name: model.name,
-				meta: {
-					position: 0
-				},
-				params: {}
-			};
-		}
-
-		info.meta = {
-			...info.meta,
-			position: 0
-		};
-
-		const res = await updateModelById(localStorage.token, info.id, info);
-
-		if (res) {
-			toast.success($i18n.t(`Model {{name}} is now at the top`, { name: info.id }));
-		}
-
-		await models.set(await getModels(localStorage.token));
-		_models = $models;
-	};
-
 	const hideModelHandler = async (model) => {
 	const hideModelHandler = async (model) => {
 		let info = model.info;
 		let info = model.info;
 
 
@@ -192,8 +130,8 @@
 			);
 			);
 		}
 		}
 
 
-		await models.set(await getModels(localStorage.token));
-		_models = $models;
+		await _models.set(await getModels(localStorage.token));
+		models = await getWorkspaceModels(localStorage.token);
 	};
 	};
 
 
 	const downloadModels = async (models) => {
 	const downloadModels = async (models) => {
@@ -210,60 +148,10 @@
 		saveAs(blob, `${model.id}-${Date.now()}.json`);
 		saveAs(blob, `${model.id}-${Date.now()}.json`);
 	};
 	};
 
 
-	const positionChangeHandler = async () => {
-		// Get the new order of the models
-		const modelIds = Array.from(document.getElementById('model-list').children).map((child) =>
-			child.id.replace('model-item-', '')
-		);
-
-		// Update the position of the models
-		for (const [index, id] of modelIds.entries()) {
-			const model = $models.find((m) => m.id === id);
-			if (model) {
-				let info = model.info;
-
-				if (!info) {
-					info = {
-						id: model.id,
-						name: model.name,
-						meta: {
-							position: index
-						},
-						params: {}
-					};
-				}
-
-				info.meta = {
-					...info.meta,
-					position: index
-				};
-				await updateModelById(localStorage.token, info.id, info);
-			}
-		}
-
-		await tick();
-		await models.set(await getModels(localStorage.token));
-	};
-
 	onMount(async () => {
 	onMount(async () => {
-		// Legacy code to sync localModelfiles with models
-		_models = $models;
-		localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
-
-		if (localModelfiles) {
-			console.log(localModelfiles);
-		}
+		models = await getWorkspaceModels(localStorage.token);
 
 
-		if (!$mobile) {
-			// SortableJS
-			sortable = new Sortable(document.getElementById('model-list'), {
-				animation: 150,
-				onUpdate: async (event) => {
-					console.log(event);
-					positionChangeHandler();
-				}
-			});
-		}
+		loaded = true;
 
 
 		const onKeyDown = (event) => {
 		const onKeyDown = (event) => {
 			if (event.key === 'Shift') {
 			if (event.key === 'Shift') {
@@ -299,356 +187,276 @@
 	</title>
 	</title>
 </svelte:head>
 </svelte:head>
 
 
-<ModelDeleteConfirmDialog
-	bind:show={showModelDeleteConfirm}
-	on:confirm={() => {
-		deleteModelHandler(selectedModel);
-	}}
-/>
-
-<div class="flex flex-col gap-1 mt-1.5 mb-2">
-	<div class="flex justify-between items-center">
-		<div class="flex items-center md:self-center text-xl font-medium px-0.5">
-			{$i18n.t('Models')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
-			<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
-				>{filteredModels.length}</span
-			>
+{#if loaded}
+	<ModelDeleteConfirmDialog
+		bind:show={showModelDeleteConfirm}
+		on:confirm={() => {
+			deleteModelHandler(selectedModel);
+		}}
+	/>
+
+	<div class="flex flex-col gap-1 mt-1.5 mb-2">
+		<div class="flex justify-between items-center">
+			<div class="flex items-center md:self-center text-xl font-medium px-0.5">
+				{$i18n.t('Models')}
+				<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
+				<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
+					>{filteredModels.length}</span
+				>
+			</div>
 		</div>
 		</div>
-	</div>
 
 
-	<div class=" flex flex-1 items-center w-full space-x-2">
-		<div class="flex flex-1 items-center">
-			<div class=" self-center ml-1 mr-3">
-				<Search className="size-3.5" />
+		<div class=" flex flex-1 items-center w-full space-x-2">
+			<div class="flex flex-1 items-center">
+				<div class=" self-center ml-1 mr-3">
+					<Search className="size-3.5" />
+				</div>
+				<input
+					class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
+					bind:value={searchValue}
+					placeholder={$i18n.t('Search Models')}
+				/>
 			</div>
 			</div>
-			<input
-				class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
-				bind:value={searchValue}
-				placeholder={$i18n.t('Search Models')}
-			/>
-		</div>
 
 
-		<div>
-			<a
-				class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
-				href="/workspace/models/create"
-			>
-				<Plus className="size-3.5" />
-			</a>
+			<div>
+				<a
+					class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
+					href="/workspace/models/create"
+				>
+					<Plus className="size-3.5" />
+				</a>
+			</div>
 		</div>
 		</div>
 	</div>
 	</div>
-</div>
-
-<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-1" href="/workspace/models/create">
-	<div class=" self-center w-8 flex-shrink-0">
-		<div
-			class="w-full h-8 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
-		>
-			<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
-				<path
-					fill-rule="evenodd"
-					d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-					clip-rule="evenodd"
-				/>
-			</svg>
+
+	<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-1" href="/workspace/models/create">
+		<div class=" self-center w-8 flex-shrink-0">
+			<div
+				class="w-full h-8 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
+			>
+				<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
+					<path
+						fill-rule="evenodd"
+						d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
 		</div>
 		</div>
-	</div>
 
 
-	<div class=" self-center">
-		<div class=" font-semibold line-clamp-1">{$i18n.t('Create a model')}</div>
-		<div class=" text-sm line-clamp-1 text-gray-500">
-			{$i18n.t('Customize models for a specific purpose')}
+		<div class=" self-center">
+			<div class=" font-semibold line-clamp-1">{$i18n.t('Create a model')}</div>
+			<div class=" text-sm line-clamp-1 text-gray-500">
+				{$i18n.t('Customize models for a specific purpose')}
+			</div>
 		</div>
 		</div>
-	</div>
-</a>
-
-<div class=" my-2 mb-5" id="model-list">
-	{#each filteredModels as model}
-		<div
-			class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
-			id="model-item-{model.id}"
-		>
-			<a
-				class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
-				href={`/?models=${encodeURIComponent(model.id)}`}
-			>
-				<div class=" self-start w-8 pt-0.5">
-					<div
-						class=" rounded-full object-cover {(model?.info?.meta?.hidden ?? false)
-							? 'brightness-90 dark:brightness-50'
-							: ''} "
-					>
-						<img
-							src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
-							alt="modelfile profile"
-							class=" rounded-full w-full h-auto object-cover"
-						/>
-					</div>
-				</div>
+	</a>
 
 
-				<div
-					class=" flex-1 self-center {(model?.info?.meta?.hidden ?? false) ? 'text-gray-500' : ''}"
+	<div class=" my-2 mb-5" id="model-list">
+		{#each filteredModels as model}
+			<div
+				class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
+				id="model-item-{model.id}"
+			>
+				<a
+					class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
+					href={`/?models=${encodeURIComponent(model.id)}`}
 				>
 				>
-					<Tooltip
-						content={marked.parse(
-							model?.ollama?.digest
-								? `${model?.ollama?.digest} *(${model?.ollama?.modified_at})*`
-								: ''
-						)}
-						className=" w-fit"
-						placement="top-start"
-					>
-						<div class="  font-semibold line-clamp-1">{model.name}</div>
-					</Tooltip>
-					<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
-						{!!model?.info?.meta?.description
-							? model?.info?.meta?.description
-							: model?.ollama?.digest
-								? `${model.id} (${model?.ollama?.digest})`
-								: model.id}
+					<div class=" self-center w-8">
+						<div
+							class=" rounded-full object-cover {model.is_active
+								? ''
+								: 'opacity-50 dark:opacity-50'} "
+						>
+							<img
+								src={model?.meta?.profile_image_url ?? '/static/favicon.png'}
+								alt="modelfile profile"
+								class=" rounded-full w-full h-auto object-cover"
+							/>
+						</div>
 					</div>
 					</div>
-				</div>
-			</a>
-			<div class="flex flex-row gap-0.5 self-center">
-				{#if shiftKey}
-					<Tooltip
-						content={(model?.info?.meta?.hidden ?? false)
-							? $i18n.t('Show Model')
-							: $i18n.t('Hide Model')}
-					>
-						<button
-							class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-							type="button"
-							on:click={() => {
-								hideModelHandler(model);
-							}}
+
+					<div class=" flex-1 self-center {model.is_active ? '' : 'text-gray-500'}">
+						<Tooltip
+							content={marked.parse(model?.meta?.description ?? model.id)}
+							className=" w-fit"
+							placement="top-start"
 						>
 						>
-							{#if model?.info?.meta?.hidden ?? false}
+							<div class="  font-semibold line-clamp-1">{model.name}</div>
+						</Tooltip>
+						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
+							{model?.meta?.description ?? model.id}
+						</div>
+					</div>
+				</a>
+				<div class="flex flex-row gap-0.5 items-center self-center">
+					{#if shiftKey}
+						<Tooltip content={$i18n.t('Delete')}>
+							<button
+								class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+								type="button"
+								on:click={() => {
+									deleteModelHandler(model);
+								}}
+							>
+								<GarbageBin />
+							</button>
+						</Tooltip>
+					{:else}
+						{#if $user?.role === 'admin' || model.user_id === $user?.id}
+							<a
+								class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+								type="button"
+								href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
+							>
 								<svg
 								<svg
 									xmlns="http://www.w3.org/2000/svg"
 									xmlns="http://www.w3.org/2000/svg"
 									fill="none"
 									fill="none"
 									viewBox="0 0 24 24"
 									viewBox="0 0 24 24"
 									stroke-width="1.5"
 									stroke-width="1.5"
 									stroke="currentColor"
 									stroke="currentColor"
-									class="size-4"
+									class="w-4 h-4"
 								>
 								>
 									<path
 									<path
 										stroke-linecap="round"
 										stroke-linecap="round"
 										stroke-linejoin="round"
 										stroke-linejoin="round"
-										d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
+										d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
 									/>
 									/>
 								</svg>
 								</svg>
-							{:else}
-								<svg
-									xmlns="http://www.w3.org/2000/svg"
-									fill="none"
-									viewBox="0 0 24 24"
-									stroke-width="1.5"
-									stroke="currentColor"
-									class="size-4"
-								>
-									<path
-										stroke-linecap="round"
-										stroke-linejoin="round"
-										d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
-									/>
-									<path
-										stroke-linecap="round"
-										stroke-linejoin="round"
-										d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
-									/>
-								</svg>
-							{/if}
-						</button>
-					</Tooltip>
-
-					<Tooltip content={$i18n.t('Delete')}>
-						<button
-							class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-							type="button"
-							on:click={() => {
-								deleteModelHandler(model);
+							</a>
+						{/if}
+
+						<ModelMenu
+							user={$user}
+							{model}
+							shareHandler={() => {
+								shareModelHandler(model);
+							}}
+							cloneHandler={() => {
+								cloneModelHandler(model);
+							}}
+							exportHandler={() => {
+								exportModelHandler(model);
 							}}
 							}}
+							hideHandler={() => {
+								hideModelHandler(model);
+							}}
+							deleteHandler={() => {
+								selectedModel = model;
+								showModelDeleteConfirm = true;
+							}}
+							onClose={() => {}}
 						>
 						>
-							<GarbageBin />
-						</button>
-					</Tooltip>
-				{:else}
-					<a
-						class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-						type="button"
-						href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
-					>
+							<button
+								class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+								type="button"
+							>
+								<EllipsisHorizontal className="size-5" />
+							</button>
+						</ModelMenu>
+
+						<div class="ml-1">
+							<Tooltip content={model.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
+								<Switch
+									bind:state={model.is_active}
+									on:change={async (e) => {
+										toggleModelById(localStorage.token, model.id);
+										_models.set(await getModels(localStorage.token));
+									}}
+								/>
+							</Tooltip>
+						</div>
+					{/if}
+				</div>
+			</div>
+		{/each}
+	</div>
+
+	{#if $user?.role === 'admin'}
+		<div class=" flex justify-end w-full mb-3">
+			<div class="flex space-x-1">
+				<input
+					id="models-import-input"
+					bind:this={modelsImportInputElement}
+					bind:files={importFiles}
+					type="file"
+					accept=".json"
+					hidden
+					on:change={() => {
+						console.log(importFiles);
+
+						let reader = new FileReader();
+						reader.onload = async (event) => {
+							let savedModels = JSON.parse(event.target.result);
+							console.log(savedModels);
+
+							for (const model of savedModels) {
+								if (model?.info ?? false) {
+									if ($_models.find((m) => m.id === model.id)) {
+										await updateModelById(localStorage.token, model.id, model.info).catch(
+											(error) => {
+												return null;
+											}
+										);
+									} else {
+										await createNewModel(localStorage.token, model.info).catch((error) => {
+											return null;
+										});
+									}
+								}
+							}
+
+							await _models.set(await getModels(localStorage.token));
+							models = await getWorkspaceModels(localStorage.token);
+						};
+
+						reader.readAsText(importFiles[0]);
+					}}
+				/>
+
+				<button
+					class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+					on:click={() => {
+						modelsImportInputElement.click();
+					}}
+				>
+					<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Models')}</div>
+
+					<div class=" self-center">
 						<svg
 						<svg
 							xmlns="http://www.w3.org/2000/svg"
 							xmlns="http://www.w3.org/2000/svg"
-							fill="none"
-							viewBox="0 0 24 24"
-							stroke-width="1.5"
-							stroke="currentColor"
-							class="w-4 h-4"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="w-3.5 h-3.5"
 						>
 						>
 							<path
 							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
+								fill-rule="evenodd"
+								d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
+								clip-rule="evenodd"
 							/>
 							/>
 						</svg>
 						</svg>
-					</a>
-
-					<ModelMenu
-						{model}
-						shareHandler={() => {
-							shareModelHandler(model);
-						}}
-						cloneHandler={() => {
-							cloneModelHandler(model);
-						}}
-						exportHandler={() => {
-							exportModelHandler(model);
-						}}
-						moveToTopHandler={() => {
-							moveToTopHandler(model);
-						}}
-						hideHandler={() => {
-							hideModelHandler(model);
-						}}
-						deleteHandler={() => {
-							selectedModel = model;
-							showModelDeleteConfirm = true;
-						}}
-						onClose={() => {}}
-					>
-						<button
-							class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-							type="button"
-						>
-							<EllipsisHorizontal className="size-5" />
-						</button>
-					</ModelMenu>
-				{/if}
-			</div>
-		</div>
-	{/each}
-</div>
-
-<div class=" flex justify-end w-full mb-3">
-	<div class="flex space-x-1">
-		<input
-			id="models-import-input"
-			bind:this={modelsImportInputElement}
-			bind:files={importFiles}
-			type="file"
-			accept=".json"
-			hidden
-			on:change={() => {
-				console.log(importFiles);
-
-				let reader = new FileReader();
-				reader.onload = async (event) => {
-					let savedModels = JSON.parse(event.target.result);
-					console.log(savedModels);
-
-					for (const model of savedModels) {
-						if (model?.info ?? false) {
-							if ($models.find((m) => m.id === model.id)) {
-								await updateModelById(localStorage.token, model.id, model.info).catch((error) => {
-									return null;
-								});
-							} else {
-								await addNewModel(localStorage.token, model.info).catch((error) => {
-									return null;
-								});
-							}
-						}
-					}
-
-					await models.set(await getModels(localStorage.token));
-					_models = $models;
-				};
-
-				reader.readAsText(importFiles[0]);
-			}}
-		/>
-
-		<button
-			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
-			on:click={() => {
-				modelsImportInputElement.click();
-			}}
-		>
-			<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Models')}</div>
-
-			<div class=" self-center">
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 16 16"
-					fill="currentColor"
-					class="w-3.5 h-3.5"
-				>
-					<path
-						fill-rule="evenodd"
-						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
-						clip-rule="evenodd"
-					/>
-				</svg>
-			</div>
-		</button>
-
-		<button
-			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
-			on:click={async () => {
-				downloadModels($models);
-			}}
-		>
-			<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Models')}</div>
-
-			<div class=" self-center">
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 16 16"
-					fill="currentColor"
-					class="w-3.5 h-3.5"
-				>
-					<path
-						fill-rule="evenodd"
-						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
-						clip-rule="evenodd"
-					/>
-				</svg>
-			</div>
-		</button>
-	</div>
-
-	{#if localModelfiles.length > 0}
-		<div class="flex">
-			<div class=" self-center text-sm font-medium mr-4">
-				{localModelfiles.length} Local Modelfiles Detected
-			</div>
+					</div>
+				</button>
 
 
-			<div class="flex space-x-1">
 				<button
 				<button
-					class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
+					class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
 					on:click={async () => {
 					on:click={async () => {
-						downloadModels(localModelfiles);
-
-						localStorage.removeItem('modelfiles');
-						localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
+						downloadModels($_models);
 					}}
 					}}
 				>
 				>
+					<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Models')}</div>
+
 					<div class=" self-center">
 					<div class=" self-center">
 						<svg
 						<svg
 							xmlns="http://www.w3.org/2000/svg"
 							xmlns="http://www.w3.org/2000/svg"
-							fill="none"
-							viewBox="0 0 24 24"
-							stroke-width="1.5"
-							stroke="currentColor"
-							class="w-4 h-4"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="w-3.5 h-3.5"
 						>
 						>
 							<path
 							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
+								fill-rule="evenodd"
+								d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
+								clip-rule="evenodd"
 							/>
 							/>
 						</svg>
 						</svg>
 					</div>
 					</div>
@@ -656,44 +464,35 @@
 			</div>
 			</div>
 		</div>
 		</div>
 	{/if}
 	{/if}
-</div>
 
 
-{#if $config?.features.enable_community_sharing}
-	<div class=" my-16">
-		<div class=" text-lg font-semibold mb-3 line-clamp-1">
-			{$i18n.t('Made by OpenWebUI Community')}
-		</div>
+	{#if $config?.features.enable_community_sharing}
+		<div class=" my-16">
+			<div class=" text-lg font-semibold mb-0.5 line-clamp-1">
+				{$i18n.t('Made by OpenWebUI Community')}
+			</div>
 
 
-		<a
-			class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
-			href="https://openwebui.com/#open-webui-community"
-			target="_blank"
-		>
-			<div class=" self-center w-10 flex-shrink-0">
-				<div
-					class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
-				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 24 24"
-						fill="currentColor"
-						class="w-6"
-					>
-						<path
-							fill-rule="evenodd"
-							d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-							clip-rule="evenodd"
-						/>
-					</svg>
+			<a
+				class=" flex cursor-pointer items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-850 w-full mb-2 px-3.5 py-1.5 rounded-xl transition"
+				href="https://openwebui.com/#open-webui-community"
+				target="_blank"
+			>
+				<div class=" self-center">
+					<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a model')}</div>
+					<div class=" text-sm line-clamp-1">
+						{$i18n.t('Discover, download, and explore model presets')}
+					</div>
 				</div>
 				</div>
-			</div>
 
 
-			<div class=" self-center">
-				<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a model')}</div>
-				<div class=" text-sm line-clamp-1">
-					{$i18n.t('Discover, download, and explore model presets')}
+				<div>
+					<div>
+						<ChevronRight />
+					</div>
 				</div>
 				</div>
-			</div>
-		</a>
+			</a>
+		</div>
+	{/if}
+{:else}
+	<div class="w-full h-full flex justify-center items-center">
+		<Spinner />
 	</div>
 	</div>
 {/if}
 {/if}

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

@@ -53,7 +53,7 @@
 				}}
 				}}
 			>
 			>
 				<button
 				<button
-					class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
+					class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-850 rounded-3xl"
 					type="button">{$i18n.t('Select Knowledge')}</button
 					type="button">{$i18n.t('Select Knowledge')}</button
 				>
 				>
 			</Selector>
 			</Selector>

+ 94 - 57
src/lib/components/workspace/Models/ModelEditor.svelte

@@ -1,10 +1,6 @@
 <script lang="ts">
 <script lang="ts">
-	import { v4 as uuidv4 } from 'uuid';
-	import { toast } from 'svelte-sonner';
-	import { goto } from '$app/navigation';
-
 	import { onMount, getContext, tick } from 'svelte';
 	import { onMount, getContext, tick } from 'svelte';
-	import { models, tools, functions, knowledge as knowledgeCollections } from '$lib/stores';
+	import { models, tools, functions, knowledge as knowledgeCollections, user } from '$lib/stores';
 
 
 	import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
 	import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
 	import Tags from '$lib/components/common/Tags.svelte';
 	import Tags from '$lib/components/common/Tags.svelte';
@@ -16,14 +12,20 @@
 	import Textarea from '$lib/components/common/Textarea.svelte';
 	import Textarea from '$lib/components/common/Textarea.svelte';
 	import { getTools } from '$lib/apis/tools';
 	import { getTools } from '$lib/apis/tools';
 	import { getFunctions } from '$lib/apis/functions';
 	import { getFunctions } from '$lib/apis/functions';
-	import { getKnowledgeItems } from '$lib/apis/knowledge';
+	import { getKnowledgeBases } from '$lib/apis/knowledge';
+	import AccessControl from '../common/AccessControl.svelte';
+	import { stringify } from 'postcss';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	export let onSubmit: Function;
 	export let onSubmit: Function;
+	export let onBack: null | Function = null;
+
 	export let model = null;
 	export let model = null;
 	export let edit = false;
 	export let edit = false;
 
 
+	export let preset = true;
+
 	let loading = false;
 	let loading = false;
 	let success = false;
 	let success = false;
 
 
@@ -77,12 +79,14 @@
 	let filterIds = [];
 	let filterIds = [];
 	let actionIds = [];
 	let actionIds = [];
 
 
+	let accessControl = null;
+
 	const addUsage = (base_model_id) => {
 	const addUsage = (base_model_id) => {
 		const baseModel = $models.find((m) => m.id === base_model_id);
 		const baseModel = $models.find((m) => m.id === base_model_id);
 
 
 		if (baseModel) {
 		if (baseModel) {
 			if (baseModel.owned_by === 'openai') {
 			if (baseModel.owned_by === 'openai') {
-				capabilities.usage = baseModel.info?.meta?.capabilities?.usage ?? false;
+				capabilities.usage = baseModel?.meta?.capabilities?.usage ?? false;
 			} else {
 			} else {
 				delete capabilities.usage;
 				delete capabilities.usage;
 			}
 			}
@@ -95,6 +99,8 @@
 
 
 		info.id = id;
 		info.id = id;
 		info.name = name;
 		info.name = name;
+
+		info.access_control = accessControl;
 		info.meta.capabilities = capabilities;
 		info.meta.capabilities = capabilities;
 
 
 		if (knowledge.length > 0) {
 		if (knowledge.length > 0) {
@@ -145,7 +151,7 @@
 	onMount(async () => {
 	onMount(async () => {
 		await tools.set(await getTools(localStorage.token));
 		await tools.set(await getTools(localStorage.token));
 		await functions.set(await getFunctions(localStorage.token));
 		await functions.set(await getFunctions(localStorage.token));
-		await knowledgeCollections.set(await getKnowledgeItems(localStorage.token));
+		await knowledgeCollections.set(await getKnowledgeBases(localStorage.token));
 
 
 		// Scroll to top 'workspace-container' element
 		// Scroll to top 'workspace-container' element
 		const workspaceContainer = document.getElementById('workspace-container');
 		const workspaceContainer = document.getElementById('workspace-container');
@@ -154,38 +160,37 @@
 		}
 		}
 
 
 		if (model) {
 		if (model) {
+			console.log(model);
 			name = model.name;
 			name = model.name;
 			await tick();
 			await tick();
 
 
 			id = model.id;
 			id = model.id;
 
 
-			if (model.info.base_model_id) {
+			if (model.base_model_id) {
 				const base_model = $models
 				const base_model = $models
-					.filter((m) => !m?.preset && m?.owned_by !== 'arena')
-					.find((m) =>
-						[model.info.base_model_id, `${model.info.base_model_id}:latest`].includes(m.id)
-					);
+					.filter((m) => !m?.preset && !(m?.arena ?? false))
+					.find((m) => [model.base_model_id, `${model.base_model_id}:latest`].includes(m.id));
 
 
 				console.log('base_model', base_model);
 				console.log('base_model', base_model);
 
 
 				if (base_model) {
 				if (base_model) {
-					model.info.base_model_id = base_model.id;
+					model.base_model_id = base_model.id;
 				} else {
 				} else {
-					model.info.base_model_id = null;
+					model.base_model_id = null;
 				}
 				}
 			}
 			}
 
 
-			params = { ...params, ...model?.info?.params };
+			params = { ...params, ...model?.params };
 			params.stop = params?.stop
 			params.stop = params?.stop
 				? (typeof params.stop === 'string' ? params.stop.split(',') : (params?.stop ?? [])).join(
 				? (typeof params.stop === 'string' ? params.stop.split(',') : (params?.stop ?? [])).join(
 						','
 						','
 					)
 					)
 				: null;
 				: null;
 
 
-			toolIds = model?.info?.meta?.toolIds ?? [];
-			filterIds = model?.info?.meta?.filterIds ?? [];
-			actionIds = model?.info?.meta?.actionIds ?? [];
-			knowledge = (model?.info?.meta?.knowledge ?? []).map((item) => {
+			toolIds = model?.meta?.toolIds ?? [];
+			filterIds = model?.meta?.filterIds ?? [];
+			actionIds = model?.meta?.actionIds ?? [];
+			knowledge = (model?.meta?.knowledge ?? []).map((item) => {
 				if (item?.collection_name) {
 				if (item?.collection_name) {
 					return {
 					return {
 						id: item.collection_name,
 						id: item.collection_name,
@@ -203,17 +208,22 @@
 					return item;
 					return item;
 				}
 				}
 			});
 			});
-			capabilities = { ...capabilities, ...(model?.info?.meta?.capabilities ?? {}) };
+			capabilities = { ...capabilities, ...(model?.meta?.capabilities ?? {}) };
 			if (model?.owned_by === 'openai') {
 			if (model?.owned_by === 'openai') {
 				capabilities.usage = false;
 				capabilities.usage = false;
 			}
 			}
 
 
+			accessControl = model?.access_control ?? null;
+
+			console.log(model?.access_control);
+			console.log(accessControl);
+
 			info = {
 			info = {
 				...info,
 				...info,
 				...JSON.parse(
 				...JSON.parse(
 					JSON.stringify(
 					JSON.stringify(
-						model?.info
-							? model?.info
+						model
+							? model
 							: {
 							: {
 									id: model.id,
 									id: model.id,
 									name: model.name
 									name: model.name
@@ -230,6 +240,31 @@
 </script>
 </script>
 
 
 {#if loaded}
 {#if loaded}
+	{#if onBack}
+		<button
+			class="flex space-x-1"
+			on:click={() => {
+				onBack();
+			}}
+		>
+			<div class=" self-center">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="h-4 w-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+			<div class=" self-center text-sm font-medium">{'Back'}</div>
+		</button>
+	{/if}
+
 	<div class="w-full max-h-full flex justify-center">
 	<div class="w-full max-h-full flex justify-center">
 		<input
 		<input
 			bind:this={filesInputElement}
 			bind:this={filesInputElement}
@@ -298,7 +333,7 @@
 			}}
 			}}
 		/>
 		/>
 
 
-		{#if !edit || model}
+		{#if !edit || (edit && model)}
 			<form
 			<form
 				class="flex flex-col md:flex-row w-full gap-3 md:gap-6"
 				class="flex flex-col md:flex-row w-full gap-3 md:gap-6"
 				on:submit|preventDefault={() => {
 				on:submit|preventDefault={() => {
@@ -308,7 +343,7 @@
 				<div class="self-center md:self-start flex justify-center my-2 flex-shrink-0">
 				<div class="self-center md:self-start flex justify-center my-2 flex-shrink-0">
 					<div class="self-center">
 					<div class="self-center">
 						<button
 						<button
-							class="rounded-2xl flex flex-shrink-0 items-center bg-white shadow-2xl group relative"
+							class="rounded-2xl flex flex-shrink-0 items-center bg-white shadow-xl group relative"
 							type="button"
 							type="button"
 							on:click={() => {
 							on:click={() => {
 								filesInputElement.click();
 								filesInputElement.click();
@@ -318,13 +353,13 @@
 								<img
 								<img
 									src={info.meta.profile_image_url}
 									src={info.meta.profile_image_url}
 									alt="model profile"
 									alt="model profile"
-									class="rounded-lg size-72 md:size-64 object-cover shrink-0"
+									class="rounded-lg size-72 md:size-60 object-cover shrink-0"
 								/>
 								/>
 							{:else}
 							{:else}
 								<img
 								<img
 									src="/static/favicon.png"
 									src="/static/favicon.png"
 									alt="model profile"
 									alt="model profile"
-									class=" rounded-lg size-72 md:size-64 object-cover shrink-0"
+									class=" rounded-lg size-72 md:size-60 object-cover shrink-0"
 								/>
 								/>
 							{/if}
 							{/if}
 
 
@@ -383,7 +418,7 @@
 						</div>
 						</div>
 					</div>
 					</div>
 
 
-					{#if !edit || model.preset}
+					{#if preset}
 						<div class="my-1">
 						<div class="my-1">
 							<div class=" text-sm font-semibold mb-1">{$i18n.t('Base Model (From)')}</div>
 							<div class=" text-sm font-semibold mb-1">{$i18n.t('Base Model (From)')}</div>
 
 
@@ -441,7 +476,33 @@
 						{/if}
 						{/if}
 					</div>
 					</div>
 
 
-					<hr class=" dark:border-gray-850 my-1.5" />
+					<div class="my-1">
+						<div class="">
+							<Tags
+								tags={info?.meta?.tags ?? []}
+								on:delete={(e) => {
+									const tagName = e.detail;
+									info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
+								}}
+								on:add={(e) => {
+									const tagName = e.detail;
+									if (!(info?.meta?.tags ?? null)) {
+										info.meta.tags = [{ name: tagName }];
+									} else {
+										info.meta.tags = [...info.meta.tags, { name: tagName }];
+									}
+								}}
+							/>
+						</div>
+					</div>
+
+					<div class="my-2">
+						<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
+							<AccessControl bind:accessControl />
+						</div>
+					</div>
+
+					<hr class=" border-gray-50 dark:border-gray-850 my-1.5" />
 
 
 					<div class="my-2">
 					<div class="my-2">
 						<div class="flex w-full justify-between">
 						<div class="flex w-full justify-between">
@@ -495,7 +556,7 @@
 						</div>
 						</div>
 					</div>
 					</div>
 
 
-					<hr class=" dark:border-gray-850 my-1" />
+					<hr class=" border-gray-50 dark:border-gray-850 my-1" />
 
 
 					<div class="my-2">
 					<div class="my-2">
 						<div class="flex w-full justify-between items-center">
 						<div class="flex w-full justify-between items-center">
@@ -592,7 +653,7 @@
 						{/if}
 						{/if}
 					</div>
 					</div>
 
 
-					<hr class=" dark:border-gray-850 my-1.5" />
+					<hr class=" border-gray-50 dark:border-gray-850 my-1.5" />
 
 
 					<div class="my-2">
 					<div class="my-2">
 						<Knowledge bind:selectedKnowledge={knowledge} collections={$knowledgeCollections} />
 						<Knowledge bind:selectedKnowledge={knowledge} collections={$knowledgeCollections} />
@@ -620,30 +681,6 @@
 						<Capabilities bind:capabilities />
 						<Capabilities bind:capabilities />
 					</div>
 					</div>
 
 
-					<div class="my-1">
-						<div class="flex w-full justify-between items-center">
-							<div class=" self-center text-sm font-semibold">{$i18n.t('Tags')}</div>
-						</div>
-
-						<div class="mt-2">
-							<Tags
-								tags={info?.meta?.tags ?? []}
-								on:delete={(e) => {
-									const tagName = e.detail;
-									info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
-								}}
-								on:add={(e) => {
-									const tagName = e.detail;
-									if (!(info?.meta?.tags ?? null)) {
-										info.meta.tags = [{ name: tagName }];
-									} else {
-										info.meta.tags = [...info.meta.tags, { name: tagName }];
-									}
-								}}
-							/>
-						</div>
-					</div>
-
 					<div class="my-2 text-gray-300 dark:text-gray-700">
 					<div class="my-2 text-gray-300 dark:text-gray-700">
 						<div class="flex w-full justify-between mb-2">
 						<div class="flex w-full justify-between mb-2">
 							<div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>
 							<div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>
@@ -679,8 +716,8 @@
 					<div class="my-2 flex justify-end pb-20">
 					<div class="my-2 flex justify-end pb-20">
 						<button
 						<button
 							class=" text-sm px-3 py-2 transition rounded-lg {loading
 							class=" text-sm px-3 py-2 transition rounded-lg {loading
-								? ' cursor-not-allowed bg-white hover:bg-gray-100 text-black'
-								: ' bg-white hover:bg-gray-100 text-black'} flex w-full justify-center"
+								? ' cursor-not-allowed bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'
+								: 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
 							type="submit"
 							type="submit"
 							disabled={loading}
 							disabled={loading}
 						>
 						>

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

@@ -16,13 +16,13 @@
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
+	export let user;
 	export let model;
 	export let model;
 
 
 	export let shareHandler: Function;
 	export let shareHandler: Function;
 	export let cloneHandler: Function;
 	export let cloneHandler: Function;
 	export let exportHandler: Function;
 	export let exportHandler: Function;
 
 
-	export let moveToTopHandler: Function;
 	export let hideHandler: Function;
 	export let hideHandler: Function;
 	export let deleteHandler: Function;
 	export let deleteHandler: Function;
 	export let onClose: Function;
 	export let onClose: Function;
@@ -82,69 +82,6 @@
 				<div class="flex items-center">{$i18n.t('Export')}</div>
 				<div class="flex items-center">{$i18n.t('Export')}</div>
 			</DropdownMenu.Item>
 			</DropdownMenu.Item>
 
 
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-				on:click={() => {
-					moveToTopHandler();
-				}}
-			>
-				<ArrowUpCircle />
-
-				<div class="flex items-center">{$i18n.t('Move to Top')}</div>
-			</DropdownMenu.Item>
-
-			<DropdownMenu.Item
-				class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-				on:click={() => {
-					hideHandler();
-				}}
-			>
-				{#if model?.info?.meta?.hidden ?? false}
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						fill="none"
-						viewBox="0 0 24 24"
-						stroke-width="1.5"
-						stroke="currentColor"
-						class="size-4"
-					>
-						<path
-							stroke-linecap="round"
-							stroke-linejoin="round"
-							d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
-						/>
-					</svg>
-				{:else}
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						fill="none"
-						viewBox="0 0 24 24"
-						stroke-width="1.5"
-						stroke="currentColor"
-						class="size-4"
-					>
-						<path
-							stroke-linecap="round"
-							stroke-linejoin="round"
-							d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
-						/>
-						<path
-							stroke-linecap="round"
-							stroke-linejoin="round"
-							d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
-						/>
-					</svg>
-				{/if}
-
-				<div class="flex items-center">
-					{#if model?.info?.meta?.hidden ?? false}
-						{$i18n.t('Show Model')}
-					{:else}
-						{$i18n.t('Hide Model')}
-					{/if}
-				</div>
-			</DropdownMenu.Item>
-
 			<hr class="border-gray-100 dark:border-gray-800 my-1" />
 			<hr class="border-gray-100 dark:border-gray-800 my-1" />
 
 
 			<DropdownMenu.Item
 			<DropdownMenu.Item

+ 240 - 231
src/lib/components/workspace/Prompts.svelte

@@ -3,28 +3,39 @@
 	import fileSaver from 'file-saver';
 	import fileSaver from 'file-saver';
 	const { saveAs } = fileSaver;
 	const { saveAs } = fileSaver;
 
 
-	import { onMount, getContext } from 'svelte';
-	import { WEBUI_NAME, config, prompts } from '$lib/stores';
-	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
-	import { error } from '@sveltejs/kit';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
+	import { onMount, getContext } from 'svelte';
+	import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
+
+	import {
+		createNewPrompt,
+		deletePromptByCommand,
+		getPrompts,
+		getPromptList
+	} from '$lib/apis/prompts';
+
 	import PromptMenu from './Prompts/PromptMenu.svelte';
 	import PromptMenu from './Prompts/PromptMenu.svelte';
 	import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
 	import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import Search from '../icons/Search.svelte';
 	import Search from '../icons/Search.svelte';
 	import Plus from '../icons/Plus.svelte';
 	import Plus from '../icons/Plus.svelte';
+	import ChevronRight from '../icons/ChevronRight.svelte';
+	import Spinner from '../common/Spinner.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
+	let promptsImportInputElement: HTMLInputElement;
+	let loaded = false;
 
 
 	let importFiles = '';
 	let importFiles = '';
 	let query = '';
 	let query = '';
-	let promptsImportInputElement: HTMLInputElement;
+
+	let prompts = [];
 
 
 	let showDeleteConfirm = false;
 	let showDeleteConfirm = false;
 	let deletePrompt = null;
 	let deletePrompt = null;
 
 
 	let filteredItems = [];
 	let filteredItems = [];
-	$: filteredItems = $prompts.filter((p) => query === '' || p.command.includes(query));
+	$: filteredItems = prompts.filter((p) => query === '' || p.command.includes(query));
 
 
 	const shareHandler = async (prompt) => {
 	const shareHandler = async (prompt) => {
 		toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
 		toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
@@ -59,8 +70,18 @@
 	const deleteHandler = async (prompt) => {
 	const deleteHandler = async (prompt) => {
 		const command = prompt.command;
 		const command = prompt.command;
 		await deletePromptByCommand(localStorage.token, command);
 		await deletePromptByCommand(localStorage.token, command);
-		await prompts.set(await getPrompts(localStorage.token));
+		await init();
 	};
 	};
+
+	const init = async () => {
+		prompts = await getPromptList(localStorage.token);
+		await _prompts.set(await getPrompts(localStorage.token));
+	};
+
+	onMount(async () => {
+		await init();
+		loaded = true;
+	});
 </script>
 </script>
 
 
 <svelte:head>
 <svelte:head>
@@ -69,251 +90,239 @@
 	</title>
 	</title>
 </svelte:head>
 </svelte:head>
 
 
-<DeleteConfirmDialog
-	bind:show={showDeleteConfirm}
-	title={$i18n.t('Delete prompt?')}
-	on:confirm={() => {
-		deleteHandler(deletePrompt);
-	}}
->
-	<div class=" text-sm text-gray-500">
-		{$i18n.t('This will delete')} <span class="  font-semibold">{deletePrompt.command}</span>.
-	</div>
-</DeleteConfirmDialog>
-
-<div class="flex flex-col gap-1 mt-1.5 mb-2">
-	<div class="flex justify-between items-center">
-		<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
-			{$i18n.t('Prompts')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
-			<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
-				>{filteredItems.length}</span
-			>
+{#if loaded}
+	<DeleteConfirmDialog
+		bind:show={showDeleteConfirm}
+		title={$i18n.t('Delete prompt?')}
+		on:confirm={() => {
+			deleteHandler(deletePrompt);
+		}}
+	>
+		<div class=" text-sm text-gray-500">
+			{$i18n.t('This will delete')} <span class="  font-semibold">{deletePrompt.command}</span>.
 		</div>
 		</div>
-	</div>
+	</DeleteConfirmDialog>
 
 
-	<div class=" flex w-full space-x-2">
-		<div class="flex flex-1">
-			<div class=" self-center ml-1 mr-3">
-				<Search className="size-3.5" />
+	<div class="flex flex-col gap-1 mt-1.5 mb-2">
+		<div class="flex justify-between items-center">
+			<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
+				{$i18n.t('Prompts')}
+				<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
+				<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
+					>{filteredItems.length}</span
+				>
 			</div>
 			</div>
-			<input
-				class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
-				bind:value={query}
-				placeholder={$i18n.t('Search Prompts')}
-			/>
 		</div>
 		</div>
 
 
-		<div>
-			<a
-				class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
-				href="/workspace/prompts/create"
-			>
-				<Plus className="size-3.5" />
-			</a>
-		</div>
-	</div>
-</div>
-
-<div class="mb-5">
-	{#each filteredItems as prompt}
-		<div
-			class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
-		>
-			<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
-				<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
-					<div class=" flex-1 self-center pl-1.5">
-						<div class=" font-semibold line-clamp-1">{prompt.command}</div>
-						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
-							{prompt.title}
-						</div>
-					</div>
-				</a>
+		<div class=" flex w-full space-x-2">
+			<div class="flex flex-1">
+				<div class=" self-center ml-1 mr-3">
+					<Search className="size-3.5" />
+				</div>
+				<input
+					class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+					bind:value={query}
+					placeholder={$i18n.t('Search Prompts')}
+				/>
 			</div>
 			</div>
-			<div class="flex flex-row gap-0.5 self-center">
+
+			<div>
 				<a
 				<a
-					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-					type="button"
-					href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}
+					class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
+					href="/workspace/prompts/create"
 				>
 				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						fill="none"
-						viewBox="0 0 24 24"
-						stroke-width="1.5"
-						stroke="currentColor"
-						class="w-4 h-4"
-					>
-						<path
-							stroke-linecap="round"
-							stroke-linejoin="round"
-							d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
-						/>
-					</svg>
+					<Plus className="size-3.5" />
 				</a>
 				</a>
+			</div>
+		</div>
+	</div>
 
 
-				<PromptMenu
-					shareHandler={() => {
-						shareHandler(prompt);
-					}}
-					cloneHandler={() => {
-						cloneHandler(prompt);
-					}}
-					exportHandler={() => {
-						exportHandler(prompt);
-					}}
-					deleteHandler={async () => {
-						deletePrompt = prompt;
-						showDeleteConfirm = true;
-					}}
-					onClose={() => {}}
-				>
-					<button
-						class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+	<div class="mb-5">
+		{#each filteredItems as prompt}
+			<div
+				class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
+			>
+				<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
+					<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
+						<div class=" flex-1 self-center pl-1.5">
+							<div class=" font-semibold line-clamp-1">{prompt.command}</div>
+							<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
+								{prompt.title}
+							</div>
+						</div>
+					</a>
+				</div>
+				<div class="flex flex-row gap-0.5 self-center">
+					<a
+						class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 						type="button"
 						type="button"
+						href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}
 					>
 					>
-						<EllipsisHorizontal className="size-5" />
-					</button>
-				</PromptMenu>
-			</div>
-		</div>
-	{/each}
-</div>
-
-<div class=" flex justify-end w-full mb-3">
-	<div class="flex space-x-2">
-		<input
-			id="prompts-import-input"
-			bind:this={promptsImportInputElement}
-			bind:files={importFiles}
-			type="file"
-			accept=".json"
-			hidden
-			on:change={() => {
-				console.log(importFiles);
-
-				const reader = new FileReader();
-				reader.onload = async (event) => {
-					const savedPrompts = JSON.parse(event.target.result);
-					console.log(savedPrompts);
-
-					for (const prompt of savedPrompts) {
-						await createNewPrompt(
-							localStorage.token,
-							prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command,
-							prompt.title,
-							prompt.content
-						).catch((error) => {
-							toast.error(error);
-							return null;
-						});
-					}
-
-					await prompts.set(await getPrompts(localStorage.token));
-				};
-
-				reader.readAsText(importFiles[0]);
-			}}
-		/>
-
-		<button
-			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
-			on:click={() => {
-				promptsImportInputElement.click();
-			}}
-		>
-			<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Prompts')}</div>
-
-			<div class=" self-center">
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 16 16"
-					fill="currentColor"
-					class="w-4 h-4"
-				>
-					<path
-						fill-rule="evenodd"
-						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
-						clip-rule="evenodd"
-					/>
-				</svg>
-			</div>
-		</button>
-
-		<button
-			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
-			on:click={async () => {
-				// promptsImportInputElement.click();
-				let blob = new Blob([JSON.stringify($prompts)], {
-					type: 'application/json'
-				});
-				saveAs(blob, `prompts-export-${Date.now()}.json`);
-			}}
-		>
-			<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Prompts')}</div>
-
-			<div class=" self-center">
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 16 16"
-					fill="currentColor"
-					class="w-4 h-4"
-				>
-					<path
-						fill-rule="evenodd"
-						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
-						clip-rule="evenodd"
-					/>
-				</svg>
-			</div>
-		</button>
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
+							/>
+						</svg>
+					</a>
 
 
-		<!-- <button
-						on:click={() => {
-							loadDefaultPrompts();
+					<PromptMenu
+						shareHandler={() => {
+							shareHandler(prompt);
+						}}
+						cloneHandler={() => {
+							cloneHandler(prompt);
 						}}
 						}}
+						exportHandler={() => {
+							exportHandler(prompt);
+						}}
+						deleteHandler={async () => {
+							deletePrompt = prompt;
+							showDeleteConfirm = true;
+						}}
+						onClose={() => {}}
 					>
 					>
-						dd
-					</button> -->
+						<button
+							class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+							type="button"
+						>
+							<EllipsisHorizontal className="size-5" />
+						</button>
+					</PromptMenu>
+				</div>
+			</div>
+		{/each}
 	</div>
 	</div>
-</div>
 
 
-{#if $config?.features.enable_community_sharing}
-	<div class=" my-16">
-		<div class=" text-lg font-semibold mb-3 line-clamp-1">
-			{$i18n.t('Made by OpenWebUI Community')}
-		</div>
+	{#if $user?.role === 'admin'}
+		<div class=" flex justify-end w-full mb-3">
+			<div class="flex space-x-2">
+				<input
+					id="prompts-import-input"
+					bind:this={promptsImportInputElement}
+					bind:files={importFiles}
+					type="file"
+					accept=".json"
+					hidden
+					on:change={() => {
+						console.log(importFiles);
 
 
-		<a
-			class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
-			href="https://openwebui.com/#open-webui-community"
-			target="_blank"
-		>
-			<div class=" self-center w-10 flex-shrink-0">
-				<div
-					class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
+						const reader = new FileReader();
+						reader.onload = async (event) => {
+							const savedPrompts = JSON.parse(event.target.result);
+							console.log(savedPrompts);
+
+							for (const prompt of savedPrompts) {
+								await createNewPrompt(
+									localStorage.token,
+									prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command,
+									prompt.title,
+									prompt.content
+								).catch((error) => {
+									toast.error(error);
+									return null;
+								});
+							}
+
+							prompts = await getPromptList(localStorage.token);
+							await _prompts.set(await getPrompts(localStorage.token));
+						};
+
+						reader.readAsText(importFiles[0]);
+					}}
+				/>
+
+				<button
+					class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+					on:click={() => {
+						promptsImportInputElement.click();
+					}}
 				>
 				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 24 24"
-						fill="currentColor"
-						class="w-6"
-					>
-						<path
-							fill-rule="evenodd"
-							d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-							clip-rule="evenodd"
-						/>
-					</svg>
-				</div>
+					<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Prompts')}</div>
+
+					<div class=" self-center">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+				</button>
+
+				<button
+					class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+					on:click={async () => {
+						// promptsImportInputElement.click();
+						let blob = new Blob([JSON.stringify(prompts)], {
+							type: 'application/json'
+						});
+						saveAs(blob, `prompts-export-${Date.now()}.json`);
+					}}
+				>
+					<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Prompts')}</div>
+
+					<div class=" self-center">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+				</button>
 			</div>
 			</div>
+		</div>
+	{/if}
 
 
-			<div class=" self-center">
-				<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
-				<div class=" text-sm line-clamp-1">
-					{$i18n.t('Discover, download, and explore custom prompts')}
-				</div>
+	{#if $config?.features.enable_community_sharing}
+		<div class=" my-16">
+			<div class=" text-lg font-semibold mb-0.5 line-clamp-1">
+				{$i18n.t('Made by OpenWebUI Community')}
 			</div>
 			</div>
-		</a>
+
+			<a
+				class=" flex cursor-pointer items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-850 w-full mb-2 px-3.5 py-1.5 rounded-xl transition"
+				href="https://openwebui.com/#open-webui-community"
+				target="_blank"
+			>
+				<div class=" self-center">
+					<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
+					<div class=" text-sm line-clamp-1">
+						{$i18n.t('Discover, download, and explore custom prompts')}
+					</div>
+				</div>
+
+				<div>
+					<div>
+						<ChevronRight />
+					</div>
+				</div>
+			</a>
+		</div>
+	{/if}
+{:else}
+	<div class="w-full h-full flex justify-center items-center">
+		<Spinner />
 	</div>
 	</div>
 {/if}
 {/if}

+ 32 - 4
src/lib/components/workspace/Prompts/PromptEditor.svelte

@@ -4,6 +4,9 @@
 	import Textarea from '$lib/components/common/Textarea.svelte';
 	import Textarea from '$lib/components/common/Textarea.svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import AccessControl from '../common/AccessControl.svelte';
+	import LockClosed from '$lib/components/icons/LockClosed.svelte';
+	import AccessControlModal from '../common/AccessControlModal.svelte';
 
 
 	export let onSubmit: Function;
 	export let onSubmit: Function;
 	export let edit = false;
 	export let edit = false;
@@ -17,6 +20,10 @@
 	let command = '';
 	let command = '';
 	let content = '';
 	let content = '';
 
 
+	let accessControl = null;
+
+	let showAccessControlModal = false;
+
 	$: if (!edit) {
 	$: if (!edit) {
 		command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : '';
 		command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : '';
 	}
 	}
@@ -28,7 +35,8 @@
 			await onSubmit({
 			await onSubmit({
 				title,
 				title,
 				command,
 				command,
-				content
+				content,
+				access_control: accessControl
 			});
 			});
 		} else {
 		} else {
 			toast.error(
 			toast.error(
@@ -54,10 +62,14 @@
 
 
 			command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command;
 			command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command;
 			content = prompt.content;
 			content = prompt.content;
+
+			accessControl = prompt?.access_control ?? null;
 		}
 		}
 	});
 	});
 </script>
 </script>
 
 
+<AccessControlModal bind:show={showAccessControlModal} bind:accessControl />
+
 <div class="w-full max-h-full flex justify-center">
 <div class="w-full max-h-full flex justify-center">
 	<form
 	<form
 		class="flex flex-col w-full mb-10"
 		class="flex flex-col w-full mb-10"
@@ -76,13 +88,29 @@
 				placement="bottom-start"
 				placement="bottom-start"
 			>
 			>
 				<div class="flex flex-col w-full">
 				<div class="flex flex-col w-full">
-					<div>
+					<div class="flex items-center">
 						<input
 						<input
 							class="text-2xl font-semibold w-full bg-transparent outline-none"
 							class="text-2xl font-semibold w-full bg-transparent outline-none"
 							placeholder={$i18n.t('Title')}
 							placeholder={$i18n.t('Title')}
 							bind:value={title}
 							bind:value={title}
 							required
 							required
 						/>
 						/>
+
+						<div>
+							<button
+								class="bg-gray-50 hover:bg-gray-100 text-black transition px-2 py-1 rounded-full flex gap-1 items-center"
+								type="button"
+								on:click={() => {
+									showAccessControlModal = true;
+								}}
+							>
+								<LockClosed strokeWidth="2.5" className="size-3.5" />
+
+								<div class="text-sm font-medium flex-shrink-0">
+									{$i18n.t('Share')}
+								</div>
+							</button>
+						</div>
 					</div>
 					</div>
 
 
 					<div class="flex gap-0.5 items-center text-xs text-gray-500">
 					<div class="flex gap-0.5 items-center text-xs text-gray-500">
@@ -138,8 +166,8 @@
 		<div class="my-4 flex justify-end pb-20">
 		<div class="my-4 flex justify-end pb-20">
 			<button
 			<button
 				class=" text-sm w-full lg:w-fit px-4 py-2 transition rounded-lg {loading
 				class=" text-sm w-full lg:w-fit px-4 py-2 transition rounded-lg {loading
-					? ' cursor-not-allowed bg-white hover:bg-gray-100 text-black'
-					: ' bg-white hover:bg-gray-100 text-black'} flex justify-center"
+					? ' cursor-not-allowed bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'
+					: 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
 				type="submit"
 				type="submit"
 				disabled={loading}
 				disabled={loading}
 			>
 			>

+ 306 - 302
src/lib/components/workspace/Tools.svelte

@@ -4,7 +4,7 @@
 	const { saveAs } = fileSaver;
 	const { saveAs } = fileSaver;
 
 
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
-	import { WEBUI_NAME, config, prompts, tools } from '$lib/stores';
+	import { WEBUI_NAME, config, prompts, tools as _tools, user } from '$lib/stores';
 	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
 	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
 
 
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
@@ -13,6 +13,7 @@
 		deleteToolById,
 		deleteToolById,
 		exportTools,
 		exportTools,
 		getToolById,
 		getToolById,
+		getToolList,
 		getTools
 		getTools
 	} from '$lib/apis/tools';
 	} from '$lib/apis/tools';
 	import ArrowDownTray from '../icons/ArrowDownTray.svelte';
 	import ArrowDownTray from '../icons/ArrowDownTray.svelte';
@@ -27,10 +28,13 @@
 	import GarbageBin from '../icons/GarbageBin.svelte';
 	import GarbageBin from '../icons/GarbageBin.svelte';
 	import Search from '../icons/Search.svelte';
 	import Search from '../icons/Search.svelte';
 	import Plus from '../icons/Plus.svelte';
 	import Plus from '../icons/Plus.svelte';
+	import ChevronRight from '../icons/ChevronRight.svelte';
+	import Spinner from '../common/Spinner.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	let shiftKey = false;
 	let shiftKey = false;
+	let loaded = false;
 
 
 	let toolsImportInputElement: HTMLInputElement;
 	let toolsImportInputElement: HTMLInputElement;
 	let importFiles;
 	let importFiles;
@@ -44,8 +48,10 @@
 
 
 	let showDeleteConfirm = false;
 	let showDeleteConfirm = false;
 
 
+	let tools = [];
 	let filteredItems = [];
 	let filteredItems = [];
-	$: filteredItems = $tools.filter(
+
+	$: filteredItems = tools.filter(
 		(t) =>
 		(t) =>
 			query === '' ||
 			query === '' ||
 			t.name.toLowerCase().includes(query.toLowerCase()) ||
 			t.name.toLowerCase().includes(query.toLowerCase()) ||
@@ -117,11 +123,20 @@
 
 
 		if (res) {
 		if (res) {
 			toast.success($i18n.t('Tool deleted successfully'));
 			toast.success($i18n.t('Tool deleted successfully'));
-			tools.set(await getTools(localStorage.token));
+
+			init();
 		}
 		}
 	};
 	};
 
 
-	onMount(() => {
+	const init = async () => {
+		tools = await getToolList(localStorage.token);
+		_tools.set(await getTools(localStorage.token));
+	};
+
+	onMount(async () => {
+		await init();
+		loaded = true;
+
 		const onKeyDown = (event) => {
 		const onKeyDown = (event) => {
 			if (event.key === 'Shift') {
 			if (event.key === 'Shift') {
 				shiftKey = true;
 				shiftKey = true;
@@ -156,347 +171,336 @@
 	</title>
 	</title>
 </svelte:head>
 </svelte:head>
 
 
-<div class="flex flex-col gap-1 mt-1.5 mb-2">
-	<div class="flex justify-between items-center">
-		<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
-			{$i18n.t('Tools')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
-			<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
-				>{filteredItems.length}</span
-			>
+{#if loaded}
+	<div class="flex flex-col gap-1 mt-1.5 mb-2">
+		<div class="flex justify-between items-center">
+			<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
+				{$i18n.t('Tools')}
+				<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
+				<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
+					>{filteredItems.length}</span
+				>
+			</div>
 		</div>
 		</div>
-	</div>
 
 
-	<div class=" flex w-full space-x-2">
-		<div class="flex flex-1">
-			<div class=" self-center ml-1 mr-3">
-				<Search className="size-3.5" />
+		<div class=" flex w-full space-x-2">
+			<div class="flex flex-1">
+				<div class=" self-center ml-1 mr-3">
+					<Search className="size-3.5" />
+				</div>
+				<input
+					class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+					bind:value={query}
+					placeholder={$i18n.t('Search Tools')}
+				/>
 			</div>
 			</div>
-			<input
-				class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
-				bind:value={query}
-				placeholder={$i18n.t('Search Tools')}
-			/>
-		</div>
 
 
-		<div>
-			<a
-				class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
-				href="/workspace/tools/create"
-			>
-				<Plus className="size-3.5" />
-			</a>
+			<div>
+				<a
+					class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
+					href="/workspace/tools/create"
+				>
+					<Plus className="size-3.5" />
+				</a>
+			</div>
 		</div>
 		</div>
 	</div>
 	</div>
-</div>
 
 
-<div class="mb-5">
-	{#each filteredItems as tool}
-		<div
-			class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
-		>
-			<a
-				class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
-				href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
+	<div class="mb-5">
+		{#each filteredItems as tool}
+			<div
+				class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
 			>
 			>
-				<div class="flex items-center text-left">
-					<div class=" flex-1 self-center pl-1">
-						<div class=" font-semibold flex items-center gap-1.5">
-							<div
-								class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
-							>
-								TOOL
-							</div>
-
-							{#if tool?.meta?.manifest?.version}
+				<a
+					class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
+					href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
+				>
+					<div class="flex items-center text-left">
+						<div class=" flex-1 self-center pl-1">
+							<div class=" font-semibold flex items-center gap-1.5">
 								<div
 								<div
-									class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+									class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 								>
 								>
-									v{tool?.meta?.manifest?.version ?? ''}
+									TOOL
 								</div>
 								</div>
-							{/if}
 
 
-							<div class="line-clamp-1">
-								{tool.name}
+								{#if tool?.meta?.manifest?.version}
+									<div
+										class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+									>
+										v{tool?.meta?.manifest?.version ?? ''}
+									</div>
+								{/if}
+
+								<div class="line-clamp-1">
+									{tool.name}
+								</div>
 							</div>
 							</div>
-						</div>
 
 
-						<div class="flex gap-1.5 px-1">
-							<div class=" text-gray-500 text-xs font-medium flex-shrink-0">{tool.id}</div>
+							<div class="flex gap-1.5 px-1">
+								<div class=" text-gray-500 text-xs font-medium flex-shrink-0">{tool.id}</div>
 
 
-							<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
-								{tool.meta.description}
+								<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
+									{tool.meta.description}
+								</div>
 							</div>
 							</div>
 						</div>
 						</div>
 					</div>
 					</div>
-				</div>
-			</a>
-			<div class="flex flex-row gap-0.5 self-center">
-				{#if shiftKey}
-					<Tooltip content={$i18n.t('Delete')}>
-						<button
-							class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-							type="button"
-							on:click={() => {
-								deleteHandler(tool);
-							}}
-						>
-							<GarbageBin />
-						</button>
-					</Tooltip>
-				{:else}
-					{#if tool?.meta?.manifest?.funding_url ?? false}
-						<Tooltip content="Support">
+				</a>
+				<div class="flex flex-row gap-0.5 self-center">
+					{#if shiftKey}
+						<Tooltip content={$i18n.t('Delete')}>
+							<button
+								class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+								type="button"
+								on:click={() => {
+									deleteHandler(tool);
+								}}
+							>
+								<GarbageBin />
+							</button>
+						</Tooltip>
+					{:else}
+						{#if tool?.meta?.manifest?.funding_url ?? false}
+							<Tooltip content="Support">
+								<button
+									class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+									type="button"
+									on:click={() => {
+										selectedTool = tool;
+										showManifestModal = true;
+									}}
+								>
+									<Heart />
+								</button>
+							</Tooltip>
+						{/if}
+
+						<Tooltip content={$i18n.t('Valves')}>
 							<button
 							<button
 								class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 								class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 								type="button"
 								type="button"
 								on:click={() => {
 								on:click={() => {
 									selectedTool = tool;
 									selectedTool = tool;
-									showManifestModal = true;
+									showValvesModal = true;
 								}}
 								}}
 							>
 							>
-								<Heart />
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									fill="none"
+									viewBox="0 0 24 24"
+									stroke-width="1.5"
+									stroke="currentColor"
+									class="size-4"
+								>
+									<path
+										stroke-linecap="round"
+										stroke-linejoin="round"
+										d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
+									/>
+									<path
+										stroke-linecap="round"
+										stroke-linejoin="round"
+										d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
+									/>
+								</svg>
 							</button>
 							</button>
 						</Tooltip>
 						</Tooltip>
-					{/if}
 
 
-					<Tooltip content={$i18n.t('Valves')}>
-						<button
-							class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-							type="button"
-							on:click={() => {
+						<ToolMenu
+							editHandler={() => {
+								goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
+							}}
+							shareHandler={() => {
+								shareHandler(tool);
+							}}
+							cloneHandler={() => {
+								cloneHandler(tool);
+							}}
+							exportHandler={() => {
+								exportHandler(tool);
+							}}
+							deleteHandler={async () => {
 								selectedTool = tool;
 								selectedTool = tool;
-								showValvesModal = true;
+								showDeleteConfirm = true;
 							}}
 							}}
+							onClose={() => {}}
 						>
 						>
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								fill="none"
-								viewBox="0 0 24 24"
-								stroke-width="1.5"
-								stroke="currentColor"
-								class="size-4"
+							<button
+								class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+								type="button"
 							>
 							>
-								<path
-									stroke-linecap="round"
-									stroke-linejoin="round"
-									d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
-								/>
-								<path
-									stroke-linecap="round"
-									stroke-linejoin="round"
-									d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
-								/>
-							</svg>
-						</button>
-					</Tooltip>
-
-					<ToolMenu
-						editHandler={() => {
-							goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
-						}}
-						shareHandler={() => {
-							shareHandler(tool);
-						}}
-						cloneHandler={() => {
-							cloneHandler(tool);
-						}}
-						exportHandler={() => {
-							exportHandler(tool);
-						}}
-						deleteHandler={async () => {
-							selectedTool = tool;
-							showDeleteConfirm = true;
-						}}
-						onClose={() => {}}
-					>
-						<button
-							class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
-							type="button"
-						>
-							<EllipsisHorizontal className="size-5" />
-						</button>
-					</ToolMenu>
-				{/if}
+								<EllipsisHorizontal className="size-5" />
+							</button>
+						</ToolMenu>
+					{/if}
+				</div>
 			</div>
 			</div>
-		</div>
-	{/each}
-</div>
-
-<div class=" text-gray-500 text-xs mt-1 mb-2">
-	ⓘ {$i18n.t(
-		'Admins have access to all tools at all times; users need tools assigned per model in the workspace.'
-	)}
-</div>
-
-<div class=" flex justify-end w-full mb-2">
-	<div class="flex space-x-2">
-		<input
-			id="documents-import-input"
-			bind:this={toolsImportInputElement}
-			bind:files={importFiles}
-			type="file"
-			accept=".json"
-			hidden
-			on:change={() => {
-				console.log(importFiles);
-				showConfirm = true;
-			}}
-		/>
-
-		<button
-			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
-			on:click={() => {
-				toolsImportInputElement.click();
-			}}
-		>
-			<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Tools')}</div>
-
-			<div class=" self-center">
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 16 16"
-					fill="currentColor"
-					class="w-4 h-4"
+		{/each}
+	</div>
+
+	{#if $user?.role === 'admin'}
+		<div class=" flex justify-end w-full mb-2">
+			<div class="flex space-x-2">
+				<input
+					id="documents-import-input"
+					bind:this={toolsImportInputElement}
+					bind:files={importFiles}
+					type="file"
+					accept=".json"
+					hidden
+					on:change={() => {
+						console.log(importFiles);
+						showConfirm = true;
+					}}
+				/>
+
+				<button
+					class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+					on:click={() => {
+						toolsImportInputElement.click();
+					}}
 				>
 				>
-					<path
-						fill-rule="evenodd"
-						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
-						clip-rule="evenodd"
-					/>
-				</svg>
-			</div>
-		</button>
-
-		<button
-			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
-			on:click={async () => {
-				const _tools = await exportTools(localStorage.token).catch((error) => {
-					toast.error(error);
-					return null;
-				});
-
-				if (_tools) {
-					let blob = new Blob([JSON.stringify(_tools)], {
-						type: 'application/json'
-					});
-					saveAs(blob, `tools-export-${Date.now()}.json`);
-				}
-			}}
-		>
-			<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Tools')}</div>
-
-			<div class=" self-center">
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 16 16"
-					fill="currentColor"
-					class="w-4 h-4"
+					<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Tools')}</div>
+
+					<div class=" self-center">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+				</button>
+
+				<button
+					class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+					on:click={async () => {
+						const _tools = await exportTools(localStorage.token).catch((error) => {
+							toast.error(error);
+							return null;
+						});
+
+						if (_tools) {
+							let blob = new Blob([JSON.stringify(_tools)], {
+								type: 'application/json'
+							});
+							saveAs(blob, `tools-export-${Date.now()}.json`);
+						}
+					}}
 				>
 				>
-					<path
-						fill-rule="evenodd"
-						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
-						clip-rule="evenodd"
-					/>
-				</svg>
+					<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Tools')}</div>
+
+					<div class=" self-center">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+				</button>
 			</div>
 			</div>
-		</button>
-	</div>
-</div>
-
-{#if $config?.features.enable_community_sharing}
-	<div class=" my-16">
-		<div class=" text-lg font-semibold mb-3 line-clamp-1">
-			{$i18n.t('Made by OpenWebUI Community')}
 		</div>
 		</div>
+	{/if}
 
 
-		<a
-			class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
-			href="https://openwebui.com/#open-webui-community"
-			target="_blank"
-		>
-			<div class=" self-center w-10 flex-shrink-0">
-				<div
-					class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
-				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 24 24"
-						fill="currentColor"
-						class="w-6"
-					>
-						<path
-							fill-rule="evenodd"
-							d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-							clip-rule="evenodd"
-						/>
-					</svg>
-				</div>
+	{#if $config?.features.enable_community_sharing}
+		<div class=" my-16">
+			<div class=" text-lg font-semibold mb-0.5 line-clamp-1">
+				{$i18n.t('Made by OpenWebUI Community')}
 			</div>
 			</div>
 
 
-			<div class=" self-center">
-				<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div>
-				<div class=" text-sm line-clamp-1">
-					{$i18n.t('Discover, download, and explore custom tools')}
+			<a
+				class=" flex cursor-pointer items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-850 w-full mb-2 px-3.5 py-1.5 rounded-xl transition"
+				href="https://openwebui.com/#open-webui-community"
+				target="_blank"
+			>
+				<div class=" self-center">
+					<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div>
+					<div class=" text-sm line-clamp-1">
+						{$i18n.t('Discover, download, and explore custom tools')}
+					</div>
 				</div>
 				</div>
-			</div>
-		</a>
-	</div>
-{/if}
-
-<DeleteConfirmDialog
-	bind:show={showDeleteConfirm}
-	title={$i18n.t('Delete tool?')}
-	on:confirm={() => {
-		deleteHandler(selectedTool);
-	}}
->
-	<div class=" text-sm text-gray-500">
-		{$i18n.t('This will delete')} <span class="  font-semibold">{selectedTool.name}</span>.
-	</div>
-</DeleteConfirmDialog>
-
-<ValvesModal bind:show={showValvesModal} type="tool" id={selectedTool?.id ?? null} />
-<ManifestModal bind:show={showManifestModal} manifest={selectedTool?.meta?.manifest ?? {}} />
-
-<ConfirmDialog
-	bind:show={showConfirm}
-	on:confirm={() => {
-		const reader = new FileReader();
-		reader.onload = async (event) => {
-			const _tools = JSON.parse(event.target.result);
-			console.log(_tools);
-
-			for (const tool of _tools) {
-				const res = await createNewTool(localStorage.token, tool).catch((error) => {
-					toast.error(error);
-					return null;
-				});
-			}
-
-			toast.success($i18n.t('Tool imported successfully'));
-			tools.set(await getTools(localStorage.token));
-		};
 
 
-		reader.readAsText(importFiles[0]);
-	}}
->
-	<div class="text-sm text-gray-500">
-		<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
-			<div>{$i18n.t('Please carefully review the following warnings:')}</div>
-
-			<ul class=" mt-1 list-disc pl-4 text-xs">
-				<li>
-					{$i18n.t('Tools have a function calling system that allows arbitrary code execution')}.
-				</li>
-				<li>{$i18n.t('Do not install tools from sources you do not fully trust.')}</li>
-			</ul>
+				<div>
+					<div>
+						<ChevronRight />
+					</div>
+				</div>
+			</a>
 		</div>
 		</div>
+	{/if}
+
+	<DeleteConfirmDialog
+		bind:show={showDeleteConfirm}
+		title={$i18n.t('Delete tool?')}
+		on:confirm={() => {
+			deleteHandler(selectedTool);
+		}}
+	>
+		<div class=" text-sm text-gray-500">
+			{$i18n.t('This will delete')} <span class="  font-semibold">{selectedTool.name}</span>.
+		</div>
+	</DeleteConfirmDialog>
+
+	<ValvesModal bind:show={showValvesModal} type="tool" id={selectedTool?.id ?? null} />
+	<ManifestModal bind:show={showManifestModal} manifest={selectedTool?.meta?.manifest ?? {}} />
+
+	<ConfirmDialog
+		bind:show={showConfirm}
+		on:confirm={() => {
+			const reader = new FileReader();
+			reader.onload = async (event) => {
+				const _tools = JSON.parse(event.target.result);
+				console.log(_tools);
+
+				for (const tool of _tools) {
+					const res = await createNewTool(localStorage.token, tool).catch((error) => {
+						toast.error(error);
+						return null;
+					});
+				}
+
+				toast.success($i18n.t('Tool imported successfully'));
+				tools.set(await getTools(localStorage.token));
+			};
+
+			reader.readAsText(importFiles[0]);
+		}}
+	>
+		<div class="text-sm text-gray-500">
+			<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
+				<div>{$i18n.t('Please carefully review the following warnings:')}</div>
+
+				<ul class=" mt-1 list-disc pl-4 text-xs">
+					<li>
+						{$i18n.t('Tools have a function calling system that allows arbitrary code execution')}.
+					</li>
+					<li>{$i18n.t('Do not install tools from sources you do not fully trust.')}</li>
+				</ul>
+			</div>
 
 
-		<div class="my-3">
-			{$i18n.t(
-				'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.'
-			)}
+			<div class="my-3">
+				{$i18n.t(
+					'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.'
+				)}
+			</div>
 		</div>
 		</div>
+	</ConfirmDialog>
+{:else}
+	<div class="w-full h-full flex justify-center items-center">
+		<Spinner />
 	</div>
 	</div>
-</ConfirmDialog>
+{/if}

+ 30 - 13
src/lib/components/workspace/Tools/ToolkitEditor.svelte

@@ -9,12 +9,16 @@
 	import Badge from '$lib/components/common/Badge.svelte';
 	import Badge from '$lib/components/common/Badge.svelte';
 	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
 	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import LockClosed from '$lib/components/icons/LockClosed.svelte';
+	import AccessControlModal from '../common/AccessControlModal.svelte';
 
 
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	let formElement = null;
 	let formElement = null;
 	let loading = false;
 	let loading = false;
+
 	let showConfirm = false;
 	let showConfirm = false;
+	let showAccessControlModal = false;
 
 
 	export let edit = false;
 	export let edit = false;
 	export let clone = false;
 	export let clone = false;
@@ -25,6 +29,8 @@
 		description: ''
 		description: ''
 	};
 	};
 	export let content = '';
 	export let content = '';
+	export let accessControl = null;
+
 	let _content = '';
 	let _content = '';
 
 
 	$: if (content) {
 	$: if (content) {
@@ -148,7 +154,8 @@ class Tools:
 			id,
 			id,
 			name,
 			name,
 			meta,
 			meta,
-			content
+			content,
+			access_control: accessControl
 		});
 		});
 	};
 	};
 
 
@@ -172,6 +179,8 @@ class Tools:
 	};
 	};
 </script>
 </script>
 
 
+<AccessControlModal bind:show={showAccessControlModal} bind:accessControl />
+
 <div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
 <div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
 	<div class="mx-auto w-full md:px-0 h-full">
 	<div class="mx-auto w-full md:px-0 h-full">
 		<form
 		<form
@@ -203,11 +212,11 @@ class Tools:
 						</div>
 						</div>
 
 
 						<div class="flex-1">
 						<div class="flex-1">
-							<Tooltip content={$i18n.t('e.g. My ToolKit')} placement="top-start">
+							<Tooltip content={$i18n.t('e.g. My Tools')} placement="top-start">
 								<input
 								<input
-									class="w-full text-2xl font-medium bg-transparent outline-none"
+									class="w-full text-2xl font-semibold bg-transparent outline-none"
 									type="text"
 									type="text"
-									placeholder={$i18n.t('Toolkit Name')}
+									placeholder={$i18n.t('Tool Name')}
 									bind:value={name}
 									bind:value={name}
 									required
 									required
 								/>
 								/>
@@ -215,7 +224,19 @@ class Tools:
 						</div>
 						</div>
 
 
 						<div>
 						<div>
-							<Badge type="muted" content={$i18n.t('Tool')} />
+							<button
+								class="bg-gray-50 hover:bg-gray-100 text-black transition px-2 py-1 rounded-full flex gap-1 items-center"
+								type="button"
+								on:click={() => {
+									showAccessControlModal = true;
+								}}
+							>
+								<LockClosed strokeWidth="2.5" className="size-3.5" />
+
+								<div class="text-sm font-medium flex-shrink-0">
+									{$i18n.t('Share')}
+								</div>
+							</button>
 						</div>
 						</div>
 					</div>
 					</div>
 
 
@@ -225,15 +246,11 @@ class Tools:
 								{id}
 								{id}
 							</div>
 							</div>
 						{:else}
 						{:else}
-							<Tooltip
-								className="w-full"
-								content={$i18n.t('e.g. my_toolkit')}
-								placement="top-start"
-							>
+							<Tooltip className="w-full" content={$i18n.t('e.g. my_tools')} placement="top-start">
 								<input
 								<input
 									class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
 									class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
 									type="text"
 									type="text"
-									placeholder={$i18n.t('Toolkit ID')}
+									placeholder={$i18n.t('Tool ID')}
 									bind:value={id}
 									bind:value={id}
 									required
 									required
 									disabled={edit}
 									disabled={edit}
@@ -243,13 +260,13 @@ class Tools:
 
 
 						<Tooltip
 						<Tooltip
 							className="w-full self-center items-center flex"
 							className="w-full self-center items-center flex"
-							content={$i18n.t('e.g. A toolkit for performing various operations')}
+							content={$i18n.t('e.g. Tools for performing various operations')}
 							placement="top-start"
 							placement="top-start"
 						>
 						>
 							<input
 							<input
 								class="w-full text-sm bg-transparent outline-none"
 								class="w-full text-sm bg-transparent outline-none"
 								type="text"
 								type="text"
-								placeholder={$i18n.t('Toolkit Description')}
+								placeholder={$i18n.t('Tool Description')}
 								bind:value={meta.description}
 								bind:value={meta.description}
 								required
 								required
 							/>
 							/>

+ 193 - 0
src/lib/components/workspace/common/AccessControl.svelte

@@ -0,0 +1,193 @@
+<script lang="ts">
+	import { getContext, onMount } from 'svelte';
+
+	const i18n = getContext('i18n');
+
+	import { getGroups } from '$lib/apis/groups';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Plus from '$lib/components/icons/Plus.svelte';
+	import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
+
+	export let onChange: Function = () => {};
+
+	export let accessControl = null;
+
+	let selectedGroupId = '';
+	let groups = [];
+
+	onMount(async () => {
+		groups = await getGroups(localStorage.token);
+	});
+
+	$: onChange(accessControl);
+</script>
+
+<div class=" rounded-lg flex flex-col gap-2">
+	<div class="">
+		<div class=" text-sm font-semibold mb-1">{$i18n.t('Visibility')}</div>
+
+		<div class="flex gap-2.5 items-center mb-1">
+			<div>
+				<div class=" p-2 bg-black/5 dark:bg-white/5 rounded-full">
+					{#if accessControl !== null}
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-5 h-5"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
+							/>
+						</svg>
+					{:else}
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-5 h-5"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M6.115 5.19l.319 1.913A6 6 0 008.11 10.36L9.75 12l-.387.775c-.217.433-.132.956.21 1.298l1.348 1.348c.21.21.329.497.329.795v1.089c0 .426.24.815.622 1.006l.153.076c.433.217.956.132 1.298-.21l.723-.723a8.7 8.7 0 002.288-4.042 1.087 1.087 0 00-.358-1.099l-1.33-1.108c-.251-.21-.582-.299-.905-.245l-1.17.195a1.125 1.125 0 01-.98-.314l-.295-.295a1.125 1.125 0 010-1.591l.13-.132a1.125 1.125 0 011.3-.21l.603.302a.809.809 0 001.086-1.086L14.25 7.5l1.256-.837a4.5 4.5 0 001.528-1.732l.146-.292M6.115 5.19A9 9 0 1017.18 4.64M6.115 5.19A8.965 8.965 0 0112 3c1.929 0 3.716.607 5.18 1.64"
+							/>
+						</svg>
+					{/if}
+				</div>
+			</div>
+
+			<div>
+				<select
+					id="models"
+					class="outline-none bg-transparent text-sm font-medium rounded-lg block w-fit pr-10 max-w-full placeholder-gray-400"
+					value={accessControl !== null ? 'private' : 'public'}
+					on:change={(e) => {
+						if (e.target.value === 'public') {
+							accessControl = null;
+						} else {
+							accessControl = {
+								read: {
+									group_ids: []
+								}
+							};
+						}
+					}}
+				>
+					<option class=" text-gray-700" value="private" selected>Private</option>
+					<option class=" text-gray-700" value="public" selected>Public</option>
+				</select>
+
+				<div class=" text-xs text-gray-400 font-medium">
+					{#if accessControl !== null}
+						{$i18n.t('Only select users and groups with permission can access')}
+					{:else}
+						{$i18n.t('Accessible to all users')}
+					{/if}
+				</div>
+			</div>
+		</div>
+	</div>
+
+	{#if accessControl !== null}
+		{@const accessGroups = groups.filter((group) =>
+			accessControl.read.group_ids.includes(group.id)
+		)}
+		<div>
+			<div class="">
+				<div class="flex justify-between mb-1.5">
+					<div class="text-sm font-semibold">
+						{$i18n.t('Groups')}
+					</div>
+				</div>
+
+				<div class="flex flex-col gap-2">
+					{#if accessGroups.length > 0}
+						{#each accessGroups as group}
+							<div class="flex items-center gap-3 justify-between text-xs w-full transition">
+								<div class="flex items-center gap-1.5 w-full font-medium">
+									<div>
+										<UserCircleSolid className="size-4" />
+									</div>
+
+									<div>
+										{group.name}
+									</div>
+								</div>
+
+								<div class="w-full flex justify-end">
+									<button
+										class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+										type="button"
+										on:click={() => {
+											accessControl.read.group_ids = accessControl.read.group_ids.filter(
+												(id) => id !== group.id
+											);
+										}}
+									>
+										<XMark />
+									</button>
+								</div>
+							</div>
+						{/each}
+					{:else}
+						<div class="flex items-center justify-center">
+							<div class="text-gray-500 text-xs text-center py-2 px-10">
+								{$i18n.t('No groups with access, add a group to grant access')}
+							</div>
+						</div>
+					{/if}
+				</div>
+			</div>
+
+			<hr class=" my-2 border-black/5 dark:border-white/5" />
+
+			<div class="mb-1">
+				<div class="flex w-full">
+					<div class="flex flex-1 items-center">
+						<div class="w-full">
+							<select
+								class="outline-none bg-transparent text-sm font-medium rounded-lg block w-full pr-10 max-w-full dark:placeholder-gray-700"
+								bind:value={selectedGroupId}
+							>
+								<option class=" text-gray-700" value="" disabled selected
+									>{$i18n.t('Select a group')}</option
+								>
+								{#each groups.filter((group) => !accessControl.read.group_ids.includes(group.id)) as group}
+									<option class=" text-gray-700" value={group.id}>{group.name}</option>
+								{/each}
+							</select>
+						</div>
+						<div>
+							<Tooltip content={$i18n.t('Add Group')}>
+								<button
+									class=" p-1 rounded-xl bg-transparent dark:hover:bg-white/5 hover:bg-black/5 transition font-medium text-sm flex items-center space-x-1"
+									type="button"
+									on:click={() => {
+										if (selectedGroupId !== '') {
+											accessControl.read.group_ids = [
+												...accessControl.read.group_ids,
+												selectedGroupId
+											];
+
+											selectedGroupId = '';
+										}
+									}}
+								>
+									<Plus className="size-3.5" />
+								</button>
+							</Tooltip>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+	{/if}
+</div>

+ 43 - 0
src/lib/components/workspace/common/AccessControlModal.svelte

@@ -0,0 +1,43 @@
+<script>
+	import { getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import Modal from '$lib/components/common/Modal.svelte';
+	import AccessControl from './AccessControl.svelte';
+
+	export let show = false;
+	export let accessControl = null;
+
+	export let onChange = () => {};
+</script>
+
+<Modal size="sm" bind:show>
+	<div>
+		<div class=" flex justify-between dark:text-gray-100 px-5 pt-3 pb-1">
+			<div class=" text-lg font-medium self-center font-primary">
+				{$i18n.t('Share')}
+			</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="w-full px-5 pb-4">
+			<AccessControl bind:accessControl {onChange} />
+		</div>
+	</div>
+</Modal>

+ 1 - 1
src/routes/(app)/+layout.svelte

@@ -10,7 +10,7 @@
 	import { page } from '$app/stores';
 	import { page } from '$app/stores';
 	import { fade } from 'svelte/transition';
 	import { fade } from 'svelte/transition';
 
 
-	import { getKnowledgeItems } from '$lib/apis/knowledge';
+	import { getKnowledgeBases } from '$lib/apis/knowledge';
 	import { getFunctions } from '$lib/apis/functions';
 	import { getFunctions } from '$lib/apis/functions';
 	import { getModels, getVersionUpdates } from '$lib/apis';
 	import { getModels, getVersionUpdates } from '$lib/apis';
 	import { getAllTags } from '$lib/apis/chats';
 	import { getAllTags } from '$lib/apis/chats';

+ 7 - 0
src/routes/(app)/admin/+layout.svelte

@@ -65,6 +65,13 @@
 							href="/admin/evaluations">{$i18n.t('Evaluations')}</a
 							href="/admin/evaluations">{$i18n.t('Evaluations')}</a
 						>
 						>
 
 
+						<a
+							class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/admin/functions')
+								? ''
+								: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
+							href="/admin/functions">{$i18n.t('Functions')}</a
+						>
+
 						<a
 						<a
 							class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/admin/settings')
 							class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/admin/settings')
 								? ''
 								? ''

+ 1 - 1
src/routes/(app)/workspace/functions/+page.svelte → src/routes/(app)/admin/functions/+page.svelte

@@ -3,7 +3,7 @@
 	import { functions } from '$lib/stores';
 	import { functions } from '$lib/stores';
 
 
 	import { getFunctions } from '$lib/apis/functions';
 	import { getFunctions } from '$lib/apis/functions';
-	import Functions from '$lib/components/workspace/Functions.svelte';
+	import Functions from '$lib/components/admin/Functions.svelte';
 
 
 	onMount(async () => {
 	onMount(async () => {
 		await Promise.all([
 		await Promise.all([

+ 1 - 1
src/routes/(app)/workspace/functions/create/+page.svelte → src/routes/(app)/admin/functions/create/+page.svelte

@@ -5,7 +5,7 @@
 
 
 	import { functions, models } from '$lib/stores';
 	import { functions, models } from '$lib/stores';
 	import { createNewFunction, getFunctions } from '$lib/apis/functions';
 	import { createNewFunction, getFunctions } from '$lib/apis/functions';
-	import FunctionEditor from '$lib/components/workspace/Functions/FunctionEditor.svelte';
+	import FunctionEditor from '$lib/components/admin/Functions/FunctionEditor.svelte';
 	import { getModels } from '$lib/apis';
 	import { getModels } from '$lib/apis';
 	import { compareVersion, extractFrontmatter } from '$lib/utils';
 	import { compareVersion, extractFrontmatter } from '$lib/utils';
 	import { WEBUI_VERSION } from '$lib/constants';
 	import { WEBUI_VERSION } from '$lib/constants';

+ 1 - 1
src/routes/(app)/workspace/functions/edit/+page.svelte → src/routes/(app)/admin/functions/edit/+page.svelte

@@ -7,7 +7,7 @@
 	import { functions, models } from '$lib/stores';
 	import { functions, models } from '$lib/stores';
 	import { updateFunctionById, getFunctions, getFunctionById } from '$lib/apis/functions';
 	import { updateFunctionById, getFunctions, getFunctionById } from '$lib/apis/functions';
 
 
-	import FunctionEditor from '$lib/components/workspace/Functions/FunctionEditor.svelte';
+	import FunctionEditor from '$lib/components/admin/Functions/FunctionEditor.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import { getModels } from '$lib/apis';
 	import { getModels } from '$lib/apis';
 	import { compareVersion, extractFrontmatter } from '$lib/utils';
 	import { compareVersion, extractFrontmatter } from '$lib/utils';

+ 59 - 49
src/routes/(app)/workspace/+layout.svelte

@@ -15,11 +15,6 @@
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 
 
 	import MenuLines from '$lib/components/icons/MenuLines.svelte';
 	import MenuLines from '$lib/components/icons/MenuLines.svelte';
-	import { getModels } from '$lib/apis';
-	import { getPrompts } from '$lib/apis/prompts';
-	import { getKnowledgeItems } from '$lib/apis/knowledge';
-	import { getTools } from '$lib/apis/tools';
-	import { getFunctions } from '$lib/apis/functions';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -27,7 +22,21 @@
 
 
 	onMount(async () => {
 	onMount(async () => {
 		if ($user?.role !== 'admin') {
 		if ($user?.role !== 'admin') {
-			await goto('/');
+			if ($page.url.pathname.includes('/models') && !$user?.permissions?.workspace?.models) {
+				goto('/');
+			} else if (
+				$page.url.pathname.includes('/knowledge') &&
+				!$user?.permissions?.workspace?.knowledge
+			) {
+				goto('/');
+			} else if (
+				$page.url.pathname.includes('/prompts') &&
+				!$user?.permissions?.workspace?.prompts
+			) {
+				goto('/');
+			} else if ($page.url.pathname.includes('/tools') && !$user?.permissions?.workspace?.tools) {
+				goto('/');
+			}
 		}
 		}
 
 
 		loaded = true;
 		loaded = true;
@@ -46,7 +55,7 @@
 			? 'md:max-w-[calc(100%-260px)]'
 			? 'md:max-w-[calc(100%-260px)]'
 			: ''}"
 			: ''}"
 	>
 	>
-		<div class="   px-2.5 py-1 backdrop-blur-xl">
+		<div class="   px-2.5 pt-1 backdrop-blur-xl">
 			<div class=" flex items-center gap-1">
 			<div class=" flex items-center gap-1">
 				<div class="{$showSidebar ? 'md:hidden' : ''} self-center flex flex-none items-center">
 				<div class="{$showSidebar ? 'md:hidden' : ''} self-center flex flex-none items-center">
 					<button
 					<button
@@ -67,50 +76,51 @@
 					<div
 					<div
 						class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
 						class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
 					>
 					>
-						<a
-							class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/workspace/models')
-								? ''
-								: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
-							href="/workspace/models">{$i18n.t('Models')}</a
-						>
+						{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models}
+							<a
+								class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes(
+									'/workspace/models'
+								)
+									? ''
+									: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
+								href="/workspace/models">{$i18n.t('Models')}</a
+							>
+						{/if}
 
 
-						<a
-							class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes(
-								'/workspace/knowledge'
-							)
-								? ''
-								: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
-							href="/workspace/knowledge"
-						>
-							{$i18n.t('Knowledge')}
-						</a>
+						{#if $user?.role === 'admin' || $user?.permissions?.workspace?.knowledge}
+							<a
+								class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes(
+									'/workspace/knowledge'
+								)
+									? ''
+									: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
+								href="/workspace/knowledge"
+							>
+								{$i18n.t('Knowledge')}
+							</a>
+						{/if}
 
 
-						<a
-							class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/workspace/prompts')
-								? ''
-								: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
-							href="/workspace/prompts">{$i18n.t('Prompts')}</a
-						>
+						{#if $user?.role === 'admin' || $user?.permissions?.workspace?.prompts}
+							<a
+								class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes(
+									'/workspace/prompts'
+								)
+									? ''
+									: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
+								href="/workspace/prompts">{$i18n.t('Prompts')}</a
+							>
+						{/if}
 
 
-						<a
-							class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/workspace/tools')
-								? ''
-								: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
-							href="/workspace/tools"
-						>
-							{$i18n.t('Tools')}
-						</a>
-
-						<a
-							class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes(
-								'/workspace/functions'
-							)
-								? ''
-								: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
-							href="/workspace/functions"
-						>
-							{$i18n.t('Functions')}
-						</a>
+						{#if $user?.role === 'admin' || $user?.permissions?.workspace?.tools}
+							<a
+								class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/workspace/tools')
+									? ''
+									: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
+								href="/workspace/tools"
+							>
+								{$i18n.t('Tools')}
+							</a>
+						{/if}
 					</div>
 					</div>
 				</div>
 				</div>
 
 
@@ -118,7 +128,7 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<div class=" -mt-1 pb-1 px-[18px] flex-1 max-h-full overflow-y-auto" id="workspace-container">
+		<div class="  pb-1 px-[18px] flex-1 max-h-full overflow-y-auto" id="workspace-container">
 			<slot />
 			<slot />
 		</div>
 		</div>
 	</div>
 	</div>

+ 2 - 2
src/routes/(app)/workspace/knowledge/+page.svelte

@@ -2,13 +2,13 @@
 	import { onMount } from 'svelte';
 	import { onMount } from 'svelte';
 	import { knowledge } from '$lib/stores';
 	import { knowledge } from '$lib/stores';
 
 
-	import { getKnowledgeItems } from '$lib/apis/knowledge';
+	import { getKnowledgeBases } from '$lib/apis/knowledge';
 	import Knowledge from '$lib/components/workspace/Knowledge.svelte';
 	import Knowledge from '$lib/components/workspace/Knowledge.svelte';
 
 
 	onMount(async () => {
 	onMount(async () => {
 		await Promise.all([
 		await Promise.all([
 			(async () => {
 			(async () => {
-				knowledge.set(await getKnowledgeItems(localStorage.token));
+				knowledge.set(await getKnowledgeBases(localStorage.token));
 			})()
 			})()
 		]);
 		]);
 	});
 	});

+ 2 - 2
src/routes/(app)/workspace/knowledge/[id]/+page.svelte

@@ -1,5 +1,5 @@
 <script>
 <script>
-	import Collection from '$lib/components/workspace/Knowledge/Collection.svelte';
+	import KnowledgeBase from '$lib/components/workspace/Knowledge/KnowledgeBase.svelte';
 </script>
 </script>
 
 
-<Collection />
+<KnowledgeBase />

+ 2 - 2
src/routes/(app)/workspace/knowledge/create/+page.svelte

@@ -1,5 +1,5 @@
 <script>
 <script>
-	import CreateCollection from '$lib/components/workspace/Knowledge/CreateCollection.svelte';
+	import CreateKnowledgeBase from '$lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte';
 </script>
 </script>
 
 
-<CreateCollection />
+<CreateKnowledgeBase />

+ 5 - 2
src/routes/(app)/workspace/models/create/+page.svelte

@@ -5,7 +5,7 @@
 	import { models } from '$lib/stores';
 	import { models } from '$lib/stores';
 
 
 	import { onMount, tick, getContext } from 'svelte';
 	import { onMount, tick, getContext } from 'svelte';
-	import { addNewModel, getModelById, getModelInfos } from '$lib/apis/models';
+	import { createNewModel, getModelById } from '$lib/apis/models';
 	import { getModels } from '$lib/apis';
 	import { getModels } from '$lib/apis';
 
 
 	import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
 	import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
@@ -21,7 +21,7 @@
 		}
 		}
 
 
 		if (modelInfo) {
 		if (modelInfo) {
-			const res = await addNewModel(localStorage.token, {
+			const res = await createNewModel(localStorage.token, {
 				...modelInfo,
 				...modelInfo,
 				meta: {
 				meta: {
 					...modelInfo.meta,
 					...modelInfo.meta,
@@ -31,6 +31,9 @@
 						: null
 						: null
 				},
 				},
 				params: { ...modelInfo.params }
 				params: { ...modelInfo.params }
+			}).catch((error) => {
+				toast.error(error);
+				return null;
 			});
 			});
 
 
 			if (res) {
 			if (res) {

+ 6 - 3
src/routes/(app)/workspace/models/edit/+page.svelte

@@ -8,17 +8,20 @@
 	import { page } from '$app/stores';
 	import { page } from '$app/stores';
 	import { models } from '$lib/stores';
 	import { models } from '$lib/stores';
 
 
-	import { updateModelById } from '$lib/apis/models';
+	import { getModelById, updateModelById } from '$lib/apis/models';
 
 
 	import { getModels } from '$lib/apis';
 	import { getModels } from '$lib/apis';
 	import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
 	import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
 
 
 	let model = null;
 	let model = null;
 
 
-	onMount(() => {
+	onMount(async () => {
 		const _id = $page.url.searchParams.get('id');
 		const _id = $page.url.searchParams.get('id');
 		if (_id) {
 		if (_id) {
-			model = $models.find((m) => m.id === _id && m?.owned_by !== 'arena');
+			model = await getModelById(localStorage.token, _id).catch((e) => {
+				return null;
+			});
+
 			if (!model) {
 			if (!model) {
 				goto('/workspace/models');
 				goto('/workspace/models');
 			}
 			}

+ 1 - 15
src/routes/(app)/workspace/prompts/+page.svelte

@@ -1,19 +1,5 @@
 <script>
 <script>
-	import { onMount } from 'svelte';
-	import { prompts } from '$lib/stores';
-
-	import { getPrompts } from '$lib/apis/prompts';
 	import Prompts from '$lib/components/workspace/Prompts.svelte';
 	import Prompts from '$lib/components/workspace/Prompts.svelte';
-
-	onMount(async () => {
-		await Promise.all([
-			(async () => {
-				prompts.set(await getPrompts(localStorage.token));
-			})()
-		]);
-	});
 </script>
 </script>
 
 
-{#if $prompts !== null}
-	<Prompts />
-{/if}
+<Prompts />

+ 14 - 11
src/routes/(app)/workspace/prompts/create/+page.svelte

@@ -1,23 +1,24 @@
-<script>
+<script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 	import { prompts } from '$lib/stores';
 	import { prompts } from '$lib/stores';
 	import { onMount, tick, getContext } from 'svelte';
 	import { onMount, tick, getContext } from 'svelte';
 
 
+	const i18n = getContext('i18n');
+
 	import { createNewPrompt, getPrompts } from '$lib/apis/prompts';
 	import { createNewPrompt, getPrompts } from '$lib/apis/prompts';
 	import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
 	import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
 
 
 	let prompt = null;
 	let prompt = null;
-	const onSubmit = async ({ title, command, content }) => {
-		const prompt = await createNewPrompt(localStorage.token, command, title, content).catch(
-			(error) => {
-				toast.error(error);
-
-				return null;
-			}
-		);
+	const onSubmit = async (_prompt) => {
+		const prompt = await createNewPrompt(localStorage.token, _prompt).catch((error) => {
+			toast.error(error);
+			return null;
+		});
 
 
 		if (prompt) {
 		if (prompt) {
+			toast.success($i18n.t('Prompt created successfully'));
+
 			await prompts.set(await getPrompts(localStorage.token));
 			await prompts.set(await getPrompts(localStorage.token));
 			await goto('/workspace/prompts');
 			await goto('/workspace/prompts');
 		}
 		}
@@ -37,7 +38,8 @@
 			prompt = {
 			prompt = {
 				title: _prompt.title,
 				title: _prompt.title,
 				command: _prompt.command,
 				command: _prompt.command,
-				content: _prompt.content
+				content: _prompt.content,
+				access_control: null
 			};
 			};
 		});
 		});
 
 
@@ -51,7 +53,8 @@
 			prompt = {
 			prompt = {
 				title: _prompt.title,
 				title: _prompt.title,
 				command: _prompt.command,
 				command: _prompt.command,
-				content: _prompt.content
+				content: _prompt.content,
+				access_control: null
 			};
 			};
 			sessionStorage.removeItem('prompt');
 			sessionStorage.removeItem('prompt');
 		}
 		}

+ 20 - 11
src/routes/(app)/workspace/prompts/edit/+page.svelte

@@ -1,24 +1,26 @@
-<script>
+<script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 	import { prompts } from '$lib/stores';
 	import { prompts } from '$lib/stores';
 	import { onMount, tick, getContext } from 'svelte';
 	import { onMount, tick, getContext } from 'svelte';
 
 
-	import { getPrompts, updatePromptByCommand } from '$lib/apis/prompts';
+	const i18n = getContext('i18n');
+
+	import { getPromptByCommand, getPrompts, updatePromptByCommand } from '$lib/apis/prompts';
 	import { page } from '$app/stores';
 	import { page } from '$app/stores';
 
 
 	import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
 	import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
 
 
 	let prompt = null;
 	let prompt = null;
-	const onSubmit = async ({ title, command, content }) => {
-		const prompt = await updatePromptByCommand(localStorage.token, command, title, content).catch(
-			(error) => {
-				toast.error(error);
-				return null;
-			}
-		);
+	const onSubmit = async (_prompt) => {
+		console.log(_prompt);
+		const prompt = await updatePromptByCommand(localStorage.token, _prompt).catch((error) => {
+			toast.error(error);
+			return null;
+		});
 
 
 		if (prompt) {
 		if (prompt) {
+			toast.success($i18n.t('Prompt updated successfully'));
 			await prompts.set(await getPrompts(localStorage.token));
 			await prompts.set(await getPrompts(localStorage.token));
 			await goto('/workspace/prompts');
 			await goto('/workspace/prompts');
 		}
 		}
@@ -27,13 +29,20 @@
 	onMount(async () => {
 	onMount(async () => {
 		const command = $page.url.searchParams.get('command');
 		const command = $page.url.searchParams.get('command');
 		if (command) {
 		if (command) {
-			const _prompt = $prompts.filter((prompt) => prompt.command === command).at(0);
+			const _prompt = await getPromptByCommand(
+				localStorage.token,
+				command.replace(/\//g, '')
+			).catch((error) => {
+				toast.error(error);
+				return null;
+			});
 
 
 			if (_prompt) {
 			if (_prompt) {
 				prompt = {
 				prompt = {
 					title: _prompt.title,
 					title: _prompt.title,
 					command: _prompt.command,
 					command: _prompt.command,
-					content: _prompt.content
+					content: _prompt.content,
+					access_control: _prompt?.access_control ?? null
 				};
 				};
 			} else {
 			} else {
 				goto('/workspace/prompts');
 				goto('/workspace/prompts');

+ 1 - 13
src/routes/(app)/workspace/tools/+page.svelte

@@ -1,19 +1,7 @@
 <script>
 <script>
 	import { onMount } from 'svelte';
 	import { onMount } from 'svelte';
-	import { tools } from '$lib/stores';
 
 
-	import { getTools } from '$lib/apis/tools';
 	import Tools from '$lib/components/workspace/Tools.svelte';
 	import Tools from '$lib/components/workspace/Tools.svelte';
-
-	onMount(async () => {
-		await Promise.all([
-			(async () => {
-				tools.set(await getTools(localStorage.token));
-			})()
-		]);
-	});
 </script>
 </script>
 
 
-{#if $tools !== null}
-	<Tools />
-{/if}
+<Tools />

部分文件因为文件数量过多而无法显示