mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-13 04:45:19 +00:00
commit
034674c19c
102 changed files with 7486 additions and 4705 deletions
|
|
@ -13,9 +13,7 @@ import requests
|
|||
from open_webui.apps.webui.models.models import Models
|
||||
from open_webui.config import (
|
||||
CORS_ALLOW_ORIGIN,
|
||||
ENABLE_MODEL_FILTER,
|
||||
ENABLE_OLLAMA_API,
|
||||
MODEL_FILTER_LIST,
|
||||
OLLAMA_BASE_URLS,
|
||||
OLLAMA_API_CONFIGS,
|
||||
UPLOAD_DIR,
|
||||
|
|
@ -66,32 +64,16 @@ app.add_middleware(
|
|||
|
||||
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.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||
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.
|
||||
# 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.
|
||||
|
||||
|
||||
@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.get("/")
|
||||
async def get_status():
|
||||
|
|
@ -326,8 +308,6 @@ async def get_all_models():
|
|||
else:
|
||||
models = {"models": []}
|
||||
|
||||
app.state.MODELS = {model["model"]: model for model in models["models"]}
|
||||
|
||||
return models
|
||||
|
||||
|
||||
|
|
@ -339,16 +319,18 @@ async def get_ollama_tags(
|
|||
if url_idx is None:
|
||||
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
|
||||
else:
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
|
@ -473,8 +455,11 @@ async def push_model(
|
|||
user=Depends(get_admin_user),
|
||||
):
|
||||
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:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -523,8 +508,11 @@ async def copy_model(
|
|||
user=Depends(get_admin_user),
|
||||
):
|
||||
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:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -579,8 +567,11 @@ async def delete_model(
|
|||
user=Depends(get_admin_user),
|
||||
):
|
||||
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:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -628,13 +619,16 @@ async def delete_model(
|
|||
|
||||
@app.post("/api/show")
|
||||
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(
|
||||
status_code=400,
|
||||
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]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
|
|
@ -704,23 +698,26 @@ async def generate_embeddings(
|
|||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
return generate_ollama_embeddings(form_data=form_data, url_idx=url_idx)
|
||||
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,
|
||||
url_idx: Optional[int] = None,
|
||||
):
|
||||
log.info(f"generate_ollama_embeddings {form_data}")
|
||||
|
||||
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
|
||||
|
||||
if ":" not in model:
|
||||
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:
|
||||
raise HTTPException(
|
||||
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,
|
||||
url_idx: Optional[int] = None,
|
||||
):
|
||||
log.info(f"generate_ollama_batch_embeddings {form_data}")
|
||||
|
||||
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
|
||||
|
||||
if ":" not in model:
|
||||
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:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -854,13 +854,16 @@ async def generate_completion(
|
|||
user=Depends(get_verified_user),
|
||||
):
|
||||
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
|
||||
|
||||
if ":" not in model:
|
||||
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:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -895,14 +898,17 @@ class GenerateChatCompletionForm(BaseModel):
|
|||
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 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(
|
||||
status_code=400,
|
||||
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]
|
||||
return url
|
||||
|
||||
|
|
@ -922,12 +928,14 @@ async def generate_chat_completion(
|
|||
|
||||
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)
|
||||
|
||||
|
|
@ -949,7 +957,7 @@ async def generate_chat_completion(
|
|||
if ":" not in payload["model"]:
|
||||
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.debug(f"generate_chat_completion() - 2.payload = {payload}")
|
||||
|
||||
|
|
@ -1008,12 +1016,13 @@ async def generate_openai_chat_completion(
|
|||
|
||||
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)
|
||||
|
||||
|
|
@ -1030,7 +1039,7 @@ async def generate_openai_chat_completion(
|
|||
if ":" not in payload["model"]:
|
||||
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}")
|
||||
|
||||
api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
|
||||
|
|
@ -1054,15 +1063,16 @@ async def get_openai_models(
|
|||
if url_idx is None:
|
||||
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 {
|
||||
"data": [
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ from open_webui.apps.webui.models.models import Models
|
|||
from open_webui.config import (
|
||||
CACHE_DIR,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
ENABLE_MODEL_FILTER,
|
||||
ENABLE_OPENAI_API,
|
||||
MODEL_FILTER_LIST,
|
||||
OPENAI_API_BASE_URLS,
|
||||
OPENAI_API_KEYS,
|
||||
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.access_control import has_access
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
||||
|
|
@ -61,25 +61,11 @@ app.add_middleware(
|
|||
|
||||
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.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
|
||||
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")
|
||||
async def get_config(user=Depends(get_admin_user)):
|
||||
|
|
@ -264,7 +250,7 @@ def merge_models_lists(model_lists):
|
|||
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:
|
||||
return []
|
||||
|
||||
|
|
@ -335,22 +321,13 @@ async def get_all_models_raw() -> list:
|
|||
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()")
|
||||
if not app.state.config.ENABLE_OPENAI_API:
|
||||
return [] if raw else {"data": []}
|
||||
|
||||
responses = await get_all_models_raw()
|
||||
if raw:
|
||||
return responses
|
||||
if not app.state.config.ENABLE_OPENAI_API:
|
||||
return {"data": []}
|
||||
|
||||
responses = await get_all_models_responses()
|
||||
|
||||
def extract_data(response):
|
||||
if response and "data" in response:
|
||||
|
|
@ -360,9 +337,7 @@ async def get_all_models(raw=False) -> dict[str, list] | list:
|
|||
return None
|
||||
|
||||
models = {"data": merge_models_lists(map(extract_data, responses))}
|
||||
|
||||
log.debug(f"models: {models}")
|
||||
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
||||
|
||||
return models
|
||||
|
||||
|
|
@ -370,18 +345,12 @@ async def get_all_models(raw=False) -> dict[str, list] | list:
|
|||
@app.get("/models")
|
||||
@app.get("/models/{url_idx}")
|
||||
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
|
||||
models = {
|
||||
"data": [],
|
||||
}
|
||||
|
||||
if url_idx is None:
|
||||
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:
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[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["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
if ENABLE_FORWARD_USER_INFO_HEADERS:
|
||||
headers["X-OpenWebUI-User-Name"] = user.name
|
||||
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:
|
||||
# ClientError covers all aiohttp requests issues
|
||||
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)}"
|
||||
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):
|
||||
url: str
|
||||
|
|
@ -492,11 +477,10 @@ async def verify_connection(
|
|||
|
||||
|
||||
@app.post("/chat/completions")
|
||||
@app.post("/chat/completions/{url_idx}")
|
||||
async def generate_chat_completion(
|
||||
form_data: dict,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
bypass_filter: Optional[bool] = False,
|
||||
):
|
||||
idx = 0
|
||||
payload = {**form_data}
|
||||
|
|
@ -507,6 +491,7 @@ async def generate_chat_completion(
|
|||
model_id = form_data.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
# Check model info and override the payload
|
||||
if model_info:
|
||||
if 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_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(
|
||||
app.state.config.OPENAI_API_BASE_URLS[idx], {}
|
||||
)
|
||||
|
|
@ -526,6 +535,7 @@ async def generate_chat_completion(
|
|||
if 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"):
|
||||
payload["user"] = {
|
||||
"name": user.name,
|
||||
|
|
@ -536,8 +546,9 @@ async def generate_chat_completion(
|
|||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[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)
|
||||
if "api.openai.com" not in url and not is_o1:
|
||||
if "max_completion_tokens" in payload:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import os
|
|||
import uuid
|
||||
from typing import Optional, Union
|
||||
|
||||
import asyncio
|
||||
import requests
|
||||
|
||||
from huggingface_hub import snapshot_download
|
||||
|
|
@ -291,7 +292,13 @@ def get_embedding_function(
|
|||
if embedding_engine == "":
|
||||
return lambda query: embedding_function.encode(query).tolist()
|
||||
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,
|
||||
model=embedding_model,
|
||||
text=query,
|
||||
|
|
@ -469,7 +476,7 @@ def get_model_path(model: str, update_model: bool = False):
|
|||
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"
|
||||
) -> Optional[list[list[float]]]:
|
||||
try:
|
||||
|
|
@ -492,14 +499,16 @@ def generate_openai_batch_embeddings(
|
|||
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 isinstance(text, list):
|
||||
embeddings = generate_ollama_batch_embeddings(
|
||||
embeddings = await generate_ollama_batch_embeddings(
|
||||
GenerateEmbedForm(**{"model": model, "input": text})
|
||||
)
|
||||
else:
|
||||
embeddings = generate_ollama_batch_embeddings(
|
||||
embeddings = await generate_ollama_batch_embeddings(
|
||||
GenerateEmbedForm(**{"model": model, "input": [text]})
|
||||
)
|
||||
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")
|
||||
|
||||
if isinstance(text, list):
|
||||
embeddings = generate_openai_batch_embeddings(model, text, key, url)
|
||||
embeddings = await generate_openai_batch_embeddings(model, text, key, url)
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from open_webui.apps.webui.routers import (
|
|||
chats,
|
||||
folders,
|
||||
configs,
|
||||
groups,
|
||||
files,
|
||||
functions,
|
||||
memories,
|
||||
|
|
@ -85,7 +86,11 @@ from open_webui.utils.payload import (
|
|||
|
||||
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__)
|
||||
|
||||
|
|
@ -105,6 +110,8 @@ app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
|
|||
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
|
||||
|
||||
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
|
||||
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||
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_CIPHERS = LDAP_CIPHERS
|
||||
|
||||
app.state.MODELS = {}
|
||||
app.state.TOOLS = {}
|
||||
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(prompts.router, prefix="/prompts", tags=["prompts"])
|
||||
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(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(functions.router, prefix="/functions", tags=["functions"])
|
||||
app.include_router(evaluations.router, prefix="/evaluations", tags=["evaluations"])
|
||||
|
||||
app.include_router(folders.router, prefix="/folders", tags=["folders"])
|
||||
app.include_router(files.router, prefix="/files", tags=["files"])
|
||||
|
||||
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
|
||||
|
||||
|
||||
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_info = Models.get_model_by_id(model_id)
|
||||
|
||||
|
|
@ -405,7 +413,7 @@ async def generate_function_chat_completion(form_data, user):
|
|||
user,
|
||||
{
|
||||
**extra_params,
|
||||
"__model__": app.state.MODELS[form_data["model"]],
|
||||
"__model__": models.get(form_data["model"], None),
|
||||
"__messages__": form_data["messages"],
|
||||
"__files__": files,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
backend/open_webui/apps/webui/models/groups.py
Normal file
181
backend/open_webui/apps/webui/models/groups.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -13,6 +13,7 @@ from open_webui.apps.webui.models.files import FileMetadataResponse
|
|||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||
|
||||
from open_webui.utils.access_control import has_access
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
|
@ -34,6 +35,23 @@ class Knowledge(Base):
|
|||
data = 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)
|
||||
updated_at = Column(BigInteger)
|
||||
|
||||
|
|
@ -50,6 +68,8 @@ class KnowledgeModel(BaseModel):
|
|||
data: Optional[dict] = None
|
||||
meta: Optional[dict] = None
|
||||
|
||||
access_control: Optional[dict] = None
|
||||
|
||||
created_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
|
||||
|
|
@ -65,6 +85,8 @@ class KnowledgeResponse(BaseModel):
|
|||
description: str
|
||||
data: Optional[dict] = None
|
||||
meta: Optional[dict] = None
|
||||
|
||||
access_control: Optional[dict] = None
|
||||
created_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
|
||||
|
|
@ -75,12 +97,7 @@ class KnowledgeForm(BaseModel):
|
|||
name: str
|
||||
description: str
|
||||
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:
|
||||
|
|
@ -110,7 +127,7 @@ class KnowledgeTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_knowledge_items(self) -> list[KnowledgeModel]:
|
||||
def get_knowledge_bases(self) -> list[KnowledgeModel]:
|
||||
with get_db() as db:
|
||||
return [
|
||||
KnowledgeModel.model_validate(knowledge)
|
||||
|
|
@ -119,6 +136,17 @@ class KnowledgeTable:
|
|||
.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]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
|
@ -128,14 +156,32 @@ class KnowledgeTable:
|
|||
return None
|
||||
|
||||
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(exclude_none=True),
|
||||
**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]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
knowledge = self.get_knowledge_by_id(id=id)
|
||||
db.query(Knowledge).filter_by(id=id).update(
|
||||
{
|
||||
"data": data,
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,19 @@ from typing import Optional
|
|||
|
||||
from open_webui.apps.webui.internal.db import Base, JSONField, get_db
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
from open_webui.apps.webui.models.groups import Groups
|
||||
|
||||
|
||||
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.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
|
@ -67,6 +78,25 @@ class Model(Base):
|
|||
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)
|
||||
created_at = Column(BigInteger)
|
||||
|
||||
|
|
@ -80,6 +110,9 @@ class ModelModel(BaseModel):
|
|||
params: ModelParams
|
||||
meta: ModelMeta
|
||||
|
||||
access_control: Optional[dict] = None
|
||||
|
||||
is_active: bool
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
|
@ -93,8 +126,16 @@ class ModelModel(BaseModel):
|
|||
|
||||
class ModelResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
base_model_id: Optional[str] = None
|
||||
|
||||
name: str
|
||||
params: ModelParams
|
||||
meta: ModelMeta
|
||||
|
||||
access_control: Optional[dict] = None
|
||||
|
||||
is_active: bool
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
|
@ -105,6 +146,8 @@ class ModelForm(BaseModel):
|
|||
name: str
|
||||
meta: ModelMeta
|
||||
params: ModelParams
|
||||
access_control: Optional[dict] = None
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class ModelsTable:
|
||||
|
|
@ -138,6 +181,31 @@ class ModelsTable:
|
|||
with get_db() as db:
|
||||
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]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
|
@ -146,6 +214,23 @@ class ModelsTable:
|
|||
except Exception:
|
||||
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]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
|
@ -153,7 +238,7 @@ class ModelsTable:
|
|||
result = (
|
||||
db.query(Model)
|
||||
.filter_by(id=id)
|
||||
.update(model.model_dump(exclude={"id"}, exclude_none=True))
|
||||
.update(model.model_dump(exclude={"id"}))
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@ import time
|
|||
from typing import Optional
|
||||
|
||||
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 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
|
||||
|
|
@ -19,6 +23,23 @@ class Prompt(Base):
|
|||
content = Column(Text)
|
||||
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):
|
||||
command: str
|
||||
|
|
@ -27,6 +48,7 @@ class PromptModel(BaseModel):
|
|||
content: str
|
||||
timestamp: int # timestamp in epoch
|
||||
|
||||
access_control: Optional[dict] = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
|
|
@ -39,6 +61,7 @@ class PromptForm(BaseModel):
|
|||
command: str
|
||||
title: str
|
||||
content: str
|
||||
access_control: Optional[dict] = None
|
||||
|
||||
|
||||
class PromptsTable:
|
||||
|
|
@ -48,16 +71,14 @@ class PromptsTable:
|
|||
prompt = PromptModel(
|
||||
**{
|
||||
"user_id": user_id,
|
||||
"command": form_data.command,
|
||||
"title": form_data.title,
|
||||
"content": form_data.content,
|
||||
**form_data.model_dump(),
|
||||
"timestamp": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
with get_db() as db:
|
||||
result = Prompt(**prompt.dict())
|
||||
result = Prompt(**prompt.model_dump())
|
||||
db.add(result)
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
|
|
@ -82,6 +103,18 @@ class PromptsTable:
|
|||
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(
|
||||
self, command: str, form_data: PromptForm
|
||||
) -> Optional[PromptModel]:
|
||||
|
|
@ -90,6 +123,7 @@ class PromptsTable:
|
|||
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||
prompt.title = form_data.title
|
||||
prompt.content = form_data.content
|
||||
prompt.access_control = form_data.access_control
|
||||
prompt.timestamp = int(time.time())
|
||||
db.commit()
|
||||
return PromptModel.model_validate(prompt)
|
||||
|
|
|
|||
|
|
@ -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.env import SRC_LOG_LEVELS
|
||||
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.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
|
@ -26,6 +29,24 @@ class Tool(Base):
|
|||
specs = Column(JSONField)
|
||||
meta = 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)
|
||||
created_at = Column(BigInteger)
|
||||
|
||||
|
|
@ -42,6 +63,8 @@ class ToolModel(BaseModel):
|
|||
content: str
|
||||
specs: list[dict]
|
||||
meta: ToolMeta
|
||||
access_control: Optional[dict] = None
|
||||
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
|
@ -58,6 +81,7 @@ class ToolResponse(BaseModel):
|
|||
user_id: str
|
||||
name: str
|
||||
meta: ToolMeta
|
||||
access_control: Optional[dict] = None
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
|
@ -67,6 +91,7 @@ class ToolForm(BaseModel):
|
|||
name: str
|
||||
content: str
|
||||
meta: ToolMeta
|
||||
access_control: Optional[dict] = None
|
||||
|
||||
|
||||
class ToolValves(BaseModel):
|
||||
|
|
@ -113,6 +138,18 @@ class ToolsTable:
|
|||
with get_db() as db:
|
||||
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]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
|
|
|||
|
|
@ -40,10 +40,12 @@ from open_webui.utils.utils import (
|
|||
get_password_hash,
|
||||
)
|
||||
from open_webui.utils.webhook import post_webhook
|
||||
from open_webui.utils.access_control import get_permissions
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
from ldap3 import Server, Connection, ALL, Tls
|
||||
from ssl import CERT_REQUIRED, PROTOCOL_TLS
|
||||
from ldap3 import Server, Connection, ALL, Tls
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -58,6 +60,7 @@ log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
|||
|
||||
class SessionUserResponse(Token, UserResponse):
|
||||
expires_at: Optional[int] = None
|
||||
permissions: Optional[dict] = None
|
||||
|
||||
|
||||
@router.get("/", response_model=SessionUserResponse)
|
||||
|
|
@ -90,6 +93,10 @@ async def get_session_user(
|
|||
secure=WEBUI_SESSION_COOKIE_SECURE,
|
||||
)
|
||||
|
||||
user_permissions = get_permissions(
|
||||
user.id, request.app.state.config.USER_PERMISSIONS
|
||||
)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"token_type": "Bearer",
|
||||
|
|
@ -99,6 +106,7 @@ async def get_session_user(
|
|||
"name": user.name,
|
||||
"role": user.role,
|
||||
"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_USE_TLS = request.app.state.config.LDAP_USE_TLS
|
||||
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:
|
||||
raise HTTPException(400, detail="LDAP authentication is not enabled")
|
||||
|
||||
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:
|
||||
log.error(f"An error occurred on TLS: {str(e)}")
|
||||
raise HTTPException(400, detail=str(e))
|
||||
|
||||
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():
|
||||
raise HTTPException(400, detail="Application account bind failed")
|
||||
|
||||
search_success = connection_app.search(
|
||||
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:
|
||||
raise HTTPException(400, detail="User not found in the LDAP server")
|
||||
|
||||
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
|
||||
|
||||
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():
|
||||
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:
|
||||
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:
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||
raise HTTPException(
|
||||
500, detail=ERROR_MESSAGES.CREATE_USER_ERROR
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
|
@ -224,7 +257,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||
if user:
|
||||
token = create_token(
|
||||
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
|
||||
|
|
@ -246,7 +281,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||
else:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
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:
|
||||
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,
|
||||
)
|
||||
|
||||
user_permissions = get_permissions(
|
||||
user.id, request.app.state.config.USER_PERMISSIONS
|
||||
)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"token_type": "Bearer",
|
||||
|
|
@ -334,6 +376,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
|||
"name": user.name,
|
||||
"role": user.role,
|
||||
"profile_image_url": user.profile_image_url,
|
||||
"permissions": user_permissions,
|
||||
}
|
||||
else:
|
||||
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 {
|
||||
"token": token,
|
||||
"token_type": "Bearer",
|
||||
|
|
@ -435,6 +482,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
"name": user.name,
|
||||
"role": user.role,
|
||||
"profile_image_url": user.profile_image_url,
|
||||
"permissions": user_permissions,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||
|
|
@ -583,19 +631,18 @@ class LdapServerConfig(BaseModel):
|
|||
label: str
|
||||
host: str
|
||||
port: Optional[int] = None
|
||||
attribute_for_username: str = 'uid'
|
||||
attribute_for_username: str = "uid"
|
||||
app_dn: str
|
||||
app_dn_password: str
|
||||
search_base: str
|
||||
search_filters: str = ''
|
||||
search_filters: str = ""
|
||||
use_tls: bool = True
|
||||
certificate_path: Optional[str] = None
|
||||
ciphers: Optional[str] = 'ALL'
|
||||
ciphers: Optional[str] = "ALL"
|
||||
|
||||
|
||||
@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 {
|
||||
"label": request.app.state.config.LDAP_SERVER_LABEL,
|
||||
"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,
|
||||
"use_tls": request.app.state.config.LDAP_USE_TLS,
|
||||
"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")
|
||||
async def update_ldap_server(
|
||||
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:
|
||||
value = getattr(form_data, key)
|
||||
if not value:
|
||||
raise HTTPException(400, detail=f"Required field {key} is empty")
|
||||
|
||||
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_HOST = form_data.host
|
||||
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_PASSWORD = form_data.app_dn_password
|
||||
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,
|
||||
"use_tls": request.app.state.config.LDAP_USE_TLS,
|
||||
"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")
|
||||
async def get_ldap_config(request: Request, user=Depends(get_admin_user)):
|
||||
return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
|
||||
|
||||
|
||||
class LdapConfigForm(BaseModel):
|
||||
enable_ldap: Optional[bool] = None
|
||||
|
||||
|
||||
@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
|
||||
return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ from open_webui.constants import ERROR_MESSAGES
|
|||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
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.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
|
@ -50,9 +53,10 @@ async def get_session_user_chat_list(
|
|||
|
||||
@router.delete("/", response_model=bool)
|
||||
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(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
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
|
||||
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(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
|
|||
120
backend/open_webui/apps/webui/routers/groups.py
Normal file
120
backend/open_webui/apps/webui/routers/groups.py
Normal file
|
|
@ -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),
|
||||
)
|
||||
|
|
@ -6,7 +6,6 @@ import logging
|
|||
|
||||
from open_webui.apps.webui.models.knowledge import (
|
||||
Knowledges,
|
||||
KnowledgeUpdateForm,
|
||||
KnowledgeForm,
|
||||
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.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
|
||||
|
||||
|
||||
|
|
@ -26,64 +28,98 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||
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:
|
||||
knowledge_bases = []
|
||||
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read")
|
||||
|
||||
for knowledge in Knowledges.get_knowledge_items():
|
||||
|
||||
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.data.get("file_ids", [])):
|
||||
missing_files = list(
|
||||
set(knowledge.data.get("file_ids", []))
|
||||
- set([file.id for file in files])
|
||||
)
|
||||
if missing_files:
|
||||
data = knowledge.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_by_id(
|
||||
id=knowledge.id, form_data=KnowledgeUpdateForm(data=data)
|
||||
)
|
||||
|
||||
files = Files.get_file_metadatas_by_ids(file_ids)
|
||||
|
||||
knowledge_bases.append(
|
||||
KnowledgeResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=files,
|
||||
)
|
||||
# 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
|
||||
|
||||
|
||||
@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", [])
|
||||
)
|
||||
|
||||
# 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])
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
raise HTTPException(
|
||||
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])
|
||||
async def update_knowledge_by_id(
|
||||
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:
|
||||
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
||||
files = Files.get_files_by_ids(file_ids)
|
||||
|
|
@ -173,9 +230,22 @@ class KnowledgeFileIdForm(BaseModel):
|
|||
def add_file_to_knowledge_by_id(
|
||||
id: str,
|
||||
form_data: KnowledgeFileIdForm,
|
||||
user=Depends(get_admin_user),
|
||||
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,
|
||||
)
|
||||
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -206,9 +276,7 @@ def add_file_to_knowledge_by_id(
|
|||
file_ids.append(form_data.file_id)
|
||||
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:
|
||||
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(
|
||||
id: str,
|
||||
form_data: KnowledgeFileIdForm,
|
||||
user=Depends(get_admin_user),
|
||||
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,
|
||||
)
|
||||
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -288,9 +368,21 @@ def update_file_from_knowledge_by_id(
|
|||
def remove_file_from_knowledge_by_id(
|
||||
id: str,
|
||||
form_data: KnowledgeFileIdForm,
|
||||
user=Depends(get_admin_user),
|
||||
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,
|
||||
)
|
||||
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -318,9 +410,7 @@ def remove_file_from_knowledge_by_id(
|
|||
file_ids.remove(form_data.file_id)
|
||||
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:
|
||||
files = Files.get_files_by_ids(file_ids)
|
||||
|
|
@ -346,32 +436,26 @@ def remove_file_from_knowledge_by_id(
|
|||
)
|
||||
|
||||
|
||||
############################
|
||||
# ResetKnowledgeById
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/{id}/reset", response_model=Optional[KnowledgeResponse])
|
||||
async def reset_knowledge_by_id(id: str, user=Depends(get_admin_user)):
|
||||
try:
|
||||
VECTOR_DB_CLIENT.delete_collection(collection_name=id)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
pass
|
||||
|
||||
knowledge = Knowledges.update_knowledge_by_id(
|
||||
id=id, form_data=KnowledgeUpdateForm(data={"file_ids": []})
|
||||
)
|
||||
return knowledge
|
||||
|
||||
|
||||
############################
|
||||
# DeleteKnowledgeById
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/{id}/delete", response_model=bool)
|
||||
async def delete_knowledge_by_id(id: str, user=Depends(get_admin_user)):
|
||||
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:
|
||||
VECTOR_DB_CLIENT.delete_collection(collection_name=id)
|
||||
except Exception as e:
|
||||
|
|
@ -379,3 +463,34 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_admin_user)):
|
|||
pass
|
||||
result = Knowledges.delete_knowledge_by_id(id=id)
|
||||
return result
|
||||
|
||||
|
||||
############################
|
||||
# ResetKnowledgeById
|
||||
############################
|
||||
|
||||
|
||||
@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:
|
||||
VECTOR_DB_CLIENT.delete_collection(collection_name=id)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
pass
|
||||
|
||||
knowledge = Knowledges.update_knowledge_data_by_id(id=id.id, data={"file_ids": []})
|
||||
|
||||
return knowledge
|
||||
|
|
|
|||
|
|
@ -8,49 +8,58 @@ from open_webui.apps.webui.models.models import (
|
|||
)
|
||||
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
|
||||
from open_webui.utils.access_control import has_access
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
###########################
|
||||
# getModels
|
||||
# GetModels
|
||||
###########################
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ModelResponse])
|
||||
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:
|
||||
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,
|
||||
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(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
|
||||
)
|
||||
|
||||
else:
|
||||
model = Models.insert_new_model(form_data, user.id)
|
||||
|
||||
if model:
|
||||
return model
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# ToggelModelById
|
||||
############################
|
||||
|
||||
|
||||
@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)
|
||||
if model:
|
||||
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:
|
||||
return model
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateModelById
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/update", response_model=Optional[ModelModel])
|
||||
@router.post("/id/{id}/update", response_model=Optional[ModelModel])
|
||||
async def update_model_by_id(
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: ModelForm,
|
||||
user=Depends(get_admin_user),
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
model = Models.get_model_by_id(id)
|
||||
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 model:
|
||||
return model
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
|
||||
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)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from open_webui.apps.webui.models.prompts import PromptForm, PromptModel, Prompt
|
|||
from open_webui.constants import ERROR_MESSAGES
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_access
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -14,7 +15,22 @@ router = APIRouter()
|
|||
|
||||
@router.get("/", response_model=list[PromptModel])
|
||||
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])
|
||||
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)
|
||||
if prompt is None:
|
||||
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}")
|
||||
|
||||
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:
|
||||
raise HTTPException(
|
||||
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(
|
||||
command: str,
|
||||
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)
|
||||
if prompt:
|
||||
return prompt
|
||||
|
|
@ -85,6 +119,19 @@ async def update_prompt_by_command(
|
|||
|
||||
|
||||
@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}")
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -3,48 +3,66 @@ from pathlib import Path
|
|||
from typing import Optional
|
||||
|
||||
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.constants import ERROR_MESSAGES
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
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.access_control import has_access
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
############################
|
||||
# GetToolkits
|
||||
# GetTools
|
||||
############################
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
############################
|
||||
# ExportToolKits
|
||||
# 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
|
||||
|
||||
|
||||
############################
|
||||
# ExportTools
|
||||
############################
|
||||
|
||||
|
||||
@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])
|
||||
async def create_new_toolkit(
|
||||
async def create_new_tools(
|
||||
request: Request,
|
||||
form_data: ToolForm,
|
||||
user=Depends(get_admin_user),
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
if not form_data.id.isidentifier():
|
||||
raise HTTPException(
|
||||
|
|
@ -54,30 +72,30 @@ async def create_new_toolkit(
|
|||
|
||||
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:
|
||||
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.meta.manifest = frontmatter
|
||||
|
||||
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])
|
||||
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.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if toolkit:
|
||||
return toolkit
|
||||
if tools:
|
||||
return tools
|
||||
else:
|
||||
raise HTTPException(
|
||||
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:
|
||||
print(e)
|
||||
|
|
@ -93,16 +111,21 @@ async def create_new_toolkit(
|
|||
|
||||
|
||||
############################
|
||||
# GetToolkitById
|
||||
# GetToolsById
|
||||
############################
|
||||
|
||||
|
||||
@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)
|
||||
async def get_tools_by_id(id: str, user=Depends(get_verified_user)):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
|
||||
if toolkit:
|
||||
return toolkit
|
||||
if tools:
|
||||
if (
|
||||
user.role == "admin"
|
||||
or tools.user_id == user.id
|
||||
or has_access(user.id, "read", tools.access_control)
|
||||
):
|
||||
return tools
|
||||
else:
|
||||
raise HTTPException(
|
||||
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])
|
||||
async def update_toolkit_by_id(
|
||||
async def update_tools_by_id(
|
||||
request: Request,
|
||||
id: str,
|
||||
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:
|
||||
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
|
||||
)
|
||||
form_data.meta.manifest = frontmatter
|
||||
|
||||
TOOLS = request.app.state.TOOLS
|
||||
TOOLS[id] = toolkit_module
|
||||
TOOLS[id] = tools_module
|
||||
|
||||
specs = get_tools_specs(TOOLS[id])
|
||||
|
||||
|
|
@ -140,14 +176,14 @@ async def update_toolkit_by_id(
|
|||
}
|
||||
|
||||
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:
|
||||
raise HTTPException(
|
||||
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:
|
||||
|
|
@ -158,14 +194,28 @@ async def update_toolkit_by_id(
|
|||
|
||||
|
||||
############################
|
||||
# DeleteToolkitById
|
||||
# DeleteToolsById
|
||||
############################
|
||||
|
||||
|
||||
@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:
|
||||
TOOLS = request.app.state.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])
|
||||
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:
|
||||
valves = Tools.get_tool_valves_by_id(id)
|
||||
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])
|
||||
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:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
tools_module = request.app.state.TOOLS[id]
|
||||
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 None
|
||||
else:
|
||||
|
|
@ -232,19 +282,19 @@ async def get_toolkit_valves_spec_by_id(
|
|||
|
||||
|
||||
@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:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
tools_module = request.app.state.TOOLS[id]
|
||||
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:
|
||||
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])
|
||||
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:
|
||||
user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id)
|
||||
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])
|
||||
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)
|
||||
):
|
||||
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:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
tools_module = request.app.state.TOOLS[id]
|
||||
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 None
|
||||
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])
|
||||
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)
|
||||
):
|
||||
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:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
tools_module = request.app.state.TOOLS[id]
|
||||
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:
|
||||
form_data = {k: v for k, v in form_data.items() if v is not None}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
############################
|
||||
# User Groups
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/groups")
|
||||
async def get_user_groups(user=Depends(get_verified_user)):
|
||||
return Users.get_user_groups(user.id)
|
||||
|
||||
|
||||
############################
|
||||
# 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)):
|
||||
return request.app.state.config.USER_PERMISSIONS
|
||||
|
||||
|
||||
@router.post("/permissions/user")
|
||||
@router.post("/default/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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ def replace_imports(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:
|
||||
tool = Tools.get_tool_by_id(toolkit_id)
|
||||
|
|
|
|||
|
|
@ -739,12 +739,36 @@ DEFAULT_USER_ROLE = PersistentConfig(
|
|||
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_CHAT_EDITING = (
|
||||
os.environ.get("USER_PERMISSIONS_CHAT_EDITING", "True").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_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 = (
|
||||
|
|
@ -753,13 +777,20 @@ USER_PERMISSIONS_CHAT_TEMPORARY = (
|
|||
|
||||
USER_PERMISSIONS = PersistentConfig(
|
||||
"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": {
|
||||
"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,
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -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", "webhook_url", os.environ.get("WEBHOOK_URL", "")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import random
|
|||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
from aiocache import cached
|
||||
import aiohttp
|
||||
import requests
|
||||
from fastapi import (
|
||||
|
|
@ -45,6 +46,7 @@ from open_webui.apps.openai.main import (
|
|||
app as openai_app,
|
||||
generate_chat_completion as generate_openai_chat_completion,
|
||||
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.utils import get_rag_context, rag_template
|
||||
|
|
@ -70,13 +72,11 @@ from open_webui.config import (
|
|||
DEFAULT_LOCALE,
|
||||
ENABLE_ADMIN_CHAT_ACCESS,
|
||||
ENABLE_ADMIN_EXPORT,
|
||||
ENABLE_MODEL_FILTER,
|
||||
ENABLE_OLLAMA_API,
|
||||
ENABLE_OPENAI_API,
|
||||
ENABLE_TAGS_GENERATION,
|
||||
ENV,
|
||||
FRONTEND_BUILD_DIR,
|
||||
MODEL_FILTER_LIST,
|
||||
OAUTH_PROVIDERS,
|
||||
ENABLE_SEARCH_QUERY,
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
|
|
@ -135,6 +135,7 @@ from open_webui.utils.utils import (
|
|||
get_http_authorization_cred,
|
||||
get_verified_user,
|
||||
)
|
||||
from open_webui.utils.access_control import has_access
|
||||
|
||||
if SAFE_MODE:
|
||||
print("SAFE MODE ENABLED")
|
||||
|
|
@ -183,7 +184,10 @@ async def lifespan(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()
|
||||
|
|
@ -191,27 +195,26 @@ app.state.config = AppConfig()
|
|||
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_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.TASK_MODEL = TASK_MODEL
|
||||
app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
|
||||
|
||||
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.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 = (
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
app.state.config.ENABLE_SEARCH_QUERY = ENABLE_SEARCH_QUERY
|
||||
|
||||
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
)
|
||||
|
||||
app.state.MODELS = {}
|
||||
|
||||
|
||||
##################################
|
||||
#
|
||||
# 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_priority(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
|
||||
|
||||
|
||||
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(
|
||||
body: dict, user: UserModel, extra_params: dict
|
||||
body: dict, user: UserModel, models, extra_params: dict
|
||||
) -> tuple[dict, dict]:
|
||||
# If tool_ids field is present, call the functions
|
||||
metadata = body.get("metadata", {})
|
||||
|
|
@ -383,14 +382,19 @@ async def chat_completion_tools_handler(
|
|||
contexts = []
|
||||
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(
|
||||
webui_app,
|
||||
tool_ids,
|
||||
user,
|
||||
{
|
||||
**extra_params,
|
||||
"__model__": app.state.MODELS[task_model_id],
|
||||
"__model__": models[task_model_id],
|
||||
"__messages__": body["messages"],
|
||||
"__files__": metadata.get("files", []),
|
||||
},
|
||||
|
|
@ -414,7 +418,7 @@ async def chat_completion_tools_handler(
|
|||
)
|
||||
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
payload = filter_pipeline(payload, user, models)
|
||||
except Exception as 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
|
||||
body = await request.body()
|
||||
body_str = body.decode("utf-8")
|
||||
body = json.loads(body_str) if body_str else {}
|
||||
|
||||
model_id = body["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
if model_id not in models:
|
||||
raise Exception("Model not found")
|
||||
model = app.state.MODELS[model_id]
|
||||
model = models[model_id]
|
||||
|
||||
user = get_current_user(
|
||||
request,
|
||||
|
|
@ -540,14 +544,27 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
|||
return await call_next(request)
|
||||
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:
|
||||
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:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
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 = {
|
||||
"chat_id": body.pop("chat_id", None),
|
||||
"message_id": body.pop("id", None),
|
||||
|
|
@ -584,15 +601,20 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
|||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
tool_ids = body.pop("tool_ids", None)
|
||||
files = body.pop("files", None)
|
||||
|
||||
metadata = {
|
||||
**metadata,
|
||||
"tool_ids": body.pop("tool_ids", None),
|
||||
"files": body.pop("files", None),
|
||||
"tool_ids": tool_ids,
|
||||
"files": files,
|
||||
}
|
||||
body["metadata"] = metadata
|
||||
|
||||
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", []))
|
||||
citations.extend(flags.get("citations", []))
|
||||
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 = [
|
||||
model
|
||||
for model in app.state.MODELS.values()
|
||||
for model in models.values()
|
||||
if "pipeline" in model
|
||||
and "type" in model["pipeline"]
|
||||
and model["pipeline"]["type"] == "filter"
|
||||
|
|
@ -708,12 +730,12 @@ def get_sorted_filters(model_id):
|
|||
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}
|
||||
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:
|
||||
sorted_filters.append(model)
|
||||
|
|
@ -784,8 +806,11 @@ class PipelineMiddleware(BaseHTTPMiddleware):
|
|||
content={"detail": "Not authenticated"},
|
||||
)
|
||||
|
||||
model_list = await get_all_models()
|
||||
models = {model["id"]: model for model in model_list}
|
||||
|
||||
try:
|
||||
data = filter_pipeline(data, user)
|
||||
data = filter_pipeline(data, user, models)
|
||||
except Exception as e:
|
||||
if len(e.args) > 1:
|
||||
return JSONResponse(
|
||||
|
|
@ -864,16 +889,10 @@ async def commit_session_after_request(request: Request, call_next):
|
|||
|
||||
@app.middleware("http")
|
||||
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())
|
||||
response = await call_next(request)
|
||||
process_time = int(time.time()) - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
|
@ -913,12 +932,10 @@ app.mount("/retrieval/api/v1", retrieval_app)
|
|||
|
||||
app.mount("/api/v1", webui_app)
|
||||
|
||||
|
||||
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 = []
|
||||
openai_models = []
|
||||
ollama_models = []
|
||||
|
|
@ -944,9 +961,15 @@ async def get_all_models():
|
|||
open_webui_models = await get_open_webui_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 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 []
|
||||
|
||||
global_action_ids = [
|
||||
|
|
@ -965,15 +988,23 @@ async def get_all_models():
|
|||
custom_model.id == model["id"]
|
||||
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:
|
||||
model["action_ids"] = action_ids
|
||||
else:
|
||||
models.remove(model)
|
||||
|
||||
elif custom_model.is_active and (
|
||||
custom_model.id not in [model["id"] for model in models]
|
||||
):
|
||||
owned_by = "openai"
|
||||
pipe = None
|
||||
action_ids = []
|
||||
|
|
@ -995,7 +1026,7 @@ async def get_all_models():
|
|||
|
||||
models.append(
|
||||
{
|
||||
"id": custom_model.id,
|
||||
"id": f"{custom_model.id}",
|
||||
"name": custom_model.name,
|
||||
"object": "model",
|
||||
"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),
|
||||
}
|
||||
]
|
||||
|
||||
action_ids = action_ids + global_action_ids
|
||||
action_ids = list(set(action_ids))
|
||||
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
|
||||
|
||||
for model in models:
|
||||
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"] = []
|
||||
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}")
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -1081,40 +1100,58 @@ async def get_models(user=Depends(get_verified_user)):
|
|||
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}
|
||||
|
||||
|
||||
@app.post("/api/chat/completions")
|
||||
async def generate_chat_completions(
|
||||
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(
|
||||
status_code=status.HTTP_404_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(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
status_code=403,
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
model = app.state.MODELS[model_id]
|
||||
|
||||
if model["owned_by"] == "arena":
|
||||
model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
|
||||
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,
|
||||
}
|
||||
|
||||
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":
|
||||
# Using /ollama/api/chat endpoint
|
||||
form_data = convert_payload_openai_to_ollama(form_data)
|
||||
form_data = GenerateChatCompletionForm(**form_data)
|
||||
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:
|
||||
response.headers["content-type"] = "text/event-stream"
|
||||
|
|
@ -1179,21 +1220,27 @@ async def generate_chat_completions(
|
|||
else:
|
||||
return convert_response_ollama_to_openai(response)
|
||||
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")
|
||||
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
|
||||
model_id = data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
if model_id not in models:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_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:
|
||||
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",
|
||||
)
|
||||
|
||||
model_list = await get_all_models()
|
||||
models = {model["id"]: model for model in model_list}
|
||||
|
||||
data = form_data
|
||||
model_id = data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
|
||||
if model_id not in models:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
model = app.state.MODELS[model_id]
|
||||
model = models[model_id]
|
||||
|
||||
__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)):
|
||||
print("generate_title")
|
||||
|
||||
model_list = await get_all_models()
|
||||
models = {model["id"]: model for model in model_list}
|
||||
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
if model_id not in models:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_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
|
||||
# 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)
|
||||
|
||||
model = app.state.MODELS[task_model_id]
|
||||
model = models[task_model_id]
|
||||
|
||||
if 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,
|
||||
**(
|
||||
{"max_tokens": 50}
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
||||
if models[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 50,
|
||||
}
|
||||
|
|
@ -1587,7 +1647,7 @@ Artificial Intelligence in Healthcare
|
|||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
payload = filter_pipeline(payload, user, models)
|
||||
except Exception as e:
|
||||
if len(e.args) > 1:
|
||||
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"},
|
||||
)
|
||||
|
||||
model_list = await get_all_models()
|
||||
models = {model["id"]: model for model in model_list}
|
||||
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
if model_id not in models:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_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
|
||||
# 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)
|
||||
|
||||
if app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "":
|
||||
|
|
@ -1661,7 +1729,7 @@ JSON format: { "tags": ["tag1", "tag2", "tag3"] }
|
|||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
payload = filter_pipeline(payload, user, models)
|
||||
except Exception as e:
|
||||
if len(e.args) > 1:
|
||||
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",
|
||||
)
|
||||
|
||||
model_list = await get_all_models()
|
||||
models = {model["id"]: model for model in model_list}
|
||||
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
if model_id not in models:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_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
|
||||
# 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)
|
||||
|
||||
model = app.state.MODELS[task_model_id]
|
||||
model = models[task_model_id]
|
||||
|
||||
if 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,
|
||||
**(
|
||||
{"max_tokens": 30}
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
||||
if models[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 30,
|
||||
}
|
||||
|
|
@ -1738,7 +1814,7 @@ Search Query:"""
|
|||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
payload = filter_pipeline(payload, user, models)
|
||||
except Exception as e:
|
||||
if len(e.args) > 1:
|
||||
return JSONResponse(
|
||||
|
|
@ -1760,8 +1836,11 @@ Search Query:"""
|
|||
async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
|
||||
print("generate_emoji")
|
||||
|
||||
model_list = await get_all_models()
|
||||
models = {model["id"]: model for model in model_list}
|
||||
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
if model_id not in models:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_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
|
||||
# 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)
|
||||
|
||||
model = app.state.MODELS[task_model_id]
|
||||
model = models[task_model_id]
|
||||
|
||||
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., 😊, 😢, 😡, 😱).
|
||||
|
|
@ -1794,7 +1878,7 @@ Message: """{{prompt}}"""
|
|||
"stream": False,
|
||||
**(
|
||||
{"max_tokens": 4}
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
||||
if models[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 4,
|
||||
}
|
||||
|
|
@ -1806,7 +1890,7 @@ Message: """{{prompt}}"""
|
|||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
payload = filter_pipeline(payload, user, models)
|
||||
except Exception as e:
|
||||
if len(e.args) > 1:
|
||||
return JSONResponse(
|
||||
|
|
@ -1828,8 +1912,11 @@ Message: """{{prompt}}"""
|
|||
async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)):
|
||||
print("generate_moa_response")
|
||||
|
||||
model_list = await get_all_models()
|
||||
models = {model["id"]: model for model in model_list}
|
||||
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
if model_id not in models:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_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
|
||||
# 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)
|
||||
|
||||
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}}"
|
||||
|
||||
|
|
@ -1867,7 +1959,7 @@ Responses from models: {{responses}}"""
|
|||
log.debug(payload)
|
||||
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
payload = filter_pipeline(payload, user, models)
|
||||
except Exception as e:
|
||||
if len(e.args) > 1:
|
||||
return JSONResponse(
|
||||
|
|
@ -1897,7 +1989,7 @@ Responses from models: {{responses}}"""
|
|||
|
||||
@app.get("/api/pipelines/list")
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
backend/open_webui/utils/access_control.py
Normal file
95
backend/open_webui/utils/access_control.py
Normal file
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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.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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -32,15 +32,16 @@ def apply_extra_params_to_tool_function(
|
|||
def get_tools(
|
||||
webui_app, tool_ids: list[str], user: UserModel, extra_params: dict
|
||||
) -> dict[str, dict]:
|
||||
tools = {}
|
||||
tools_dict = {}
|
||||
|
||||
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
|
||||
|
||||
module = webui_app.state.TOOLS.get(tool_id, 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
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
for spec in toolkit.specs:
|
||||
for spec in tools.specs:
|
||||
# TODO: Fix hack for OpenAI API
|
||||
for val in spec.get("parameters", {}).get("properties", {}).values():
|
||||
if val["type"] == "str":
|
||||
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"]
|
||||
|
||||
# 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
|
||||
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:
|
||||
tools[function_name] = tool_dict
|
||||
return tools
|
||||
tools_dict[function_name] = tool_dict
|
||||
|
||||
return tools_dict
|
||||
|
||||
|
||||
def doc_to_dict(docstring):
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import logging
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Optional, Union
|
||||
|
||||
import jwt
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Optional, Union, List, Dict
|
||||
|
||||
|
||||
from open_webui.apps.webui.models.users import Users
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.env import WEBUI_SECRET_KEY
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, Response, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from passlib.context import CryptContext
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ passlib[bcrypt]==1.7.4
|
|||
requests==2.32.3
|
||||
aiohttp==3.10.8
|
||||
async-timeout
|
||||
aiocache
|
||||
|
||||
sqlalchemy==2.0.32
|
||||
alembic==1.13.2
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ dependencies = [
|
|||
"requests==2.32.3",
|
||||
"aiohttp==3.10.8",
|
||||
"async-timeout",
|
||||
"aiocache",
|
||||
|
||||
"sqlalchemy==2.0.32",
|
||||
"alembic==1.13.2",
|
||||
|
|
|
|||
163
src/lib/apis/groups/index.ts
Normal file
163
src/lib/apis/groups/index.ts
Normal file
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
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;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/models`, {
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/models${
|
||||
base ? '/base' : ''
|
||||
}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
@ -16,36 +17,21 @@ export const getModels = async (token: string = '') => {
|
|||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let models = res?.data ?? [];
|
||||
|
||||
models = models
|
||||
.filter((models) => models)
|
||||
// Sort the models
|
||||
.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
|
||||
const lowerA = a.name.toLowerCase();
|
||||
const lowerB = b.name.toLowerCase();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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;
|
||||
|
||||
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({
|
||||
name: name,
|
||||
description: description
|
||||
description: description,
|
||||
access_control: accessControl
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
|
@ -32,7 +33,7 @@ export const createNewKnowledge = async (token: string, name: string, descriptio
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getKnowledgeItems = async (token: string = '') => {
|
||||
export const getKnowledgeBases = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/`, {
|
||||
|
|
@ -63,6 +64,37 @@ export const getKnowledgeItems = async (token: string = '') => {
|
|||
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) => {
|
||||
let error = null;
|
||||
|
||||
|
|
@ -99,6 +131,7 @@ type KnowledgeUpdateForm = {
|
|||
name?: string;
|
||||
description?: string;
|
||||
data?: object;
|
||||
access_control?: null|object;
|
||||
};
|
||||
|
||||
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({
|
||||
name: form?.name ? form.name : 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) => {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,7 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const addNewModel = async (token: string, model: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(model)
|
||||
})
|
||||
.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 getModelInfos = async (token: string = '') => {
|
||||
export const getModels = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models`, {
|
||||
|
|
@ -60,13 +32,79 @@ export const getModelInfos = async (token: string = '') => {
|
|||
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',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(model)
|
||||
})
|
||||
.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 getModelById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('id', id);
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models?${searchParams.toString()}`, {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
@ -95,13 +133,50 @@ export const getModelById = async (token: string, id: string) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
|
||||
export const toggleModelById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('id', id);
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}/toggle`, {
|
||||
method: 'POST',
|
||||
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 updateModelById = async (token: string, id: string, model: object) => {
|
||||
let error = null;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
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',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
@ -137,7 +212,7 @@ export const deleteModelById = async (token: string, id: string) => {
|
|||
const searchParams = new URLSearchParams();
|
||||
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',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
|
|||
|
|
@ -211,10 +211,12 @@ export const getOllamaVersion = async (token: string, urlIdx?: number) => {
|
|||
return res?.version ?? false;
|
||||
};
|
||||
|
||||
export const getOllamaModels = async (token: string = '') => {
|
||||
export const getOllamaModels = async (token: string = '', urlIdx: null|number = 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',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
|
||||
type PromptItem = {
|
||||
command: string;
|
||||
title: string;
|
||||
content: string;
|
||||
access_control: null|object;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const createNewPrompt = async (
|
||||
token: string,
|
||||
command: string,
|
||||
title: string,
|
||||
content: string
|
||||
prompt: PromptItem
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
|
|
@ -16,9 +24,8 @@ export const createNewPrompt = async (
|
|||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
command: `/${command}`,
|
||||
title: title,
|
||||
content: content
|
||||
...prompt,
|
||||
command: `/${prompt.command}`,
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
|
@ -69,6 +76,39 @@ export const getPrompts = async (token: string = '') => {
|
|||
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) => {
|
||||
let error = null;
|
||||
|
||||
|
|
@ -101,15 +141,15 @@ export const getPromptByCommand = async (token: string, command: string) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const updatePromptByCommand = async (
|
||||
token: string,
|
||||
command: string,
|
||||
title: string,
|
||||
content: string
|
||||
prompt: PromptItem
|
||||
) => {
|
||||
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',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
@ -117,9 +157,8 @@ export const updatePromptByCommand = async (
|
|||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
command: `/${command}`,
|
||||
title: title,
|
||||
content: content
|
||||
...prompt,
|
||||
command: `/${prompt.command}`,
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,39 @@ export const getTools = async (token: string = '') => {
|
|||
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 = '') => {
|
||||
let error = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
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/permissions/user`, {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/groups`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -28,10 +29,39 @@ export const getUserPermissions = async (token: string) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const updateUserPermissions = async (token: string, permissions: object) => {
|
||||
|
||||
|
||||
export const getUserDefaultPermissions = async (token: string) => {
|
||||
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',
|
||||
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 updateUserDefaultPermissions = async (token: string, permissions: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/default/permissions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
import { WEBUI_NAME, config, functions, models } from '$lib/stores';
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
|
|
@ -25,13 +24,14 @@
|
|||
import FunctionMenu from './Functions/FunctionMenu.svelte';
|
||||
import EllipsisHorizontal from '../icons/EllipsisHorizontal.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 DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||
import Search from '../icons/Search.svelte';
|
||||
import Plus from '../icons/Plus.svelte';
|
||||
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
id: `${_function.id}_clone`,
|
||||
name: `${_function.name} (Clone)`
|
||||
});
|
||||
goto('/workspace/functions/create');
|
||||
goto('/admin/functions/create');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -210,7 +210,7 @@
|
|||
<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/functions/create"
|
||||
href="/admin/functions/create"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</a>
|
||||
|
|
@ -225,7 +225,7 @@
|
|||
>
|
||||
<a
|
||||
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-1 self-center pl-1">
|
||||
|
|
@ -322,7 +322,7 @@
|
|||
<FunctionMenu
|
||||
{func}
|
||||
editHandler={() => {
|
||||
goto(`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`);
|
||||
goto(`/admin/functions/edit?id=${encodeURIComponent(func.id)}`);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
shareHandler(func);
|
||||
|
|
@ -452,40 +452,27 @@
|
|||
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<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')}
|
||||
</div>
|
||||
|
||||
<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"
|
||||
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=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore custom functions')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -305,7 +305,7 @@ class Pipe:
|
|||
<button
|
||||
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
||||
on:click={() => {
|
||||
goto('/workspace/functions');
|
||||
goto('/admin/functions');
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -327,7 +327,7 @@
|
|||
</button>
|
||||
</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'}
|
||||
<General
|
||||
saveHandler={async () => {
|
||||
|
|
|
|||
|
|
@ -302,6 +302,7 @@
|
|||
<OllamaConnection
|
||||
bind:url
|
||||
bind:config={OLLAMA_API_CONFIGS[url]}
|
||||
{idx}
|
||||
onSubmit={() => {
|
||||
updateOllamaHandler();
|
||||
}}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -4,15 +4,20 @@
|
|||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Cog6 from '$lib/components/icons/Cog6.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 onSubmit = () => {};
|
||||
|
||||
export let url = '';
|
||||
export let idx = 0;
|
||||
export let config = {};
|
||||
|
||||
let showManageModal = false;
|
||||
let showConfigModal = false;
|
||||
</script>
|
||||
|
||||
|
|
@ -33,6 +38,8 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<ManageOllamaModal bind:show={showManageModal} urlIdx={idx} />
|
||||
|
||||
<div class="flex gap-1.5">
|
||||
<Tooltip
|
||||
className="w-full relative"
|
||||
|
|
@ -55,6 +62,18 @@
|
|||
</Tooltip>
|
||||
|
||||
<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">
|
||||
<button
|
||||
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
} from '$lib/apis/retrieval';
|
||||
|
||||
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 ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
|
@ -312,7 +312,7 @@
|
|||
{#if embeddingEngine === 'openai'}
|
||||
<div class="my-0.5 flex gap-2">
|
||||
<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')}
|
||||
bind:value={OpenAIUrl}
|
||||
required
|
||||
|
|
@ -376,19 +376,12 @@
|
|||
{#if embeddingEngine === 'ollama'}
|
||||
<div class="flex w-full">
|
||||
<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"
|
||||
bind:value={embeddingModel}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
placeholder={$i18n.t('Set embedding model')}
|
||||
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>
|
||||
{:else}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
|
@ -1,14 +1,38 @@
|
|||
<script>
|
||||
import { getContext, tick, onMount } from 'svelte';
|
||||
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 Groups from './Users/Groups.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let selectedTab = 'overview';
|
||||
let users = [];
|
||||
|
||||
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');
|
||||
|
||||
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
|
||||
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
|
||||
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">
|
||||
{#if selectedTab === 'overview'}
|
||||
<UserList />
|
||||
<UserList {users} />
|
||||
{:else if selectedTab === 'groups'}
|
||||
<Groups />
|
||||
<Groups {users} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,16 +7,30 @@
|
|||
import { onMount, getContext } from 'svelte';
|
||||
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 Tooltip from '$lib/components/common/Tooltip.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');
|
||||
|
||||
let loaded = false;
|
||||
|
||||
export let users = [];
|
||||
|
||||
let groups = [];
|
||||
let filteredGroups;
|
||||
|
||||
|
|
@ -31,20 +45,69 @@
|
|||
});
|
||||
|
||||
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 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 () => {
|
||||
if ($user?.role !== 'admin') {
|
||||
await goto('/');
|
||||
} else {
|
||||
groups = [];
|
||||
await setGroups();
|
||||
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
|
||||
}
|
||||
loaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#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="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Groups')}
|
||||
|
|
@ -117,7 +180,58 @@
|
|||
</div>
|
||||
</div>
|
||||
{: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}
|
||||
|
||||
<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>
|
||||
{/if}
|
||||
|
|
|
|||
149
src/lib/components/admin/Users/Groups/AddGroupModal.svelte
Normal file
149
src/lib/components/admin/Users/Groups/AddGroupModal.svelte
Normal file
|
|
@ -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
src/lib/components/admin/Users/Groups/Display.svelte
Normal file
61
src/lib/components/admin/Users/Groups/Display.svelte
Normal file
|
|
@ -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
src/lib/components/admin/Users/Groups/EditGroupModal.svelte
Normal file
328
src/lib/components/admin/Users/Groups/EditGroupModal.svelte
Normal file
|
|
@ -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
src/lib/components/admin/Users/Groups/GroupItem.svelte
Normal file
84
src/lib/components/admin/Users/Groups/GroupItem.svelte
Normal file
|
|
@ -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
src/lib/components/admin/Users/Groups/Permissions.svelte
Normal file
204
src/lib/components/admin/Users/Groups/Permissions.svelte
Normal file
|
|
@ -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
src/lib/components/admin/Users/Groups/Users.svelte
Normal file
122
src/lib/components/admin/Users/Groups/Users.svelte
Normal file
|
|
@ -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>
|
||||
|
|
@ -29,9 +29,7 @@
|
|||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let loaded = false;
|
||||
let tab = '';
|
||||
let users = [];
|
||||
export let users = [];
|
||||
|
||||
let search = '';
|
||||
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 sortOrder = 'asc'; // default sort order
|
||||
|
||||
|
|
@ -131,278 +121,301 @@
|
|||
/>
|
||||
<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>
|
||||
<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
|
||||
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')}
|
||||
<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">
|
||||
{#if sortKey === 'role'}
|
||||
<span class="font-normal"
|
||||
>{#if sortOrder === 'asc'}
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
</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>
|
||||
{: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')}
|
||||
|
||||
<Tooltip content={$i18n.t('Edit User')}>
|
||||
{#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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
</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 () => {
|
||||
showEditUserModal = !showEditUserModal;
|
||||
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
|
||||
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;
|
||||
}}
|
||||
>
|
||||
|
|
@ -417,49 +430,22 @@
|
|||
<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"
|
||||
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>
|
||||
|
||||
{#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} />
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@
|
|||
mobile,
|
||||
showOverview,
|
||||
chatTitle,
|
||||
showArtifacts
|
||||
showArtifacts,
|
||||
tools
|
||||
} from '$lib/stores';
|
||||
import {
|
||||
convertMessagesToHistory,
|
||||
|
|
@ -78,6 +79,7 @@
|
|||
import ChatControls from './ChatControls.svelte';
|
||||
import EventConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||
import Placeholder from './Placeholder.svelte';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
|
||||
export let chatIdProp = '';
|
||||
|
||||
|
|
@ -153,6 +155,26 @@
|
|||
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 _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
let _messageId = JSON.parse(JSON.stringify(message.id));
|
||||
|
|
@ -480,8 +502,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
console.log(selectedModels);
|
||||
|
||||
await showControls.set(false);
|
||||
await showCallOverlay.set(false);
|
||||
await showOverview.set(false);
|
||||
|
|
@ -815,9 +835,12 @@
|
|||
console.log('submitPrompt', userPrompt, $chatId);
|
||||
|
||||
const messages = createMessagesList(history.currentId);
|
||||
selectedModels = selectedModels.map((modelId) =>
|
||||
const _selectedModels = selectedModels.map((modelId) =>
|
||||
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
||||
);
|
||||
if (JSON.stringify(selectedModels) !== JSON.stringify(_selectedModels)) {
|
||||
selectedModels = _selectedModels;
|
||||
}
|
||||
|
||||
if (userPrompt === '') {
|
||||
toast.error($i18n.t('Please enter a prompt'));
|
||||
|
|
@ -2267,13 +2290,6 @@
|
|||
bind:selectedToolIds
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
|
||||
const model = $models.find((m) => m.id === e);
|
||||
if (model?.info?.meta?.toolIds ?? false) {
|
||||
return [...new Set([...a, ...model.info.meta.toolIds])];
|
||||
}
|
||||
return a;
|
||||
}, [])}
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
{stopResponse}
|
||||
{createMessagePair}
|
||||
|
|
@ -2311,13 +2327,6 @@
|
|||
bind:selectedToolIds
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
|
||||
const model = $models.find((m) => m.id === e);
|
||||
if (model?.info?.meta?.toolIds ?? false) {
|
||||
return [...new Set([...a, ...model.info.meta.toolIds])];
|
||||
}
|
||||
return a;
|
||||
}, [])}
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
{stopResponse}
|
||||
{createMessagePair}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { removeLastWordFromString } from '$lib/utils';
|
||||
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 Knowledge from './Commands/Knowledge.svelte';
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
prompts.set(await getPrompts(localStorage.token));
|
||||
})(),
|
||||
(async () => {
|
||||
knowledge.set(await getKnowledgeItems(localStorage.token));
|
||||
knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||
})()
|
||||
]);
|
||||
loading = false;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
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 { getTools } from '$lib/apis/tools';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
|
@ -11,17 +12,13 @@
|
|||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let uploadFilesHandler: Function;
|
||||
|
||||
export let availableToolIds: string[] = [];
|
||||
export let selectedToolIds: string[] = [];
|
||||
|
||||
export let webSearchEnabled: boolean;
|
||||
|
||||
export let onClose: Function;
|
||||
|
||||
let tools = {};
|
||||
|
|
@ -31,24 +28,17 @@
|
|||
init();
|
||||
}
|
||||
|
||||
$: if (tools) {
|
||||
selectedToolIds = Object.keys(tools).filter((toolId) => tools[toolId]?.enabled ?? false);
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
console.log('init');
|
||||
if ($_tools === null) {
|
||||
await _tools.set(await getTools(localStorage.token));
|
||||
}
|
||||
|
||||
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;
|
||||
}, {});
|
||||
};
|
||||
|
|
@ -97,7 +87,18 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -729,7 +729,7 @@
|
|||
|
||||
{#if message.done}
|
||||
{#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">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
|
|
@ -1125,19 +1125,17 @@
|
|||
showRateComment = false;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
model: model
|
||||
}))}
|
||||
showTemporaryChatControl={$user.role === 'user'
|
||||
? ($config?.permissions?.chat?.temporary ?? true)
|
||||
? ($user?.permissions?.chat?.temporary ?? true)
|
||||
: true}
|
||||
bind:value={selectedModel}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -55,17 +55,15 @@
|
|||
let selectedModelIdx = 0;
|
||||
|
||||
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'],
|
||||
threshold: 0.3
|
||||
|
|
@ -76,7 +74,7 @@
|
|||
? fuse.search(searchValue).map((e) => {
|
||||
return e.item;
|
||||
})
|
||||
: items.filter((item) => !item.model?.info?.meta?.hidden);
|
||||
: items;
|
||||
|
||||
const pullModelHandler = async () => {
|
||||
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
||||
|
|
@ -583,14 +581,3 @@
|
|||
</slot>
|
||||
</DropdownMenu.Content>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
export let prompt = '';
|
||||
export let files = [];
|
||||
export let availableToolIds = [];
|
||||
|
||||
export let selectedToolIds = [];
|
||||
export let webSearchEnabled = false;
|
||||
|
||||
|
|
@ -200,7 +200,6 @@
|
|||
bind:selectedToolIds
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
{availableToolIds}
|
||||
{transparentBackground}
|
||||
{stopResponse}
|
||||
{createMessagePair}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@
|
|||
export let state = true;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: dispatch('change', state);
|
||||
</script>
|
||||
|
||||
<Switch.Root
|
||||
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
|
||||
? ' bg-emerald-600'
|
||||
: 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800"
|
||||
|
|
|
|||
19
src/lib/components/icons/LockClosed.svelte
Normal file
19
src/lib/components/icons/LockClosed.svelte
Normal file
|
|
@ -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
src/lib/components/icons/UserCircleSolid.svelte
Normal file
11
src/lib/components/icons/UserCircleSolid.svelte
Normal file
|
|
@ -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
src/lib/components/icons/UserPlusSolid.svelte
Normal file
9
src/lib/components/icons/UserPlusSolid.svelte
Normal file
|
|
@ -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
src/lib/components/icons/UsersSolid.svelte
Normal file
9
src/lib/components/icons/UsersSolid.svelte
Normal file
|
|
@ -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
src/lib/components/icons/Wrench.svelte
Normal file
20
src/lib/components/icons/Wrench.svelte
Normal file
|
|
@ -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>
|
||||
|
|
@ -470,7 +470,7 @@
|
|||
</button>
|
||||
</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">
|
||||
<a
|
||||
class="flex-grow flex space-x-3 rounded-lg px-2 py-[7px] hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
|
|
|
|||
|
|
@ -10,20 +10,22 @@
|
|||
const i18n = getContext('i18n');
|
||||
|
||||
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 Tooltip from '../common/Tooltip.svelte';
|
||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||
import Pencil from '../icons/Pencil.svelte';
|
||||
|
||||
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||
import ItemMenu from './Knowledge/ItemMenu.svelte';
|
||||
import Badge from '../common/Badge.svelte';
|
||||
import Search from '../icons/Search.svelte';
|
||||
import Plus from '../icons/Plus.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
|
||||
let loaded = false;
|
||||
|
||||
let query = '';
|
||||
let selectedItem = null;
|
||||
|
|
@ -31,13 +33,21 @@
|
|||
|
||||
let fuse = null;
|
||||
|
||||
let knowledgeBases = [];
|
||||
let filteredItems = [];
|
||||
|
||||
$: if (knowledgeBases) {
|
||||
fuse = new Fuse(knowledgeBases, {
|
||||
keys: ['name', 'description']
|
||||
});
|
||||
}
|
||||
|
||||
$: if (fuse) {
|
||||
filteredItems = query
|
||||
? fuse.search(query).map((e) => {
|
||||
return e.item;
|
||||
})
|
||||
: $knowledge;
|
||||
: knowledgeBases;
|
||||
}
|
||||
|
||||
const deleteHandler = async (item) => {
|
||||
|
|
@ -46,19 +56,15 @@
|
|||
});
|
||||
|
||||
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.'));
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
|
|
@ -68,104 +74,110 @@
|
|||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
on:confirm={() => {
|
||||
deleteHandler(selectedItem);
|
||||
}}
|
||||
/>
|
||||
{#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 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 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>
|
||||
</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>
|
||||
<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 class="my-3 mb-5 grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||
{#each filteredItems as item}
|
||||
<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={() => {
|
||||
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" />
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</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}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@
|
|||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { createNewKnowledge, getKnowledgeItems } from '$lib/apis/knowledge';
|
||||
import { createNewKnowledge, getKnowledgeBases } from '$lib/apis/knowledge';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { knowledge } from '$lib/stores';
|
||||
import AccessControl from '../common/AccessControl.svelte';
|
||||
|
||||
let loading = false;
|
||||
|
||||
let name = '';
|
||||
let description = '';
|
||||
let accessControl = null;
|
||||
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
|
|
@ -23,13 +25,18 @@
|
|||
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);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Knowledge created successfully.'));
|
||||
knowledge.set(await getKnowledgeItems(localStorage.token));
|
||||
knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||
goto(`/workspace/knowledge/${res.id}`);
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +110,12 @@
|
|||
</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>
|
||||
<button
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
import {
|
||||
addFileToKnowledgeById,
|
||||
getKnowledgeById,
|
||||
getKnowledgeItems,
|
||||
getKnowledgeBases,
|
||||
removeFileFromKnowledgeById,
|
||||
resetKnowledgeById,
|
||||
updateFileFromKnowledgeById,
|
||||
|
|
@ -27,18 +27,19 @@
|
|||
import { processFile } from '$lib/apis/retrieval';
|
||||
|
||||
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 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 RichTextInput from '$lib/components/common/RichTextInput.svelte';
|
||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||
import Drawer from '$lib/components/common/Drawer.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import MenuLines from '$lib/components/icons/MenuLines.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
|
||||
let largeScreen = true;
|
||||
|
||||
|
|
@ -62,6 +63,7 @@
|
|||
|
||||
let showAddTextContentModal = false;
|
||||
let showSyncConfirmModal = false;
|
||||
let showAccessControlModal = false;
|
||||
|
||||
let inputFiles = null;
|
||||
|
||||
|
|
@ -420,14 +422,15 @@
|
|||
|
||||
const res = await updateKnowledgeById(localStorage.token, id, {
|
||||
name: knowledge.name,
|
||||
description: knowledge.description
|
||||
description: knowledge.description,
|
||||
access_control: knowledge.access_control
|
||||
}).catch((e) => {
|
||||
toast.error(e);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Knowledge updated successfully'));
|
||||
_knowledge.set(await getKnowledgeItems(localStorage.token));
|
||||
_knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||
}
|
||||
}, 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}
|
||||
<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">
|
||||
<PaneGroup direction="horizontal">
|
||||
<Pane
|
||||
|
|
@ -687,7 +745,17 @@
|
|||
/>
|
||||
</div>
|
||||
{: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}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -753,41 +821,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="m-auto pb-32">
|
||||
<div>
|
||||
<div class=" flex w-full mt-1 mb-3.5">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between w-full px-0.5 mb-1">
|
||||
<div class="w-full">
|
||||
<input
|
||||
type="text"
|
||||
class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
|
||||
bind:value={knowledge.name}
|
||||
on:input={() => {
|
||||
changeDebounceHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex 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}
|
||||
</div>
|
||||
</Pane>
|
||||
|
|
@ -8,12 +8,17 @@
|
|||
const { saveAs } = fileSaver;
|
||||
|
||||
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';
|
||||
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';
|
||||
|
||||
|
|
@ -24,67 +29,52 @@
|
|||
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||
import Search from '../icons/Search.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 showModelDeleteConfirm = false;
|
||||
|
||||
let localModelfiles = [];
|
||||
|
||||
let importFiles;
|
||||
let modelsImportInputElement: HTMLInputElement;
|
||||
let loaded = false;
|
||||
|
||||
let _models = [];
|
||||
let models = [];
|
||||
|
||||
let filteredModels = [];
|
||||
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 = '';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const res = await deleteModelById(localStorage.token, model.id);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
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) => {
|
||||
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) => {
|
||||
|
|
@ -108,58 +98,6 @@
|
|||
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) => {
|
||||
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) => {
|
||||
|
|
@ -210,60 +148,10 @@
|
|||
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 () => {
|
||||
// Legacy code to sync localModelfiles with models
|
||||
_models = $models;
|
||||
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
|
||||
models = await getWorkspaceModels(localStorage.token);
|
||||
|
||||
if (localModelfiles) {
|
||||
console.log(localModelfiles);
|
||||
}
|
||||
|
||||
if (!$mobile) {
|
||||
// SortableJS
|
||||
sortable = new Sortable(document.getElementById('model-list'), {
|
||||
animation: 150,
|
||||
onUpdate: async (event) => {
|
||||
console.log(event);
|
||||
positionChangeHandler();
|
||||
}
|
||||
});
|
||||
}
|
||||
loaded = true;
|
||||
|
||||
const onKeyDown = (event) => {
|
||||
if (event.key === 'Shift') {
|
||||
|
|
@ -299,356 +187,276 @@
|
|||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<ModelDeleteConfirmDialog
|
||||
bind:show={showModelDeleteConfirm}
|
||||
on:confirm={() => {
|
||||
deleteModelHandler(selectedModel);
|
||||
}}
|
||||
/>
|
||||
{#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 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>
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
</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>
|
||||
|
||||
<div
|
||||
class=" flex-1 self-center {(model?.info?.meta?.hidden ?? false) ? 'text-gray-500' : ''}"
|
||||
<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
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
</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={() => {
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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-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 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"
|
||||
>
|
||||
<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
|
||||
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>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<ModelMenu
|
||||
user={$user}
|
||||
{model}
|
||||
shareHandler={() => {
|
||||
shareModelHandler(model);
|
||||
}}
|
||||
cloneHandler={() => {
|
||||
cloneModelHandler(model);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportModelHandler(model);
|
||||
}}
|
||||
hideHandler={() => {
|
||||
hideModelHandler(model);
|
||||
}}
|
||||
>
|
||||
{#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}
|
||||
</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);
|
||||
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)}`}
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
</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>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if localModelfiles.length > 0}
|
||||
<div class="flex">
|
||||
<div class=" self-center text-sm font-medium mr-4">
|
||||
{localModelfiles.length} Local Modelfiles Detected
|
||||
</div>
|
||||
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class=" flex justify-end w-full mb-3">
|
||||
<div class="flex space-x-1">
|
||||
<button
|
||||
class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
|
||||
on:click={async () => {
|
||||
downloadModels(localModelfiles);
|
||||
<input
|
||||
id="models-import-input"
|
||||
bind:this={modelsImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
hidden
|
||||
on:change={() => {
|
||||
console.log(importFiles);
|
||||
|
||||
localStorage.removeItem('modelfiles');
|
||||
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
|
||||
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
|
||||
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
|
||||
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 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>
|
||||
|
|
@ -656,44 +464,35 @@
|
|||
</div>
|
||||
</div>
|
||||
{/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')}
|
||||
{#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 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>
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</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>
|
||||
</a>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
}}
|
||||
>
|
||||
<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
|
||||
>
|
||||
</Selector>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
<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 { 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 Tags from '$lib/components/common/Tags.svelte';
|
||||
|
|
@ -16,14 +12,20 @@
|
|||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
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');
|
||||
|
||||
export let onSubmit: Function;
|
||||
export let onBack: null | Function = null;
|
||||
|
||||
export let model = null;
|
||||
export let edit = false;
|
||||
|
||||
export let preset = true;
|
||||
|
||||
let loading = false;
|
||||
let success = false;
|
||||
|
||||
|
|
@ -77,12 +79,14 @@
|
|||
let filterIds = [];
|
||||
let actionIds = [];
|
||||
|
||||
let accessControl = null;
|
||||
|
||||
const addUsage = (base_model_id) => {
|
||||
const baseModel = $models.find((m) => m.id === base_model_id);
|
||||
|
||||
if (baseModel) {
|
||||
if (baseModel.owned_by === 'openai') {
|
||||
capabilities.usage = baseModel.info?.meta?.capabilities?.usage ?? false;
|
||||
capabilities.usage = baseModel?.meta?.capabilities?.usage ?? false;
|
||||
} else {
|
||||
delete capabilities.usage;
|
||||
}
|
||||
|
|
@ -95,6 +99,8 @@
|
|||
|
||||
info.id = id;
|
||||
info.name = name;
|
||||
|
||||
info.access_control = accessControl;
|
||||
info.meta.capabilities = capabilities;
|
||||
|
||||
if (knowledge.length > 0) {
|
||||
|
|
@ -145,7 +151,7 @@
|
|||
onMount(async () => {
|
||||
await tools.set(await getTools(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
|
||||
const workspaceContainer = document.getElementById('workspace-container');
|
||||
|
|
@ -154,38 +160,37 @@
|
|||
}
|
||||
|
||||
if (model) {
|
||||
console.log(model);
|
||||
name = model.name;
|
||||
await tick();
|
||||
|
||||
id = model.id;
|
||||
|
||||
if (model.info.base_model_id) {
|
||||
if (model.base_model_id) {
|
||||
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);
|
||||
|
||||
if (base_model) {
|
||||
model.info.base_model_id = base_model.id;
|
||||
model.base_model_id = base_model.id;
|
||||
} 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
|
||||
? (typeof params.stop === 'string' ? params.stop.split(',') : (params?.stop ?? [])).join(
|
||||
','
|
||||
)
|
||||
: 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) {
|
||||
return {
|
||||
id: item.collection_name,
|
||||
|
|
@ -203,17 +208,22 @@
|
|||
return item;
|
||||
}
|
||||
});
|
||||
capabilities = { ...capabilities, ...(model?.info?.meta?.capabilities ?? {}) };
|
||||
capabilities = { ...capabilities, ...(model?.meta?.capabilities ?? {}) };
|
||||
if (model?.owned_by === 'openai') {
|
||||
capabilities.usage = false;
|
||||
}
|
||||
|
||||
accessControl = model?.access_control ?? null;
|
||||
|
||||
console.log(model?.access_control);
|
||||
console.log(accessControl);
|
||||
|
||||
info = {
|
||||
...info,
|
||||
...JSON.parse(
|
||||
JSON.stringify(
|
||||
model?.info
|
||||
? model?.info
|
||||
model
|
||||
? model
|
||||
: {
|
||||
id: model.id,
|
||||
name: model.name
|
||||
|
|
@ -230,6 +240,31 @@
|
|||
</script>
|
||||
|
||||
{#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">
|
||||
<input
|
||||
bind:this={filesInputElement}
|
||||
|
|
@ -298,7 +333,7 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
{#if !edit || model}
|
||||
{#if !edit || (edit && model)}
|
||||
<form
|
||||
class="flex flex-col md:flex-row w-full gap-3 md:gap-6"
|
||||
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">
|
||||
<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"
|
||||
on:click={() => {
|
||||
filesInputElement.click();
|
||||
|
|
@ -318,13 +353,13 @@
|
|||
<img
|
||||
src={info.meta.profile_image_url}
|
||||
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}
|
||||
<img
|
||||
src="/static/favicon.png"
|
||||
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}
|
||||
|
||||
|
|
@ -383,7 +418,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{#if !edit || model.preset}
|
||||
{#if preset}
|
||||
<div class="my-1">
|
||||
<div class=" text-sm font-semibold mb-1">{$i18n.t('Base Model (From)')}</div>
|
||||
|
||||
|
|
@ -441,7 +476,33 @@
|
|||
{/if}
|
||||
</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="flex w-full justify-between">
|
||||
|
|
@ -495,7 +556,7 @@
|
|||
</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="flex w-full justify-between items-center">
|
||||
|
|
@ -592,7 +653,7 @@
|
|||
{/if}
|
||||
</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">
|
||||
<Knowledge bind:selectedKnowledge={knowledge} collections={$knowledgeCollections} />
|
||||
|
|
@ -620,30 +681,6 @@
|
|||
<Capabilities bind:capabilities />
|
||||
</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="flex w-full justify-between mb-2">
|
||||
<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">
|
||||
<button
|
||||
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"
|
||||
disabled={loading}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@
|
|||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let user;
|
||||
export let model;
|
||||
|
||||
export let shareHandler: Function;
|
||||
export let cloneHandler: Function;
|
||||
export let exportHandler: Function;
|
||||
|
||||
export let moveToTopHandler: Function;
|
||||
export let hideHandler: Function;
|
||||
export let deleteHandler: Function;
|
||||
export let onClose: Function;
|
||||
|
|
@ -82,69 +82,6 @@
|
|||
<div class="flex items-center">{$i18n.t('Export')}</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={() => {
|
||||
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" />
|
||||
|
||||
<DropdownMenu.Item
|
||||
|
|
|
|||
|
|
@ -3,28 +3,39 @@
|
|||
import fileSaver from 'file-saver';
|
||||
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 { 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 EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import Search from '../icons/Search.svelte';
|
||||
import Plus from '../icons/Plus.svelte';
|
||||
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
let promptsImportInputElement: HTMLInputElement;
|
||||
let loaded = false;
|
||||
|
||||
let importFiles = '';
|
||||
let query = '';
|
||||
let promptsImportInputElement: HTMLInputElement;
|
||||
|
||||
let prompts = [];
|
||||
|
||||
let showDeleteConfirm = false;
|
||||
let deletePrompt = null;
|
||||
|
||||
let filteredItems = [];
|
||||
$: filteredItems = $prompts.filter((p) => query === '' || p.command.includes(query));
|
||||
$: filteredItems = prompts.filter((p) => query === '' || p.command.includes(query));
|
||||
|
||||
const shareHandler = async (prompt) => {
|
||||
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
||||
|
|
@ -59,8 +70,18 @@
|
|||
const deleteHandler = async (prompt) => {
|
||||
const command = prompt.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>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -69,251 +90,239 @@
|
|||
</title>
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
<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"
|
||||
<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>
|
||||
<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"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
<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"
|
||||
href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}
|
||||
>
|
||||
<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>
|
||||
|
||||
<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"
|
||||
type="button"
|
||||
>
|
||||
<EllipsisHorizontal className="size-5" />
|
||||
</button>
|
||||
</PromptMenu>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</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);
|
||||
|
||||
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();
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
{#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 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>
|
||||
</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>
|
||||
<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"
|
||||
href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}
|
||||
>
|
||||
<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>
|
||||
|
||||
<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"
|
||||
type="button"
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- <button
|
||||
on:click={() => {
|
||||
loadDefaultPrompts();
|
||||
}}
|
||||
>
|
||||
dd
|
||||
</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>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore custom prompts')}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
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 edit = false;
|
||||
|
|
@ -17,6 +20,10 @@
|
|||
let command = '';
|
||||
let content = '';
|
||||
|
||||
let accessControl = null;
|
||||
|
||||
let showAccessControlModal = false;
|
||||
|
||||
$: if (!edit) {
|
||||
command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : '';
|
||||
}
|
||||
|
|
@ -28,7 +35,8 @@
|
|||
await onSubmit({
|
||||
title,
|
||||
command,
|
||||
content
|
||||
content,
|
||||
access_control: accessControl
|
||||
});
|
||||
} else {
|
||||
toast.error(
|
||||
|
|
@ -54,10 +62,14 @@
|
|||
|
||||
command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command;
|
||||
content = prompt.content;
|
||||
|
||||
accessControl = prompt?.access_control ?? null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<AccessControlModal bind:show={showAccessControlModal} bind:accessControl />
|
||||
|
||||
<div class="w-full max-h-full flex justify-center">
|
||||
<form
|
||||
class="flex flex-col w-full mb-10"
|
||||
|
|
@ -76,13 +88,29 @@
|
|||
placement="bottom-start"
|
||||
>
|
||||
<div class="flex flex-col w-full">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
class="text-2xl font-semibold w-full bg-transparent outline-none"
|
||||
placeholder={$i18n.t('Title')}
|
||||
bind:value={title}
|
||||
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 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">
|
||||
<button
|
||||
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"
|
||||
disabled={loading}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
const { saveAs } = fileSaver;
|
||||
|
||||
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 { goto } from '$app/navigation';
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
deleteToolById,
|
||||
exportTools,
|
||||
getToolById,
|
||||
getToolList,
|
||||
getTools
|
||||
} from '$lib/apis/tools';
|
||||
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
|
||||
|
|
@ -27,10 +28,13 @@
|
|||
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||
import Search from '../icons/Search.svelte';
|
||||
import Plus from '../icons/Plus.svelte';
|
||||
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let shiftKey = false;
|
||||
let loaded = false;
|
||||
|
||||
let toolsImportInputElement: HTMLInputElement;
|
||||
let importFiles;
|
||||
|
|
@ -44,8 +48,10 @@
|
|||
|
||||
let showDeleteConfirm = false;
|
||||
|
||||
let tools = [];
|
||||
let filteredItems = [];
|
||||
$: filteredItems = $tools.filter(
|
||||
|
||||
$: filteredItems = tools.filter(
|
||||
(t) =>
|
||||
query === '' ||
|
||||
t.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
|
|
@ -117,11 +123,20 @@
|
|||
|
||||
if (res) {
|
||||
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) => {
|
||||
if (event.key === 'Shift') {
|
||||
shiftKey = true;
|
||||
|
|
@ -156,347 +171,336 @@
|
|||
</title>
|
||||
</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
|
||||
>
|
||||
</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" />
|
||||
{#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>
|
||||
<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 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>
|
||||
<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 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
|
||||
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>
|
||||
{/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 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>
|
||||
</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
|
||||
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;
|
||||
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>
|
||||
</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={() => {
|
||||
selectedTool = tool;
|
||||
showValvesModal = true;
|
||||
<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={() => {}}
|
||||
>
|
||||
<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>
|
||||
{/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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
{/each}
|
||||
</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-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;
|
||||
}}
|
||||
/>
|
||||
|
||||
<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"
|
||||
<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();
|
||||
}}
|
||||
>
|
||||
<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 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`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
{#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 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>
|
||||
<ChevronRight />
|
||||
</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>
|
||||
</a>
|
||||
|
||||
<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>
|
||||
</ConfirmDialog>
|
||||
{:else}
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</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>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
|
|
|||
|
|
@ -9,12 +9,16 @@
|
|||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.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();
|
||||
|
||||
let formElement = null;
|
||||
let loading = false;
|
||||
|
||||
let showConfirm = false;
|
||||
let showAccessControlModal = false;
|
||||
|
||||
export let edit = false;
|
||||
export let clone = false;
|
||||
|
|
@ -25,6 +29,8 @@
|
|||
description: ''
|
||||
};
|
||||
export let content = '';
|
||||
export let accessControl = null;
|
||||
|
||||
let _content = '';
|
||||
|
||||
$: if (content) {
|
||||
|
|
@ -148,7 +154,8 @@ class Tools:
|
|||
id,
|
||||
name,
|
||||
meta,
|
||||
content
|
||||
content,
|
||||
access_control: accessControl
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -172,6 +179,8 @@ class Tools:
|
|||
};
|
||||
</script>
|
||||
|
||||
<AccessControlModal bind:show={showAccessControlModal} bind:accessControl />
|
||||
|
||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||
<div class="mx-auto w-full md:px-0 h-full">
|
||||
<form
|
||||
|
|
@ -203,11 +212,11 @@ class Tools:
|
|||
</div>
|
||||
|
||||
<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
|
||||
class="w-full text-2xl font-medium bg-transparent outline-none"
|
||||
class="w-full text-2xl font-semibold bg-transparent outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Toolkit Name')}
|
||||
placeholder={$i18n.t('Tool Name')}
|
||||
bind:value={name}
|
||||
required
|
||||
/>
|
||||
|
|
@ -215,7 +224,19 @@ class Tools:
|
|||
</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>
|
||||
|
||||
|
|
@ -225,15 +246,11 @@ class Tools:
|
|||
{id}
|
||||
</div>
|
||||
{: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
|
||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Toolkit ID')}
|
||||
placeholder={$i18n.t('Tool ID')}
|
||||
bind:value={id}
|
||||
required
|
||||
disabled={edit}
|
||||
|
|
@ -243,13 +260,13 @@ class Tools:
|
|||
|
||||
<Tooltip
|
||||
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"
|
||||
>
|
||||
<input
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Toolkit Description')}
|
||||
placeholder={$i18n.t('Tool Description')}
|
||||
bind:value={meta.description}
|
||||
required
|
||||
/>
|
||||
|
|
|
|||
193
src/lib/components/workspace/common/AccessControl.svelte
Normal file
193
src/lib/components/workspace/common/AccessControl.svelte
Normal file
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
import { page } from '$app/stores';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { getKnowledgeItems } from '$lib/apis/knowledge';
|
||||
import { getKnowledgeBases } from '$lib/apis/knowledge';
|
||||
import { getFunctions } from '$lib/apis/functions';
|
||||
import { getModels, getVersionUpdates } from '$lib/apis';
|
||||
import { getAllTags } from '$lib/apis/chats';
|
||||
|
|
|
|||
|
|
@ -65,6 +65,13 @@
|
|||
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
|
||||
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/admin/settings')
|
||||
? ''
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { functions } from '$lib/stores';
|
||||
|
||||
import { getFunctions } from '$lib/apis/functions';
|
||||
import Functions from '$lib/components/workspace/Functions.svelte';
|
||||
import Functions from '$lib/components/admin/Functions.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { functions, models } from '$lib/stores';
|
||||
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 { compareVersion, extractFrontmatter } from '$lib/utils';
|
||||
import { WEBUI_VERSION } from '$lib/constants';
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
import { functions, models } from '$lib/stores';
|
||||
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 { getModels } from '$lib/apis';
|
||||
import { compareVersion, extractFrontmatter } from '$lib/utils';
|
||||
|
|
@ -15,11 +15,6 @@
|
|||
import { goto } from '$app/navigation';
|
||||
|
||||
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');
|
||||
|
||||
|
|
@ -27,7 +22,21 @@
|
|||
|
||||
onMount(async () => {
|
||||
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;
|
||||
|
|
@ -46,7 +55,7 @@
|
|||
? '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="{$showSidebar ? 'md:hidden' : ''} self-center flex flex-none items-center">
|
||||
<button
|
||||
|
|
@ -67,50 +76,51 @@
|
|||
<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"
|
||||
>
|
||||
<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>
|
||||
|
||||
|
|
@ -118,7 +128,7 @@
|
|||
</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 />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
import { onMount } from 'svelte';
|
||||
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';
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
knowledge.set(await getKnowledgeItems(localStorage.token));
|
||||
knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||
})()
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import Collection from '$lib/components/workspace/Knowledge/Collection.svelte';
|
||||
import KnowledgeBase from '$lib/components/workspace/Knowledge/KnowledgeBase.svelte';
|
||||
</script>
|
||||
|
||||
<Collection />
|
||||
<KnowledgeBase />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import CreateCollection from '$lib/components/workspace/Knowledge/CreateCollection.svelte';
|
||||
import CreateKnowledgeBase from '$lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte';
|
||||
</script>
|
||||
|
||||
<CreateCollection />
|
||||
<CreateKnowledgeBase />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { models } from '$lib/stores';
|
||||
|
||||
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 ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
}
|
||||
|
||||
if (modelInfo) {
|
||||
const res = await addNewModel(localStorage.token, {
|
||||
const res = await createNewModel(localStorage.token, {
|
||||
...modelInfo,
|
||||
meta: {
|
||||
...modelInfo.meta,
|
||||
|
|
@ -31,6 +31,9 @@
|
|||
: null
|
||||
},
|
||||
params: { ...modelInfo.params }
|
||||
}).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,20 @@
|
|||
import { page } from '$app/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 ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
|
||||
|
||||
let model = null;
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
const _id = $page.url.searchParams.get('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) {
|
||||
goto('/workspace/models');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { prompts } from '$lib/stores';
|
||||
|
||||
import { getPrompts } from '$lib/apis/prompts';
|
||||
import Prompts from '$lib/components/workspace/Prompts.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
prompts.set(await getPrompts(localStorage.token));
|
||||
})()
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $prompts !== null}
|
||||
<Prompts />
|
||||
{/if}
|
||||
<Prompts />
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import { prompts } from '$lib/stores';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { createNewPrompt, getPrompts } from '$lib/apis/prompts';
|
||||
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
|
||||
|
||||
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) {
|
||||
toast.success($i18n.t('Prompt created successfully'));
|
||||
|
||||
await prompts.set(await getPrompts(localStorage.token));
|
||||
await goto('/workspace/prompts');
|
||||
}
|
||||
|
|
@ -37,7 +38,8 @@
|
|||
prompt = {
|
||||
title: _prompt.title,
|
||||
command: _prompt.command,
|
||||
content: _prompt.content
|
||||
content: _prompt.content,
|
||||
access_control: null
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -51,7 +53,8 @@
|
|||
prompt = {
|
||||
title: _prompt.title,
|
||||
command: _prompt.command,
|
||||
content: _prompt.content
|
||||
content: _prompt.content,
|
||||
access_control: null
|
||||
};
|
||||
sessionStorage.removeItem('prompt');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import { prompts } from '$lib/stores';
|
||||
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 PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
|
||||
|
||||
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) {
|
||||
toast.success($i18n.t('Prompt updated successfully'));
|
||||
await prompts.set(await getPrompts(localStorage.token));
|
||||
await goto('/workspace/prompts');
|
||||
}
|
||||
|
|
@ -27,13 +29,20 @@
|
|||
onMount(async () => {
|
||||
const command = $page.url.searchParams.get('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) {
|
||||
prompt = {
|
||||
title: _prompt.title,
|
||||
command: _prompt.command,
|
||||
content: _prompt.content
|
||||
content: _prompt.content,
|
||||
access_control: _prompt?.access_control ?? null
|
||||
};
|
||||
} else {
|
||||
goto('/workspace/prompts');
|
||||
|
|
|
|||
|
|
@ -1,19 +1,7 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { tools } from '$lib/stores';
|
||||
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
import Tools from '$lib/components/workspace/Tools.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
tools.set(await getTools(localStorage.token));
|
||||
})()
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $tools !== null}
|
||||
<Tools />
|
||||
{/if}
|
||||
<Tools />
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue