mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-16 06:15:23 +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.apps.webui.models.models import Models
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
CORS_ALLOW_ORIGIN,
|
CORS_ALLOW_ORIGIN,
|
||||||
ENABLE_MODEL_FILTER,
|
|
||||||
ENABLE_OLLAMA_API,
|
ENABLE_OLLAMA_API,
|
||||||
MODEL_FILTER_LIST,
|
|
||||||
OLLAMA_BASE_URLS,
|
OLLAMA_BASE_URLS,
|
||||||
OLLAMA_API_CONFIGS,
|
OLLAMA_API_CONFIGS,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
|
|
@ -66,32 +64,16 @@ app.add_middleware(
|
||||||
|
|
||||||
app.state.config = AppConfig()
|
app.state.config = AppConfig()
|
||||||
|
|
||||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
|
||||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
|
||||||
|
|
||||||
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
||||||
app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||||
app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
|
app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
|
||||||
|
|
||||||
app.state.MODELS = {}
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances.
|
# TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances.
|
||||||
# Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin,
|
# Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin,
|
||||||
# least connections, or least response time for better resource utilization and performance optimization.
|
# least connections, or least response time for better resource utilization and performance optimization.
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
|
||||||
async def check_url(request: Request, call_next):
|
|
||||||
if len(app.state.MODELS) == 0:
|
|
||||||
await get_all_models()
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
response = await call_next(request)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@app.head("/")
|
@app.head("/")
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def get_status():
|
async def get_status():
|
||||||
|
|
@ -326,8 +308,6 @@ async def get_all_models():
|
||||||
else:
|
else:
|
||||||
models = {"models": []}
|
models = {"models": []}
|
||||||
|
|
||||||
app.state.MODELS = {model["model"]: model for model in models["models"]}
|
|
||||||
|
|
||||||
return models
|
return models
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -339,16 +319,18 @@ async def get_ollama_tags(
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
models = await get_all_models()
|
models = await get_all_models()
|
||||||
|
|
||||||
if app.state.config.ENABLE_MODEL_FILTER:
|
# TODO: Check User Group and Filter Models
|
||||||
if user.role == "user":
|
# if app.state.config.ENABLE_MODEL_FILTER:
|
||||||
models["models"] = list(
|
# if user.role == "user":
|
||||||
filter(
|
# models["models"] = list(
|
||||||
lambda model: model["name"]
|
# filter(
|
||||||
in app.state.config.MODEL_FILTER_LIST,
|
# lambda model: model["name"]
|
||||||
models["models"],
|
# in app.state.config.MODEL_FILTER_LIST,
|
||||||
)
|
# models["models"],
|
||||||
)
|
# )
|
||||||
return models
|
# )
|
||||||
|
# return models
|
||||||
|
|
||||||
return models
|
return models
|
||||||
else:
|
else:
|
||||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
|
|
@ -473,8 +455,11 @@ async def push_model(
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
if form_data.name in app.state.MODELS:
|
model_list = await get_all_models()
|
||||||
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
models = {model["model"]: model for model in model_list["models"]}
|
||||||
|
|
||||||
|
if form_data.name in models:
|
||||||
|
url_idx = models[form_data.name]["urls"][0]
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
|
@ -523,8 +508,11 @@ async def copy_model(
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
if form_data.source in app.state.MODELS:
|
model_list = await get_all_models()
|
||||||
url_idx = app.state.MODELS[form_data.source]["urls"][0]
|
models = {model["model"]: model for model in model_list["models"]}
|
||||||
|
|
||||||
|
if form_data.source in models:
|
||||||
|
url_idx = models[form_data.source]["urls"][0]
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
|
@ -579,8 +567,11 @@ async def delete_model(
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
if form_data.name in app.state.MODELS:
|
model_list = await get_all_models()
|
||||||
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
models = {model["model"]: model for model in model_list["models"]}
|
||||||
|
|
||||||
|
if form_data.name in models:
|
||||||
|
url_idx = models[form_data.name]["urls"][0]
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
|
@ -628,13 +619,16 @@ async def delete_model(
|
||||||
|
|
||||||
@app.post("/api/show")
|
@app.post("/api/show")
|
||||||
async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
|
async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
|
||||||
if form_data.name not in app.state.MODELS:
|
model_list = await get_all_models()
|
||||||
|
models = {model["model"]: model for model in model_list["models"]}
|
||||||
|
|
||||||
|
if form_data.name not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
||||||
)
|
)
|
||||||
|
|
||||||
url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
|
url_idx = random.choice(models[form_data.name]["urls"])
|
||||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.info(f"url: {url}")
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
|
|
@ -704,23 +698,26 @@ async def generate_embeddings(
|
||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
return generate_ollama_embeddings(form_data=form_data, url_idx=url_idx)
|
return await generate_ollama_embeddings(form_data=form_data, url_idx=url_idx)
|
||||||
|
|
||||||
|
|
||||||
def generate_ollama_embeddings(
|
async def generate_ollama_embeddings(
|
||||||
form_data: GenerateEmbeddingsForm,
|
form_data: GenerateEmbeddingsForm,
|
||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
):
|
):
|
||||||
log.info(f"generate_ollama_embeddings {form_data}")
|
log.info(f"generate_ollama_embeddings {form_data}")
|
||||||
|
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["model"]: model for model in model_list["models"]}
|
||||||
|
|
||||||
model = form_data.model
|
model = form_data.model
|
||||||
|
|
||||||
if ":" not in model:
|
if ":" not in model:
|
||||||
model = f"{model}:latest"
|
model = f"{model}:latest"
|
||||||
|
|
||||||
if model in app.state.MODELS:
|
if model in models:
|
||||||
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
url_idx = random.choice(models[model]["urls"])
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
|
@ -771,20 +768,23 @@ def generate_ollama_embeddings(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_ollama_batch_embeddings(
|
async def generate_ollama_batch_embeddings(
|
||||||
form_data: GenerateEmbedForm,
|
form_data: GenerateEmbedForm,
|
||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
):
|
):
|
||||||
log.info(f"generate_ollama_batch_embeddings {form_data}")
|
log.info(f"generate_ollama_batch_embeddings {form_data}")
|
||||||
|
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["model"]: model for model in model_list["models"]}
|
||||||
|
|
||||||
model = form_data.model
|
model = form_data.model
|
||||||
|
|
||||||
if ":" not in model:
|
if ":" not in model:
|
||||||
model = f"{model}:latest"
|
model = f"{model}:latest"
|
||||||
|
|
||||||
if model in app.state.MODELS:
|
if model in models:
|
||||||
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
url_idx = random.choice(models[model]["urls"])
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
|
@ -854,13 +854,16 @@ async def generate_completion(
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["model"]: model for model in model_list["models"]}
|
||||||
|
|
||||||
model = form_data.model
|
model = form_data.model
|
||||||
|
|
||||||
if ":" not in model:
|
if ":" not in model:
|
||||||
model = f"{model}:latest"
|
model = f"{model}:latest"
|
||||||
|
|
||||||
if model in app.state.MODELS:
|
if model in models:
|
||||||
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
url_idx = random.choice(models[model]["urls"])
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
|
@ -895,14 +898,17 @@ class GenerateChatCompletionForm(BaseModel):
|
||||||
keep_alive: Optional[Union[int, str]] = None
|
keep_alive: Optional[Union[int, str]] = None
|
||||||
|
|
||||||
|
|
||||||
def get_ollama_url(url_idx: Optional[int], model: str):
|
async def get_ollama_url(url_idx: Optional[int], model: str):
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
if model not in app.state.MODELS:
|
model_list = await get_all_models()
|
||||||
|
models = {model["model"]: model for model in model_list["models"]}
|
||||||
|
|
||||||
|
if model not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
|
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
|
||||||
)
|
)
|
||||||
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
url_idx = random.choice(models[model]["urls"])
|
||||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
@ -922,12 +928,14 @@ async def generate_chat_completion(
|
||||||
|
|
||||||
model_id = form_data.model
|
model_id = form_data.model
|
||||||
|
|
||||||
if not bypass_filter and app.state.config.ENABLE_MODEL_FILTER:
|
# TODO: Check User Group and Filter Models
|
||||||
if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
|
# if not bypass_filter:
|
||||||
raise HTTPException(
|
# if app.state.config.ENABLE_MODEL_FILTER:
|
||||||
status_code=403,
|
# if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
|
||||||
detail="Model not found",
|
# raise HTTPException(
|
||||||
)
|
# status_code=403,
|
||||||
|
# detail="Model not found",
|
||||||
|
# )
|
||||||
|
|
||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
|
||||||
|
|
@ -949,7 +957,7 @@ async def generate_chat_completion(
|
||||||
if ":" not in payload["model"]:
|
if ":" not in payload["model"]:
|
||||||
payload["model"] = f"{payload['model']}:latest"
|
payload["model"] = f"{payload['model']}:latest"
|
||||||
|
|
||||||
url = get_ollama_url(url_idx, payload["model"])
|
url = await get_ollama_url(url_idx, payload["model"])
|
||||||
log.info(f"url: {url}")
|
log.info(f"url: {url}")
|
||||||
log.debug(f"generate_chat_completion() - 2.payload = {payload}")
|
log.debug(f"generate_chat_completion() - 2.payload = {payload}")
|
||||||
|
|
||||||
|
|
@ -1008,12 +1016,13 @@ async def generate_openai_chat_completion(
|
||||||
|
|
||||||
model_id = completion_form.model
|
model_id = completion_form.model
|
||||||
|
|
||||||
if app.state.config.ENABLE_MODEL_FILTER:
|
# TODO: Check User Group and Filter Models
|
||||||
if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
|
# if app.state.config.ENABLE_MODEL_FILTER:
|
||||||
raise HTTPException(
|
# if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
|
||||||
status_code=403,
|
# raise HTTPException(
|
||||||
detail="Model not found",
|
# status_code=403,
|
||||||
)
|
# detail="Model not found",
|
||||||
|
# )
|
||||||
|
|
||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
|
||||||
|
|
@ -1030,7 +1039,7 @@ async def generate_openai_chat_completion(
|
||||||
if ":" not in payload["model"]:
|
if ":" not in payload["model"]:
|
||||||
payload["model"] = f"{payload['model']}:latest"
|
payload["model"] = f"{payload['model']}:latest"
|
||||||
|
|
||||||
url = get_ollama_url(url_idx, payload["model"])
|
url = await get_ollama_url(url_idx, payload["model"])
|
||||||
log.info(f"url: {url}")
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
|
api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
|
||||||
|
|
@ -1054,15 +1063,16 @@ async def get_openai_models(
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
models = await get_all_models()
|
models = await get_all_models()
|
||||||
|
|
||||||
if app.state.config.ENABLE_MODEL_FILTER:
|
# TODO: Check User Group and Filter Models
|
||||||
if user.role == "user":
|
# if app.state.config.ENABLE_MODEL_FILTER:
|
||||||
models["models"] = list(
|
# if user.role == "user":
|
||||||
filter(
|
# models["models"] = list(
|
||||||
lambda model: model["name"]
|
# filter(
|
||||||
in app.state.config.MODEL_FILTER_LIST,
|
# lambda model: model["name"]
|
||||||
models["models"],
|
# in app.state.config.MODEL_FILTER_LIST,
|
||||||
)
|
# models["models"],
|
||||||
)
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"data": [
|
"data": [
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,7 @@ from open_webui.apps.webui.models.models import Models
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
CORS_ALLOW_ORIGIN,
|
CORS_ALLOW_ORIGIN,
|
||||||
ENABLE_MODEL_FILTER,
|
|
||||||
ENABLE_OPENAI_API,
|
ENABLE_OPENAI_API,
|
||||||
MODEL_FILTER_LIST,
|
|
||||||
OPENAI_API_BASE_URLS,
|
OPENAI_API_BASE_URLS,
|
||||||
OPENAI_API_KEYS,
|
OPENAI_API_KEYS,
|
||||||
OPENAI_API_CONFIGS,
|
OPENAI_API_CONFIGS,
|
||||||
|
|
@ -39,6 +37,8 @@ from open_webui.utils.payload import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
||||||
|
|
@ -61,25 +61,11 @@ app.add_middleware(
|
||||||
|
|
||||||
app.state.config = AppConfig()
|
app.state.config = AppConfig()
|
||||||
|
|
||||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
|
||||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
|
||||||
|
|
||||||
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
|
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
|
||||||
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||||
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
|
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
|
||||||
app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
|
app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
|
||||||
|
|
||||||
app.state.MODELS = {}
|
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
|
||||||
async def check_url(request: Request, call_next):
|
|
||||||
if len(app.state.MODELS) == 0:
|
|
||||||
await get_all_models()
|
|
||||||
|
|
||||||
response = await call_next(request)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/config")
|
@app.get("/config")
|
||||||
async def get_config(user=Depends(get_admin_user)):
|
async def get_config(user=Depends(get_admin_user)):
|
||||||
|
|
@ -264,7 +250,7 @@ def merge_models_lists(model_lists):
|
||||||
return merged_list
|
return merged_list
|
||||||
|
|
||||||
|
|
||||||
async def get_all_models_raw() -> list:
|
async def get_all_models_responses() -> list:
|
||||||
if not app.state.config.ENABLE_OPENAI_API:
|
if not app.state.config.ENABLE_OPENAI_API:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -335,22 +321,13 @@ async def get_all_models_raw() -> list:
|
||||||
return responses
|
return responses
|
||||||
|
|
||||||
|
|
||||||
@overload
|
async def get_all_models() -> dict[str, list]:
|
||||||
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:
|
|
||||||
log.info("get_all_models()")
|
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 not app.state.config.ENABLE_OPENAI_API:
|
||||||
if raw:
|
return {"data": []}
|
||||||
return responses
|
|
||||||
|
responses = await get_all_models_responses()
|
||||||
|
|
||||||
def extract_data(response):
|
def extract_data(response):
|
||||||
if response and "data" in response:
|
if response and "data" in response:
|
||||||
|
|
@ -360,9 +337,7 @@ async def get_all_models(raw=False) -> dict[str, list] | list:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
models = {"data": merge_models_lists(map(extract_data, responses))}
|
models = {"data": merge_models_lists(map(extract_data, responses))}
|
||||||
|
|
||||||
log.debug(f"models: {models}")
|
log.debug(f"models: {models}")
|
||||||
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
|
||||||
|
|
||||||
return models
|
return models
|
||||||
|
|
||||||
|
|
@ -370,18 +345,12 @@ async def get_all_models(raw=False) -> dict[str, list] | list:
|
||||||
@app.get("/models")
|
@app.get("/models")
|
||||||
@app.get("/models/{url_idx}")
|
@app.get("/models/{url_idx}")
|
||||||
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
|
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
|
||||||
|
models = {
|
||||||
|
"data": [],
|
||||||
|
}
|
||||||
|
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
models = await get_all_models()
|
models = await get_all_models()
|
||||||
if app.state.config.ENABLE_MODEL_FILTER:
|
|
||||||
if user.role == "user":
|
|
||||||
models["data"] = list(
|
|
||||||
filter(
|
|
||||||
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
|
|
||||||
models["data"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return models
|
|
||||||
return models
|
|
||||||
else:
|
else:
|
||||||
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
|
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
|
||||||
key = app.state.config.OPENAI_API_KEYS[url_idx]
|
key = app.state.config.OPENAI_API_KEYS[url_idx]
|
||||||
|
|
@ -389,6 +358,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
|
||||||
headers = {}
|
headers = {}
|
||||||
headers["Authorization"] = f"Bearer {key}"
|
headers["Authorization"] = f"Bearer {key}"
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
if ENABLE_FORWARD_USER_INFO_HEADERS:
|
if ENABLE_FORWARD_USER_INFO_HEADERS:
|
||||||
headers["X-OpenWebUI-User-Name"] = user.name
|
headers["X-OpenWebUI-User-Name"] = user.name
|
||||||
headers["X-OpenWebUI-User-Id"] = user.id
|
headers["X-OpenWebUI-User-Id"] = user.id
|
||||||
|
|
@ -430,8 +400,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
return response_data
|
models = response_data
|
||||||
|
|
||||||
except aiohttp.ClientError as e:
|
except aiohttp.ClientError as e:
|
||||||
# ClientError covers all aiohttp requests issues
|
# ClientError covers all aiohttp requests issues
|
||||||
log.exception(f"Client error: {str(e)}")
|
log.exception(f"Client error: {str(e)}")
|
||||||
|
|
@ -445,6 +414,22 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
|
||||||
error_detail = f"Unexpected error: {str(e)}"
|
error_detail = f"Unexpected error: {str(e)}"
|
||||||
raise HTTPException(status_code=500, detail=error_detail)
|
raise HTTPException(status_code=500, detail=error_detail)
|
||||||
|
|
||||||
|
if user.role == "user":
|
||||||
|
# Filter models based on user access control
|
||||||
|
filtered_models = []
|
||||||
|
for model in models.get("data", []):
|
||||||
|
model_info = Models.get_model_by_id(model["id"])
|
||||||
|
if model_info:
|
||||||
|
if has_access(
|
||||||
|
user.id, type="read", access_control=model_info.access_control
|
||||||
|
):
|
||||||
|
filtered_models.append(model)
|
||||||
|
else:
|
||||||
|
filtered_models.append(model)
|
||||||
|
models["data"] = filtered_models
|
||||||
|
|
||||||
|
return models
|
||||||
|
|
||||||
|
|
||||||
class ConnectionVerificationForm(BaseModel):
|
class ConnectionVerificationForm(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
|
|
@ -492,11 +477,10 @@ async def verify_connection(
|
||||||
|
|
||||||
|
|
||||||
@app.post("/chat/completions")
|
@app.post("/chat/completions")
|
||||||
@app.post("/chat/completions/{url_idx}")
|
|
||||||
async def generate_chat_completion(
|
async def generate_chat_completion(
|
||||||
form_data: dict,
|
form_data: dict,
|
||||||
url_idx: Optional[int] = None,
|
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
|
bypass_filter: Optional[bool] = False,
|
||||||
):
|
):
|
||||||
idx = 0
|
idx = 0
|
||||||
payload = {**form_data}
|
payload = {**form_data}
|
||||||
|
|
@ -507,6 +491,7 @@ async def generate_chat_completion(
|
||||||
model_id = form_data.get("model")
|
model_id = form_data.get("model")
|
||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
|
||||||
|
# Check model info and override the payload
|
||||||
if model_info:
|
if model_info:
|
||||||
if model_info.base_model_id:
|
if model_info.base_model_id:
|
||||||
payload["model"] = model_info.base_model_id
|
payload["model"] = model_info.base_model_id
|
||||||
|
|
@ -515,9 +500,33 @@ async def generate_chat_completion(
|
||||||
payload = apply_model_params_to_body_openai(params, payload)
|
payload = apply_model_params_to_body_openai(params, payload)
|
||||||
payload = apply_model_system_prompt_to_body(params, payload, user)
|
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||||
|
|
||||||
model = app.state.MODELS[payload.get("model")]
|
# Check if user has access to the model
|
||||||
idx = model["urlIdx"]
|
if user.role == "user" and not has_access(
|
||||||
|
user.id, type="read", access_control=model_info.access_control
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Model not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attemp to get urlIdx from the model
|
||||||
|
models = await get_all_models()
|
||||||
|
|
||||||
|
# Find the model from the list
|
||||||
|
model = next(
|
||||||
|
(model for model in models["data"] if model["id"] == payload.get("model")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if model:
|
||||||
|
idx = model["urlIdx"]
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Model not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the API config for the model
|
||||||
api_config = app.state.config.OPENAI_API_CONFIGS.get(
|
api_config = app.state.config.OPENAI_API_CONFIGS.get(
|
||||||
app.state.config.OPENAI_API_BASE_URLS[idx], {}
|
app.state.config.OPENAI_API_BASE_URLS[idx], {}
|
||||||
)
|
)
|
||||||
|
|
@ -526,6 +535,7 @@ async def generate_chat_completion(
|
||||||
if prefix_id:
|
if prefix_id:
|
||||||
payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
|
payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
|
||||||
|
|
||||||
|
# Add user info to the payload if the model is a pipeline
|
||||||
if "pipeline" in model and model.get("pipeline"):
|
if "pipeline" in model and model.get("pipeline"):
|
||||||
payload["user"] = {
|
payload["user"] = {
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
|
|
@ -536,8 +546,9 @@ async def generate_chat_completion(
|
||||||
|
|
||||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||||
is_o1 = payload["model"].lower().startswith("o1-")
|
|
||||||
|
|
||||||
|
# Fix: O1 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens"
|
||||||
|
is_o1 = payload["model"].lower().startswith("o1-")
|
||||||
# Change max_completion_tokens to max_tokens (Backward compatible)
|
# Change max_completion_tokens to max_tokens (Backward compatible)
|
||||||
if "api.openai.com" not in url and not is_o1:
|
if "api.openai.com" not in url and not is_o1:
|
||||||
if "max_completion_tokens" in payload:
|
if "max_completion_tokens" in payload:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import os
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from huggingface_hub import snapshot_download
|
from huggingface_hub import snapshot_download
|
||||||
|
|
@ -291,7 +292,13 @@ def get_embedding_function(
|
||||||
if embedding_engine == "":
|
if embedding_engine == "":
|
||||||
return lambda query: embedding_function.encode(query).tolist()
|
return lambda query: embedding_function.encode(query).tolist()
|
||||||
elif embedding_engine in ["ollama", "openai"]:
|
elif embedding_engine in ["ollama", "openai"]:
|
||||||
func = lambda query: generate_embeddings(
|
|
||||||
|
# Wrapper to run the async generate_embeddings synchronously.
|
||||||
|
def sync_generate_embeddings(*args, **kwargs):
|
||||||
|
return asyncio.run(generate_embeddings(*args, **kwargs))
|
||||||
|
|
||||||
|
# Semantic expectation from the original version (using sync wrapper).
|
||||||
|
func = lambda query: sync_generate_embeddings(
|
||||||
engine=embedding_engine,
|
engine=embedding_engine,
|
||||||
model=embedding_model,
|
model=embedding_model,
|
||||||
text=query,
|
text=query,
|
||||||
|
|
@ -469,7 +476,7 @@ def get_model_path(model: str, update_model: bool = False):
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
def generate_openai_batch_embeddings(
|
async def generate_openai_batch_embeddings(
|
||||||
model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
|
model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
|
||||||
) -> Optional[list[list[float]]]:
|
) -> Optional[list[list[float]]]:
|
||||||
try:
|
try:
|
||||||
|
|
@ -492,14 +499,16 @@ def generate_openai_batch_embeddings(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def generate_embeddings(engine: str, model: str, text: Union[str, list[str]], **kwargs):
|
async def generate_embeddings(
|
||||||
|
engine: str, model: str, text: Union[str, list[str]], **kwargs
|
||||||
|
):
|
||||||
if engine == "ollama":
|
if engine == "ollama":
|
||||||
if isinstance(text, list):
|
if isinstance(text, list):
|
||||||
embeddings = generate_ollama_batch_embeddings(
|
embeddings = await generate_ollama_batch_embeddings(
|
||||||
GenerateEmbedForm(**{"model": model, "input": text})
|
GenerateEmbedForm(**{"model": model, "input": text})
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
embeddings = generate_ollama_batch_embeddings(
|
embeddings = await generate_ollama_batch_embeddings(
|
||||||
GenerateEmbedForm(**{"model": model, "input": [text]})
|
GenerateEmbedForm(**{"model": model, "input": [text]})
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
|
@ -512,9 +521,9 @@ def generate_embeddings(engine: str, model: str, text: Union[str, list[str]], **
|
||||||
url = kwargs.get("url", "https://api.openai.com/v1")
|
url = kwargs.get("url", "https://api.openai.com/v1")
|
||||||
|
|
||||||
if isinstance(text, list):
|
if isinstance(text, list):
|
||||||
embeddings = generate_openai_batch_embeddings(model, text, key, url)
|
embeddings = await generate_openai_batch_embeddings(model, text, key, url)
|
||||||
else:
|
else:
|
||||||
embeddings = generate_openai_batch_embeddings(model, [text], key, url)
|
embeddings = await generate_openai_batch_embeddings(model, [text], key, url)
|
||||||
|
|
||||||
return embeddings[0] if isinstance(text, str) else embeddings
|
return embeddings[0] if isinstance(text, str) else embeddings
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from open_webui.apps.webui.routers import (
|
||||||
chats,
|
chats,
|
||||||
folders,
|
folders,
|
||||||
configs,
|
configs,
|
||||||
|
groups,
|
||||||
files,
|
files,
|
||||||
functions,
|
functions,
|
||||||
memories,
|
memories,
|
||||||
|
|
@ -85,7 +86,11 @@ from open_webui.utils.payload import (
|
||||||
|
|
||||||
from open_webui.utils.tools import get_tools
|
from open_webui.utils.tools import get_tools
|
||||||
|
|
||||||
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, openapi_url="/openapi.json" if ENV == "dev" else None, redoc_url=None)
|
app = FastAPI(
|
||||||
|
docs_url="/docs" if ENV == "dev" else None,
|
||||||
|
openapi_url="/openapi.json" if ENV == "dev" else None,
|
||||||
|
redoc_url=None,
|
||||||
|
)
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -105,6 +110,8 @@ app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
|
||||||
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
||||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||||
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||||
|
|
||||||
|
|
||||||
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
|
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
|
||||||
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||||
app.state.config.BANNERS = WEBUI_BANNERS
|
app.state.config.BANNERS = WEBUI_BANNERS
|
||||||
|
|
@ -137,7 +144,6 @@ app.state.config.LDAP_USE_TLS = LDAP_USE_TLS
|
||||||
app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
|
app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
|
||||||
app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
|
app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
|
||||||
|
|
||||||
app.state.MODELS = {}
|
|
||||||
app.state.TOOLS = {}
|
app.state.TOOLS = {}
|
||||||
app.state.FUNCTIONS = {}
|
app.state.FUNCTIONS = {}
|
||||||
|
|
||||||
|
|
@ -161,13 +167,15 @@ app.include_router(models.router, prefix="/models", tags=["models"])
|
||||||
app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"])
|
app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"])
|
||||||
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
|
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
|
||||||
app.include_router(tools.router, prefix="/tools", tags=["tools"])
|
app.include_router(tools.router, prefix="/tools", tags=["tools"])
|
||||||
app.include_router(functions.router, prefix="/functions", tags=["functions"])
|
|
||||||
|
|
||||||
app.include_router(memories.router, prefix="/memories", tags=["memories"])
|
app.include_router(memories.router, prefix="/memories", tags=["memories"])
|
||||||
|
app.include_router(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(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"])
|
app.include_router(utils.router, prefix="/utils", tags=["utils"])
|
||||||
|
|
||||||
|
|
@ -362,7 +370,7 @@ def get_function_params(function_module, form_data, user, extra_params=None):
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
async def generate_function_chat_completion(form_data, user):
|
async def generate_function_chat_completion(form_data, user, models: dict = {}):
|
||||||
model_id = form_data.get("model")
|
model_id = form_data.get("model")
|
||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
|
||||||
|
|
@ -405,7 +413,7 @@ async def generate_function_chat_completion(form_data, user):
|
||||||
user,
|
user,
|
||||||
{
|
{
|
||||||
**extra_params,
|
**extra_params,
|
||||||
"__model__": app.state.MODELS[form_data["model"]],
|
"__model__": models.get(form_data["model"], None),
|
||||||
"__messages__": form_data["messages"],
|
"__messages__": form_data["messages"],
|
||||||
"__files__": files,
|
"__files__": files,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||||
|
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
@ -34,6 +35,23 @@ class Knowledge(Base):
|
||||||
data = Column(JSON, nullable=True)
|
data = Column(JSON, nullable=True)
|
||||||
meta = Column(JSON, nullable=True)
|
meta = Column(JSON, nullable=True)
|
||||||
|
|
||||||
|
access_control = Column(JSON, nullable=True) # Controls data access levels.
|
||||||
|
# Defines access control rules for this entry.
|
||||||
|
# - `None`: Public access, available to all users with the "user" role.
|
||||||
|
# - `{}`: Private access, restricted exclusively to the owner.
|
||||||
|
# - Custom permissions: Specific access control for reading and writing;
|
||||||
|
# Can specify group or user-level restrictions:
|
||||||
|
# {
|
||||||
|
# "read": {
|
||||||
|
# "group_ids": ["group_id1", "group_id2"],
|
||||||
|
# "user_ids": ["user_id1", "user_id2"]
|
||||||
|
# },
|
||||||
|
# "write": {
|
||||||
|
# "group_ids": ["group_id1", "group_id2"],
|
||||||
|
# "user_ids": ["user_id1", "user_id2"]
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
created_at = Column(BigInteger)
|
created_at = Column(BigInteger)
|
||||||
updated_at = Column(BigInteger)
|
updated_at = Column(BigInteger)
|
||||||
|
|
||||||
|
|
@ -50,6 +68,8 @@ class KnowledgeModel(BaseModel):
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
|
|
||||||
|
access_control: Optional[dict] = None
|
||||||
|
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
@ -65,6 +85,8 @@ class KnowledgeResponse(BaseModel):
|
||||||
description: str
|
description: str
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
|
|
||||||
|
access_control: Optional[dict] = None
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
@ -75,12 +97,7 @@ class KnowledgeForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
|
access_control: Optional[dict] = None
|
||||||
|
|
||||||
class KnowledgeUpdateForm(BaseModel):
|
|
||||||
name: Optional[str] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
data: Optional[dict] = None
|
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeTable:
|
class KnowledgeTable:
|
||||||
|
|
@ -110,7 +127,7 @@ class KnowledgeTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_knowledge_items(self) -> list[KnowledgeModel]:
|
def get_knowledge_bases(self) -> list[KnowledgeModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
return [
|
return [
|
||||||
KnowledgeModel.model_validate(knowledge)
|
KnowledgeModel.model_validate(knowledge)
|
||||||
|
|
@ -119,6 +136,17 @@ class KnowledgeTable:
|
||||||
.all()
|
.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_knowledge_bases_by_user_id(
|
||||||
|
self, user_id: str, permission: str = "write"
|
||||||
|
) -> list[KnowledgeModel]:
|
||||||
|
knowledge_bases = self.get_knowledge_bases()
|
||||||
|
return [
|
||||||
|
knowledge_base
|
||||||
|
for knowledge_base in knowledge_bases
|
||||||
|
if knowledge_base.user_id == user_id
|
||||||
|
or has_access(user_id, permission, knowledge_base.access_control)
|
||||||
|
]
|
||||||
|
|
||||||
def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
|
def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
|
||||||
try:
|
try:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
@ -128,14 +156,32 @@ class KnowledgeTable:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_knowledge_by_id(
|
def update_knowledge_by_id(
|
||||||
self, id: str, form_data: KnowledgeUpdateForm, overwrite: bool = False
|
self, id: str, form_data: KnowledgeForm, overwrite: bool = False
|
||||||
) -> Optional[KnowledgeModel]:
|
) -> Optional[KnowledgeModel]:
|
||||||
try:
|
try:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
knowledge = self.get_knowledge_by_id(id=id)
|
knowledge = self.get_knowledge_by_id(id=id)
|
||||||
db.query(Knowledge).filter_by(id=id).update(
|
db.query(Knowledge).filter_by(id=id).update(
|
||||||
{
|
{
|
||||||
**form_data.model_dump(exclude_none=True),
|
**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()),
|
"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.apps.webui.internal.db import Base, JSONField, get_db
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
from open_webui.apps.webui.models.groups import Groups
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Column, Text
|
|
||||||
|
from sqlalchemy import or_, and_, func
|
||||||
|
from sqlalchemy.dialects import postgresql, sqlite
|
||||||
|
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
@ -67,6 +78,25 @@ class Model(Base):
|
||||||
Holds a JSON encoded blob of metadata, see `ModelMeta`.
|
Holds a JSON encoded blob of metadata, see `ModelMeta`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
access_control = Column(JSON, nullable=True) # Controls data access levels.
|
||||||
|
# Defines access control rules for this entry.
|
||||||
|
# - `None`: Public access, available to all users with the "user" role.
|
||||||
|
# - `{}`: Private access, restricted exclusively to the owner.
|
||||||
|
# - Custom permissions: Specific access control for reading and writing;
|
||||||
|
# Can specify group or user-level restrictions:
|
||||||
|
# {
|
||||||
|
# "read": {
|
||||||
|
# "group_ids": ["group_id1", "group_id2"],
|
||||||
|
# "user_ids": ["user_id1", "user_id2"]
|
||||||
|
# },
|
||||||
|
# "write": {
|
||||||
|
# "group_ids": ["group_id1", "group_id2"],
|
||||||
|
# "user_ids": ["user_id1", "user_id2"]
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
|
||||||
updated_at = Column(BigInteger)
|
updated_at = Column(BigInteger)
|
||||||
created_at = Column(BigInteger)
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
|
|
@ -80,6 +110,9 @@ class ModelModel(BaseModel):
|
||||||
params: ModelParams
|
params: ModelParams
|
||||||
meta: ModelMeta
|
meta: ModelMeta
|
||||||
|
|
||||||
|
access_control: Optional[dict] = None
|
||||||
|
|
||||||
|
is_active: bool
|
||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
@ -93,8 +126,16 @@ class ModelModel(BaseModel):
|
||||||
|
|
||||||
class ModelResponse(BaseModel):
|
class ModelResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
user_id: str
|
||||||
|
base_model_id: Optional[str] = None
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
|
params: ModelParams
|
||||||
meta: ModelMeta
|
meta: ModelMeta
|
||||||
|
|
||||||
|
access_control: Optional[dict] = None
|
||||||
|
|
||||||
|
is_active: bool
|
||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
@ -105,6 +146,8 @@ class ModelForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
meta: ModelMeta
|
meta: ModelMeta
|
||||||
params: ModelParams
|
params: ModelParams
|
||||||
|
access_control: Optional[dict] = None
|
||||||
|
is_active: bool = True
|
||||||
|
|
||||||
|
|
||||||
class ModelsTable:
|
class ModelsTable:
|
||||||
|
|
@ -138,6 +181,31 @@ class ModelsTable:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
|
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
|
||||||
|
|
||||||
|
def get_models(self) -> list[ModelModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
return [
|
||||||
|
ModelModel.model_validate(model)
|
||||||
|
for model in db.query(Model).filter(Model.base_model_id != None).all()
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_base_models(self) -> list[ModelModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
return [
|
||||||
|
ModelModel.model_validate(model)
|
||||||
|
for model in db.query(Model).filter(Model.base_model_id == None).all()
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_models_by_user_id(
|
||||||
|
self, user_id: str, permission: str = "write"
|
||||||
|
) -> list[ModelModel]:
|
||||||
|
models = self.get_all_models()
|
||||||
|
return [
|
||||||
|
model
|
||||||
|
for model in models
|
||||||
|
if model.user_id == user_id
|
||||||
|
or has_access(user_id, permission, model.access_control)
|
||||||
|
]
|
||||||
|
|
||||||
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
|
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
|
||||||
try:
|
try:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
@ -146,6 +214,23 @@ class ModelsTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def toggle_model_by_id(self, id: str) -> Optional[ModelModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
try:
|
||||||
|
is_active = db.query(Model).filter_by(id=id).first().is_active
|
||||||
|
|
||||||
|
db.query(Model).filter_by(id=id).update(
|
||||||
|
{
|
||||||
|
"is_active": not is_active,
|
||||||
|
"updated_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return self.get_model_by_id(id)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
|
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
|
||||||
try:
|
try:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
@ -153,7 +238,7 @@ class ModelsTable:
|
||||||
result = (
|
result = (
|
||||||
db.query(Model)
|
db.query(Model)
|
||||||
.filter_by(id=id)
|
.filter_by(id=id)
|
||||||
.update(model.model_dump(exclude={"id"}, exclude_none=True))
|
.update(model.model_dump(exclude={"id"}))
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,12 @@ import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.apps.webui.internal.db import Base, get_db
|
from open_webui.apps.webui.internal.db import Base, get_db
|
||||||
|
from open_webui.apps.webui.models.groups import Groups
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Column, String, Text
|
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||||
|
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Prompts DB Schema
|
# Prompts DB Schema
|
||||||
|
|
@ -19,6 +23,23 @@ class Prompt(Base):
|
||||||
content = Column(Text)
|
content = Column(Text)
|
||||||
timestamp = Column(BigInteger)
|
timestamp = Column(BigInteger)
|
||||||
|
|
||||||
|
access_control = Column(JSON, nullable=True) # Controls data access levels.
|
||||||
|
# Defines access control rules for this entry.
|
||||||
|
# - `None`: Public access, available to all users with the "user" role.
|
||||||
|
# - `{}`: Private access, restricted exclusively to the owner.
|
||||||
|
# - Custom permissions: Specific access control for reading and writing;
|
||||||
|
# Can specify group or user-level restrictions:
|
||||||
|
# {
|
||||||
|
# "read": {
|
||||||
|
# "group_ids": ["group_id1", "group_id2"],
|
||||||
|
# "user_ids": ["user_id1", "user_id2"]
|
||||||
|
# },
|
||||||
|
# "write": {
|
||||||
|
# "group_ids": ["group_id1", "group_id2"],
|
||||||
|
# "user_ids": ["user_id1", "user_id2"]
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
class PromptModel(BaseModel):
|
class PromptModel(BaseModel):
|
||||||
command: str
|
command: str
|
||||||
|
|
@ -27,6 +48,7 @@ class PromptModel(BaseModel):
|
||||||
content: str
|
content: str
|
||||||
timestamp: int # timestamp in epoch
|
timestamp: int # timestamp in epoch
|
||||||
|
|
||||||
|
access_control: Optional[dict] = None
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -39,6 +61,7 @@ class PromptForm(BaseModel):
|
||||||
command: str
|
command: str
|
||||||
title: str
|
title: str
|
||||||
content: str
|
content: str
|
||||||
|
access_control: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
class PromptsTable:
|
class PromptsTable:
|
||||||
|
|
@ -48,16 +71,14 @@ class PromptsTable:
|
||||||
prompt = PromptModel(
|
prompt = PromptModel(
|
||||||
**{
|
**{
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"command": form_data.command,
|
**form_data.model_dump(),
|
||||||
"title": form_data.title,
|
|
||||||
"content": form_data.content,
|
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
result = Prompt(**prompt.dict())
|
result = Prompt(**prompt.model_dump())
|
||||||
db.add(result)
|
db.add(result)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(result)
|
db.refresh(result)
|
||||||
|
|
@ -82,6 +103,18 @@ class PromptsTable:
|
||||||
PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
|
PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_prompts_by_user_id(
|
||||||
|
self, user_id: str, permission: str = "write"
|
||||||
|
) -> list[PromptModel]:
|
||||||
|
prompts = self.get_prompts()
|
||||||
|
|
||||||
|
return [
|
||||||
|
prompt
|
||||||
|
for prompt in prompts
|
||||||
|
if prompt.user_id == user_id
|
||||||
|
or has_access(user_id, permission, prompt.access_control)
|
||||||
|
]
|
||||||
|
|
||||||
def update_prompt_by_command(
|
def update_prompt_by_command(
|
||||||
self, command: str, form_data: PromptForm
|
self, command: str, form_data: PromptForm
|
||||||
) -> Optional[PromptModel]:
|
) -> Optional[PromptModel]:
|
||||||
|
|
@ -90,6 +123,7 @@ class PromptsTable:
|
||||||
prompt = db.query(Prompt).filter_by(command=command).first()
|
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||||
prompt.title = form_data.title
|
prompt.title = form_data.title
|
||||||
prompt.content = form_data.content
|
prompt.content = form_data.content
|
||||||
|
prompt.access_control = form_data.access_control
|
||||||
prompt.timestamp = int(time.time())
|
prompt.timestamp = int(time.time())
|
||||||
db.commit()
|
db.commit()
|
||||||
return PromptModel.model_validate(prompt)
|
return PromptModel.model_validate(prompt)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ from open_webui.apps.webui.internal.db import Base, JSONField, get_db
|
||||||
from open_webui.apps.webui.models.users import Users
|
from open_webui.apps.webui.models.users import Users
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Column, String, Text
|
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||||
|
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
@ -26,6 +29,24 @@ class Tool(Base):
|
||||||
specs = Column(JSONField)
|
specs = Column(JSONField)
|
||||||
meta = Column(JSONField)
|
meta = Column(JSONField)
|
||||||
valves = Column(JSONField)
|
valves = Column(JSONField)
|
||||||
|
|
||||||
|
access_control = Column(JSON, nullable=True) # Controls data access levels.
|
||||||
|
# Defines access control rules for this entry.
|
||||||
|
# - `None`: Public access, available to all users with the "user" role.
|
||||||
|
# - `{}`: Private access, restricted exclusively to the owner.
|
||||||
|
# - Custom permissions: Specific access control for reading and writing;
|
||||||
|
# Can specify group or user-level restrictions:
|
||||||
|
# {
|
||||||
|
# "read": {
|
||||||
|
# "group_ids": ["group_id1", "group_id2"],
|
||||||
|
# "user_ids": ["user_id1", "user_id2"]
|
||||||
|
# },
|
||||||
|
# "write": {
|
||||||
|
# "group_ids": ["group_id1", "group_id2"],
|
||||||
|
# "user_ids": ["user_id1", "user_id2"]
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
updated_at = Column(BigInteger)
|
updated_at = Column(BigInteger)
|
||||||
created_at = Column(BigInteger)
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
|
|
@ -42,6 +63,8 @@ class ToolModel(BaseModel):
|
||||||
content: str
|
content: str
|
||||||
specs: list[dict]
|
specs: list[dict]
|
||||||
meta: ToolMeta
|
meta: ToolMeta
|
||||||
|
access_control: Optional[dict] = None
|
||||||
|
|
||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
@ -58,6 +81,7 @@ class ToolResponse(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
name: str
|
name: str
|
||||||
meta: ToolMeta
|
meta: ToolMeta
|
||||||
|
access_control: Optional[dict] = None
|
||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
@ -67,6 +91,7 @@ class ToolForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
content: str
|
content: str
|
||||||
meta: ToolMeta
|
meta: ToolMeta
|
||||||
|
access_control: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
class ToolValves(BaseModel):
|
class ToolValves(BaseModel):
|
||||||
|
|
@ -113,6 +138,18 @@ class ToolsTable:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
|
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
|
||||||
|
|
||||||
|
def get_tools_by_user_id(
|
||||||
|
self, user_id: str, permission: str = "write"
|
||||||
|
) -> list[ToolModel]:
|
||||||
|
tools = self.get_tools()
|
||||||
|
|
||||||
|
return [
|
||||||
|
tool
|
||||||
|
for tool in tools
|
||||||
|
if tool.user_id == user_id
|
||||||
|
or has_access(user_id, permission, tool.access_control)
|
||||||
|
]
|
||||||
|
|
||||||
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
|
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
|
||||||
try:
|
try:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,12 @@ from open_webui.utils.utils import (
|
||||||
get_password_hash,
|
get_password_hash,
|
||||||
)
|
)
|
||||||
from open_webui.utils.webhook import post_webhook
|
from open_webui.utils.webhook import post_webhook
|
||||||
|
from open_webui.utils.access_control import get_permissions
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from ldap3 import Server, Connection, ALL, Tls
|
|
||||||
from ssl import CERT_REQUIRED, PROTOCOL_TLS
|
from ssl import CERT_REQUIRED, PROTOCOL_TLS
|
||||||
|
from ldap3 import Server, Connection, ALL, Tls
|
||||||
from ldap3.utils.conv import escape_filter_chars
|
from ldap3.utils.conv import escape_filter_chars
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
@ -58,6 +60,7 @@ log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
class SessionUserResponse(Token, UserResponse):
|
class SessionUserResponse(Token, UserResponse):
|
||||||
expires_at: Optional[int] = None
|
expires_at: Optional[int] = None
|
||||||
|
permissions: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=SessionUserResponse)
|
@router.get("/", response_model=SessionUserResponse)
|
||||||
|
|
@ -90,6 +93,10 @@ async def get_session_user(
|
||||||
secure=WEBUI_SESSION_COOKIE_SECURE,
|
secure=WEBUI_SESSION_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_permissions = get_permissions(
|
||||||
|
user.id, request.app.state.config.USER_PERMISSIONS
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"token": token,
|
"token": token,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
|
|
@ -99,6 +106,7 @@ async def get_session_user(
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
"role": user.role,
|
"role": user.role,
|
||||||
"profile_image_url": user.profile_image_url,
|
"profile_image_url": user.profile_image_url,
|
||||||
|
"permissions": user_permissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -163,40 +171,67 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||||
LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
|
LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
|
||||||
LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
|
LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
|
||||||
LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
|
LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
|
||||||
LDAP_CIPHERS = request.app.state.config.LDAP_CIPHERS if request.app.state.config.LDAP_CIPHERS else 'ALL'
|
LDAP_CIPHERS = (
|
||||||
|
request.app.state.config.LDAP_CIPHERS
|
||||||
|
if request.app.state.config.LDAP_CIPHERS
|
||||||
|
else "ALL"
|
||||||
|
)
|
||||||
|
|
||||||
if not ENABLE_LDAP:
|
if not ENABLE_LDAP:
|
||||||
raise HTTPException(400, detail="LDAP authentication is not enabled")
|
raise HTTPException(400, detail="LDAP authentication is not enabled")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tls = Tls(validate=CERT_REQUIRED, version=PROTOCOL_TLS, ca_certs_file=LDAP_CA_CERT_FILE, ciphers=LDAP_CIPHERS)
|
tls = Tls(
|
||||||
|
validate=CERT_REQUIRED,
|
||||||
|
version=PROTOCOL_TLS,
|
||||||
|
ca_certs_file=LDAP_CA_CERT_FILE,
|
||||||
|
ciphers=LDAP_CIPHERS,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"An error occurred on TLS: {str(e)}")
|
log.error(f"An error occurred on TLS: {str(e)}")
|
||||||
raise HTTPException(400, detail=str(e))
|
raise HTTPException(400, detail=str(e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = Server(host=LDAP_SERVER_HOST, port=LDAP_SERVER_PORT, get_info=ALL, use_ssl=LDAP_USE_TLS, tls=tls)
|
server = Server(
|
||||||
connection_app = Connection(server, LDAP_APP_DN, LDAP_APP_PASSWORD, auto_bind='NONE', authentication='SIMPLE')
|
host=LDAP_SERVER_HOST,
|
||||||
|
port=LDAP_SERVER_PORT,
|
||||||
|
get_info=ALL,
|
||||||
|
use_ssl=LDAP_USE_TLS,
|
||||||
|
tls=tls,
|
||||||
|
)
|
||||||
|
connection_app = Connection(
|
||||||
|
server,
|
||||||
|
LDAP_APP_DN,
|
||||||
|
LDAP_APP_PASSWORD,
|
||||||
|
auto_bind="NONE",
|
||||||
|
authentication="SIMPLE",
|
||||||
|
)
|
||||||
if not connection_app.bind():
|
if not connection_app.bind():
|
||||||
raise HTTPException(400, detail="Application account bind failed")
|
raise HTTPException(400, detail="Application account bind failed")
|
||||||
|
|
||||||
search_success = connection_app.search(
|
search_success = connection_app.search(
|
||||||
search_base=LDAP_SEARCH_BASE,
|
search_base=LDAP_SEARCH_BASE,
|
||||||
search_filter=f'(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})',
|
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']
|
attributes=[f"{LDAP_ATTRIBUTE_FOR_USERNAME}", "mail", "cn"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if not search_success:
|
if not search_success:
|
||||||
raise HTTPException(400, detail="User not found in the LDAP server")
|
raise HTTPException(400, detail="User not found in the LDAP server")
|
||||||
|
|
||||||
entry = connection_app.entries[0]
|
entry = connection_app.entries[0]
|
||||||
username = str(entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}']).lower()
|
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
|
||||||
mail = str(entry['mail'])
|
mail = str(entry["mail"])
|
||||||
cn = str(entry['cn'])
|
cn = str(entry["cn"])
|
||||||
user_dn = entry.entry_dn
|
user_dn = entry.entry_dn
|
||||||
|
|
||||||
if username == form_data.user.lower():
|
if username == form_data.user.lower():
|
||||||
connection_user = Connection(server, user_dn, form_data.password, auto_bind='NONE', authentication='SIMPLE')
|
connection_user = Connection(
|
||||||
|
server,
|
||||||
|
user_dn,
|
||||||
|
form_data.password,
|
||||||
|
auto_bind="NONE",
|
||||||
|
authentication="SIMPLE",
|
||||||
|
)
|
||||||
if not connection_user.bind():
|
if not connection_user.bind():
|
||||||
raise HTTPException(400, f"Authentication failed for {form_data.user}")
|
raise HTTPException(400, f"Authentication failed for {form_data.user}")
|
||||||
|
|
||||||
|
|
@ -205,14 +240,12 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hashed = get_password_hash(form_data.password)
|
hashed = get_password_hash(form_data.password)
|
||||||
user = Auths.insert_new_auth(
|
user = Auths.insert_new_auth(mail, hashed, cn)
|
||||||
mail,
|
|
||||||
hashed,
|
|
||||||
cn
|
|
||||||
)
|
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
raise HTTPException(
|
||||||
|
500, detail=ERROR_MESSAGES.CREATE_USER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|
@ -224,7 +257,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||||
if user:
|
if user:
|
||||||
token = create_token(
|
token = create_token(
|
||||||
data={"id": user.id},
|
data={"id": user.id},
|
||||||
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
|
expires_delta=parse_duration(
|
||||||
|
request.app.state.config.JWT_EXPIRES_IN
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set the cookie token
|
# Set the cookie token
|
||||||
|
|
@ -246,7 +281,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||||
else:
|
else:
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(400, f"User {form_data.user} does not match the record. Search result: {str(entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}'])}")
|
raise HTTPException(
|
||||||
|
400,
|
||||||
|
f"User {form_data.user} does not match the record. Search result: {str(entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}'])}",
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(400, detail=str(e))
|
raise HTTPException(400, detail=str(e))
|
||||||
|
|
||||||
|
|
@ -325,6 +363,10 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
||||||
secure=WEBUI_SESSION_COOKIE_SECURE,
|
secure=WEBUI_SESSION_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_permissions = get_permissions(
|
||||||
|
user.id, request.app.state.config.USER_PERMISSIONS
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"token": token,
|
"token": token,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
|
|
@ -334,6 +376,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
"role": user.role,
|
"role": user.role,
|
||||||
"profile_image_url": user.profile_image_url,
|
"profile_image_url": user.profile_image_url,
|
||||||
|
"permissions": user_permissions,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
|
|
@ -426,6 +469,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_permissions = get_permissions(
|
||||||
|
user.id, request.app.state.config.USER_PERMISSIONS
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"token": token,
|
"token": token,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
|
|
@ -435,6 +482,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
"role": user.role,
|
"role": user.role,
|
||||||
"profile_image_url": user.profile_image_url,
|
"profile_image_url": user.profile_image_url,
|
||||||
|
"permissions": user_permissions,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||||
|
|
@ -583,19 +631,18 @@ class LdapServerConfig(BaseModel):
|
||||||
label: str
|
label: str
|
||||||
host: str
|
host: str
|
||||||
port: Optional[int] = None
|
port: Optional[int] = None
|
||||||
attribute_for_username: str = 'uid'
|
attribute_for_username: str = "uid"
|
||||||
app_dn: str
|
app_dn: str
|
||||||
app_dn_password: str
|
app_dn_password: str
|
||||||
search_base: str
|
search_base: str
|
||||||
search_filters: str = ''
|
search_filters: str = ""
|
||||||
use_tls: bool = True
|
use_tls: bool = True
|
||||||
certificate_path: Optional[str] = None
|
certificate_path: Optional[str] = None
|
||||||
ciphers: Optional[str] = 'ALL'
|
ciphers: Optional[str] = "ALL"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin/config/ldap/server", response_model=LdapServerConfig)
|
@router.get("/admin/config/ldap/server", response_model=LdapServerConfig)
|
||||||
async def get_ldap_server(
|
async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
|
||||||
request: Request, user=Depends(get_admin_user)
|
|
||||||
):
|
|
||||||
return {
|
return {
|
||||||
"label": request.app.state.config.LDAP_SERVER_LABEL,
|
"label": request.app.state.config.LDAP_SERVER_LABEL,
|
||||||
"host": request.app.state.config.LDAP_SERVER_HOST,
|
"host": request.app.state.config.LDAP_SERVER_HOST,
|
||||||
|
|
@ -607,26 +654,38 @@ async def get_ldap_server(
|
||||||
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
|
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
|
||||||
"use_tls": request.app.state.config.LDAP_USE_TLS,
|
"use_tls": request.app.state.config.LDAP_USE_TLS,
|
||||||
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
|
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
|
||||||
"ciphers": request.app.state.config.LDAP_CIPHERS
|
"ciphers": request.app.state.config.LDAP_CIPHERS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/admin/config/ldap/server")
|
@router.post("/admin/config/ldap/server")
|
||||||
async def update_ldap_server(
|
async def update_ldap_server(
|
||||||
request: Request, form_data: LdapServerConfig, user=Depends(get_admin_user)
|
request: Request, form_data: LdapServerConfig, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
required_fields = ['label', 'host', 'attribute_for_username', 'app_dn', 'app_dn_password', 'search_base']
|
required_fields = [
|
||||||
|
"label",
|
||||||
|
"host",
|
||||||
|
"attribute_for_username",
|
||||||
|
"app_dn",
|
||||||
|
"app_dn_password",
|
||||||
|
"search_base",
|
||||||
|
]
|
||||||
for key in required_fields:
|
for key in required_fields:
|
||||||
value = getattr(form_data, key)
|
value = getattr(form_data, key)
|
||||||
if not value:
|
if not value:
|
||||||
raise HTTPException(400, detail=f"Required field {key} is empty")
|
raise HTTPException(400, detail=f"Required field {key} is empty")
|
||||||
|
|
||||||
if form_data.use_tls and not form_data.certificate_path:
|
if form_data.use_tls and not form_data.certificate_path:
|
||||||
raise HTTPException(400, detail="TLS is enabled but certificate file path is missing")
|
raise HTTPException(
|
||||||
|
400, detail="TLS is enabled but certificate file path is missing"
|
||||||
|
)
|
||||||
|
|
||||||
request.app.state.config.LDAP_SERVER_LABEL = form_data.label
|
request.app.state.config.LDAP_SERVER_LABEL = form_data.label
|
||||||
request.app.state.config.LDAP_SERVER_HOST = form_data.host
|
request.app.state.config.LDAP_SERVER_HOST = form_data.host
|
||||||
request.app.state.config.LDAP_SERVER_PORT = form_data.port
|
request.app.state.config.LDAP_SERVER_PORT = form_data.port
|
||||||
request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = form_data.attribute_for_username
|
request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = (
|
||||||
|
form_data.attribute_for_username
|
||||||
|
)
|
||||||
request.app.state.config.LDAP_APP_DN = form_data.app_dn
|
request.app.state.config.LDAP_APP_DN = form_data.app_dn
|
||||||
request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password
|
request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password
|
||||||
request.app.state.config.LDAP_SEARCH_BASE = form_data.search_base
|
request.app.state.config.LDAP_SEARCH_BASE = form_data.search_base
|
||||||
|
|
@ -646,18 +705,23 @@ async def update_ldap_server(
|
||||||
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
|
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
|
||||||
"use_tls": request.app.state.config.LDAP_USE_TLS,
|
"use_tls": request.app.state.config.LDAP_USE_TLS,
|
||||||
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
|
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
|
||||||
"ciphers": request.app.state.config.LDAP_CIPHERS
|
"ciphers": request.app.state.config.LDAP_CIPHERS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin/config/ldap")
|
@router.get("/admin/config/ldap")
|
||||||
async def get_ldap_config(request: Request, user=Depends(get_admin_user)):
|
async def get_ldap_config(request: Request, user=Depends(get_admin_user)):
|
||||||
return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
|
return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
|
||||||
|
|
||||||
|
|
||||||
class LdapConfigForm(BaseModel):
|
class LdapConfigForm(BaseModel):
|
||||||
enable_ldap: Optional[bool] = None
|
enable_ldap: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/admin/config/ldap")
|
@router.post("/admin/config/ldap")
|
||||||
async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user)):
|
async def update_ldap_config(
|
||||||
|
request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
request.app.state.config.ENABLE_LDAP = form_data.enable_ldap
|
request.app.state.config.ENABLE_LDAP = form_data.enable_ldap
|
||||||
return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
|
return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@ from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||||
|
from open_webui.utils.access_control import has_permission
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
@ -50,9 +53,10 @@ async def get_session_user_chat_list(
|
||||||
|
|
||||||
@router.delete("/", response_model=bool)
|
@router.delete("/", response_model=bool)
|
||||||
async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)):
|
async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)):
|
||||||
if user.role == "user" and not request.app.state.config.USER_PERMISSIONS.get(
|
|
||||||
"chat", {}
|
if user.role == "user" and not has_permission(
|
||||||
).get("deletion", {}):
|
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
|
@ -385,8 +389,8 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified
|
||||||
|
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
if not request.app.state.config.USER_PERMISSIONS.get("chat", {}).get(
|
if not has_permission(
|
||||||
"deletion", {}
|
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
|
||||||
120
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 (
|
from open_webui.apps.webui.models.knowledge import (
|
||||||
Knowledges,
|
Knowledges,
|
||||||
KnowledgeUpdateForm,
|
|
||||||
KnowledgeForm,
|
KnowledgeForm,
|
||||||
KnowledgeResponse,
|
KnowledgeResponse,
|
||||||
)
|
)
|
||||||
|
|
@ -17,6 +16,9 @@ from open_webui.apps.retrieval.main import process_file, ProcessFileForm
|
||||||
|
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
|
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -26,64 +28,98 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetKnowledgeItems
|
# getKnowledgeBases
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get("/", response_model=list[KnowledgeResponse])
|
||||||
"/", response_model=Optional[Union[list[KnowledgeResponse], KnowledgeResponse]]
|
async def get_knowledge(user=Depends(get_verified_user)):
|
||||||
)
|
knowledge_bases = []
|
||||||
async def get_knowledge_items(
|
|
||||||
id: Optional[str] = None, user=Depends(get_verified_user)
|
|
||||||
):
|
|
||||||
if id:
|
|
||||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
|
||||||
|
|
||||||
if knowledge:
|
if user.role == "admin":
|
||||||
return knowledge
|
knowledge_bases = Knowledges.get_knowledge_bases()
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
knowledge_bases = []
|
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read")
|
||||||
|
|
||||||
for knowledge in Knowledges.get_knowledge_items():
|
# Get files for each knowledge base
|
||||||
|
for knowledge_base in knowledge_bases:
|
||||||
files = []
|
files = []
|
||||||
if knowledge.data:
|
if knowledge_base.data:
|
||||||
files = Files.get_file_metadatas_by_ids(
|
files = Files.get_file_metadatas_by_ids(
|
||||||
knowledge.data.get("file_ids", [])
|
knowledge_base.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,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
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])
|
@router.post("/create", response_model=Optional[KnowledgeResponse])
|
||||||
async def create_new_knowledge(form_data: KnowledgeForm, user=Depends(get_admin_user)):
|
async def create_new_knowledge(
|
||||||
|
form_data: KnowledgeForm, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
|
knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
|
||||||
|
|
||||||
if knowledge:
|
if knowledge:
|
||||||
|
|
@ -118,13 +156,20 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||||
|
|
||||||
if knowledge:
|
if knowledge:
|
||||||
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
|
||||||
files = Files.get_files_by_ids(file_ids)
|
|
||||||
|
|
||||||
return KnowledgeFilesResponse(
|
if (
|
||||||
**knowledge.model_dump(),
|
user.role == "admin"
|
||||||
files=files,
|
or knowledge.user_id == user.id
|
||||||
)
|
or has_access(user.id, "read", knowledge.access_control)
|
||||||
|
):
|
||||||
|
|
||||||
|
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
||||||
|
files = Files.get_files_by_ids(file_ids)
|
||||||
|
|
||||||
|
return KnowledgeFilesResponse(
|
||||||
|
**knowledge.model_dump(),
|
||||||
|
files=files,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -140,11 +185,23 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
@router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
|
@router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
|
||||||
async def update_knowledge_by_id(
|
async def update_knowledge_by_id(
|
||||||
id: str,
|
id: str,
|
||||||
form_data: KnowledgeUpdateForm,
|
form_data: KnowledgeForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
|
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||||
|
if not knowledge:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
if knowledge.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
|
||||||
if knowledge:
|
if knowledge:
|
||||||
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
||||||
files = Files.get_files_by_ids(file_ids)
|
files = Files.get_files_by_ids(file_ids)
|
||||||
|
|
@ -173,9 +230,22 @@ class KnowledgeFileIdForm(BaseModel):
|
||||||
def add_file_to_knowledge_by_id(
|
def add_file_to_knowledge_by_id(
|
||||||
id: str,
|
id: str,
|
||||||
form_data: KnowledgeFileIdForm,
|
form_data: KnowledgeFileIdForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||||
|
|
||||||
|
if not knowledge:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
if knowledge.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
file = Files.get_file_by_id(form_data.file_id)
|
file = Files.get_file_by_id(form_data.file_id)
|
||||||
if not file:
|
if not file:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -206,9 +276,7 @@ def add_file_to_knowledge_by_id(
|
||||||
file_ids.append(form_data.file_id)
|
file_ids.append(form_data.file_id)
|
||||||
data["file_ids"] = file_ids
|
data["file_ids"] = file_ids
|
||||||
|
|
||||||
knowledge = Knowledges.update_knowledge_by_id(
|
knowledge = Knowledges.update_knowledge_data_by_id(id=id.id, data=data)
|
||||||
id=id, form_data=KnowledgeUpdateForm(data=data)
|
|
||||||
)
|
|
||||||
|
|
||||||
if knowledge:
|
if knowledge:
|
||||||
files = Files.get_files_by_ids(file_ids)
|
files = Files.get_files_by_ids(file_ids)
|
||||||
|
|
@ -238,9 +306,21 @@ def add_file_to_knowledge_by_id(
|
||||||
def update_file_from_knowledge_by_id(
|
def update_file_from_knowledge_by_id(
|
||||||
id: str,
|
id: str,
|
||||||
form_data: KnowledgeFileIdForm,
|
form_data: KnowledgeFileIdForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||||
|
if not knowledge:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
if knowledge.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
file = Files.get_file_by_id(form_data.file_id)
|
file = Files.get_file_by_id(form_data.file_id)
|
||||||
if not file:
|
if not file:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -288,9 +368,21 @@ def update_file_from_knowledge_by_id(
|
||||||
def remove_file_from_knowledge_by_id(
|
def remove_file_from_knowledge_by_id(
|
||||||
id: str,
|
id: str,
|
||||||
form_data: KnowledgeFileIdForm,
|
form_data: KnowledgeFileIdForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||||
|
if not knowledge:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
if knowledge.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
file = Files.get_file_by_id(form_data.file_id)
|
file = Files.get_file_by_id(form_data.file_id)
|
||||||
if not file:
|
if not file:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -318,9 +410,7 @@ def remove_file_from_knowledge_by_id(
|
||||||
file_ids.remove(form_data.file_id)
|
file_ids.remove(form_data.file_id)
|
||||||
data["file_ids"] = file_ids
|
data["file_ids"] = file_ids
|
||||||
|
|
||||||
knowledge = Knowledges.update_knowledge_by_id(
|
knowledge = Knowledges.update_knowledge_data_by_id(id=id.id, data=data)
|
||||||
id=id, form_data=KnowledgeUpdateForm(data=data)
|
|
||||||
)
|
|
||||||
|
|
||||||
if knowledge:
|
if knowledge:
|
||||||
files = Files.get_files_by_ids(file_ids)
|
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
|
# DeleteKnowledgeById
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}/delete", response_model=bool)
|
@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:
|
try:
|
||||||
VECTOR_DB_CLIENT.delete_collection(collection_name=id)
|
VECTOR_DB_CLIENT.delete_collection(collection_name=id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -379,3 +463,34 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_admin_user)):
|
||||||
pass
|
pass
|
||||||
result = Knowledges.delete_knowledge_by_id(id=id)
|
result = Knowledges.delete_knowledge_by_id(id=id)
|
||||||
return result
|
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 open_webui.constants import ERROR_MESSAGES
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
|
|
||||||
|
|
||||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# getModels
|
# GetModels
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[ModelResponse])
|
@router.get("/", response_model=list[ModelResponse])
|
||||||
async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
|
async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
|
||||||
if id:
|
if user.role == "admin":
|
||||||
model = Models.get_model_by_id(id)
|
return Models.get_models()
|
||||||
if model:
|
|
||||||
return [model]
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return Models.get_all_models()
|
return Models.get_models_by_user_id(user.id)
|
||||||
|
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# GetBaseModels
|
||||||
|
###########################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/base", response_model=list[ModelResponse])
|
||||||
|
async def get_base_models(user=Depends(get_admin_user)):
|
||||||
|
return Models.get_base_models()
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# AddNewModel
|
# CreateNewModel
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.post("/add", response_model=Optional[ModelModel])
|
@router.post("/create", response_model=Optional[ModelModel])
|
||||||
async def add_new_model(
|
async def create_new_model(
|
||||||
request: Request,
|
|
||||||
form_data: ModelForm,
|
form_data: ModelForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
if form_data.id in request.app.state.MODELS:
|
|
||||||
|
model = Models.get_model_by_id(form_data.id)
|
||||||
|
if model:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
|
detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
model = Models.insert_new_model(form_data, user.id)
|
model = Models.insert_new_model(form_data, user.id)
|
||||||
|
|
||||||
if model:
|
if model:
|
||||||
return model
|
return model
|
||||||
else:
|
else:
|
||||||
|
|
@ -60,37 +69,84 @@ async def add_new_model(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# GetModelById
|
||||||
|
###########################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/id/{id}", response_model=Optional[ModelResponse])
|
||||||
|
async def get_model_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
|
model = Models.get_model_by_id(id)
|
||||||
|
if model:
|
||||||
|
if (
|
||||||
|
user.role == "admin"
|
||||||
|
or model.user_id == user.id
|
||||||
|
or has_access(user.id, "read", model.access_control)
|
||||||
|
):
|
||||||
|
return model
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# 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
|
# UpdateModelById
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.post("/update", response_model=Optional[ModelModel])
|
@router.post("/id/{id}/update", response_model=Optional[ModelModel])
|
||||||
async def update_model_by_id(
|
async def update_model_by_id(
|
||||||
request: Request,
|
|
||||||
id: str,
|
id: str,
|
||||||
form_data: ModelForm,
|
form_data: ModelForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
model = Models.get_model_by_id(id)
|
model = Models.get_model_by_id(id)
|
||||||
if model:
|
|
||||||
model = Models.update_model_by_id(id, form_data)
|
if not model:
|
||||||
return model
|
raise HTTPException(
|
||||||
else:
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
if form_data.id in request.app.state.MODELS:
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
model = Models.insert_new_model(form_data, user.id)
|
)
|
||||||
if model:
|
|
||||||
return model
|
model = Models.update_model_by_id(id, form_data)
|
||||||
else:
|
return model
|
||||||
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(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
|
@ -98,7 +154,20 @@ async def update_model_by_id(
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/delete", response_model=bool)
|
@router.delete("/id/{id}/delete", response_model=bool)
|
||||||
async def delete_model_by_id(id: str, user=Depends(get_admin_user)):
|
async def delete_model_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
|
model = Models.get_model_by_id(id)
|
||||||
|
if not model:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
if model.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
result = Models.delete_model_by_id(id)
|
result = Models.delete_model_by_id(id)
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from open_webui.apps.webui.models.prompts import PromptForm, PromptModel, Prompt
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -14,7 +15,22 @@ router = APIRouter()
|
||||||
|
|
||||||
@router.get("/", response_model=list[PromptModel])
|
@router.get("/", response_model=list[PromptModel])
|
||||||
async def get_prompts(user=Depends(get_verified_user)):
|
async def get_prompts(user=Depends(get_verified_user)):
|
||||||
return Prompts.get_prompts()
|
if user.role == "admin":
|
||||||
|
prompts = Prompts.get_prompts()
|
||||||
|
else:
|
||||||
|
prompts = Prompts.get_prompts_by_user_id(user.id, "read")
|
||||||
|
|
||||||
|
return prompts
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/list", response_model=list[PromptModel])
|
||||||
|
async def get_prompt_list(user=Depends(get_verified_user)):
|
||||||
|
if user.role == "admin":
|
||||||
|
prompts = Prompts.get_prompts()
|
||||||
|
else:
|
||||||
|
prompts = Prompts.get_prompts_by_user_id(user.id, "write")
|
||||||
|
|
||||||
|
return prompts
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
|
@ -23,7 +39,7 @@ async def get_prompts(user=Depends(get_verified_user)):
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create", response_model=Optional[PromptModel])
|
@router.post("/create", response_model=Optional[PromptModel])
|
||||||
async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user)):
|
async def create_new_prompt(form_data: PromptForm, user=Depends(get_verified_user)):
|
||||||
prompt = Prompts.get_prompt_by_command(form_data.command)
|
prompt = Prompts.get_prompt_by_command(form_data.command)
|
||||||
if prompt is None:
|
if prompt is None:
|
||||||
prompt = Prompts.insert_new_prompt(user.id, form_data)
|
prompt = Prompts.insert_new_prompt(user.id, form_data)
|
||||||
|
|
@ -50,7 +66,12 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
|
||||||
prompt = Prompts.get_prompt_by_command(f"/{command}")
|
prompt = Prompts.get_prompt_by_command(f"/{command}")
|
||||||
|
|
||||||
if prompt:
|
if prompt:
|
||||||
return prompt
|
if (
|
||||||
|
user.role == "admin"
|
||||||
|
or prompt.user_id == user.id
|
||||||
|
or has_access(user.id, "read", prompt.access_control)
|
||||||
|
):
|
||||||
|
return prompt
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -67,8 +88,21 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
|
||||||
async def update_prompt_by_command(
|
async def update_prompt_by_command(
|
||||||
command: str,
|
command: str,
|
||||||
form_data: PromptForm,
|
form_data: PromptForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
|
prompt = Prompts.get_prompt_by_command(f"/{command}")
|
||||||
|
if not prompt:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
if prompt.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
|
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
|
||||||
if prompt:
|
if prompt:
|
||||||
return prompt
|
return prompt
|
||||||
|
|
@ -85,6 +119,19 @@ async def update_prompt_by_command(
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/command/{command}/delete", response_model=bool)
|
@router.delete("/command/{command}/delete", response_model=bool)
|
||||||
async def delete_prompt_by_command(command: str, user=Depends(get_admin_user)):
|
async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)):
|
||||||
|
prompt = Prompts.get_prompt_by_command(f"/{command}")
|
||||||
|
if not prompt:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
if prompt.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
result = Prompts.delete_prompt_by_command(f"/{command}")
|
result = Prompts.delete_prompt_by_command(f"/{command}")
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -3,48 +3,66 @@ from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.apps.webui.models.tools import ToolForm, ToolModel, ToolResponse, Tools
|
from open_webui.apps.webui.models.tools import ToolForm, ToolModel, ToolResponse, Tools
|
||||||
from open_webui.apps.webui.utils import load_toolkit_module_by_id, replace_imports
|
from open_webui.apps.webui.utils import load_tools_module_by_id, replace_imports
|
||||||
from open_webui.config import CACHE_DIR, DATA_DIR
|
from open_webui.config import CACHE_DIR, DATA_DIR
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
from open_webui.utils.tools import get_tools_specs
|
from open_webui.utils.tools import get_tools_specs
|
||||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetToolkits
|
# GetTools
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[ToolResponse])
|
@router.get("/", response_model=list[ToolResponse])
|
||||||
async def get_toolkits(user=Depends(get_verified_user)):
|
async def get_tools(user=Depends(get_verified_user)):
|
||||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
if user.role == "admin":
|
||||||
return toolkits
|
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])
|
@router.get("/export", response_model=list[ToolModel])
|
||||||
async def get_toolkits(user=Depends(get_admin_user)):
|
async def export_tools(user=Depends(get_admin_user)):
|
||||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
tools = Tools.get_tools()
|
||||||
return toolkits
|
return tools
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# CreateNewToolKit
|
# CreateNewTools
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create", response_model=Optional[ToolResponse])
|
@router.post("/create", response_model=Optional[ToolResponse])
|
||||||
async def create_new_toolkit(
|
async def create_new_tools(
|
||||||
request: Request,
|
request: Request,
|
||||||
form_data: ToolForm,
|
form_data: ToolForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
if not form_data.id.isidentifier():
|
if not form_data.id.isidentifier():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -54,30 +72,30 @@ async def create_new_toolkit(
|
||||||
|
|
||||||
form_data.id = form_data.id.lower()
|
form_data.id = form_data.id.lower()
|
||||||
|
|
||||||
toolkit = Tools.get_tool_by_id(form_data.id)
|
tools = Tools.get_tool_by_id(form_data.id)
|
||||||
if toolkit is None:
|
if tools is None:
|
||||||
try:
|
try:
|
||||||
form_data.content = replace_imports(form_data.content)
|
form_data.content = replace_imports(form_data.content)
|
||||||
toolkit_module, frontmatter = load_toolkit_module_by_id(
|
tools_module, frontmatter = load_tools_module_by_id(
|
||||||
form_data.id, content=form_data.content
|
form_data.id, content=form_data.content
|
||||||
)
|
)
|
||||||
form_data.meta.manifest = frontmatter
|
form_data.meta.manifest = frontmatter
|
||||||
|
|
||||||
TOOLS = request.app.state.TOOLS
|
TOOLS = request.app.state.TOOLS
|
||||||
TOOLS[form_data.id] = toolkit_module
|
TOOLS[form_data.id] = tools_module
|
||||||
|
|
||||||
specs = get_tools_specs(TOOLS[form_data.id])
|
specs = get_tools_specs(TOOLS[form_data.id])
|
||||||
toolkit = Tools.insert_new_tool(user.id, form_data, specs)
|
tools = Tools.insert_new_tool(user.id, form_data, specs)
|
||||||
|
|
||||||
tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
|
tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
|
||||||
tool_cache_dir.mkdir(parents=True, exist_ok=True)
|
tool_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if toolkit:
|
if tools:
|
||||||
return toolkit
|
return tools
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT("Error creating toolkit"),
|
detail=ERROR_MESSAGES.DEFAULT("Error creating tools"),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
@ -93,16 +111,21 @@ async def create_new_toolkit(
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetToolkitById
|
# GetToolsById
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/id/{id}", response_model=Optional[ToolModel])
|
@router.get("/id/{id}", response_model=Optional[ToolModel])
|
||||||
async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
|
async def get_tools_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
toolkit = Tools.get_tool_by_id(id)
|
tools = Tools.get_tool_by_id(id)
|
||||||
|
|
||||||
if toolkit:
|
if tools:
|
||||||
return toolkit
|
if (
|
||||||
|
user.role == "admin"
|
||||||
|
or tools.user_id == user.id
|
||||||
|
or has_access(user.id, "read", tools.access_control)
|
||||||
|
):
|
||||||
|
return tools
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -111,26 +134,39 @@ async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# UpdateToolkitById
|
# UpdateToolsById
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
|
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
|
||||||
async def update_toolkit_by_id(
|
async def update_tools_by_id(
|
||||||
request: Request,
|
request: Request,
|
||||||
id: str,
|
id: str,
|
||||||
form_data: ToolForm,
|
form_data: ToolForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
|
tools = Tools.get_tool_by_id(id)
|
||||||
|
if not tools:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
if tools.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
form_data.content = replace_imports(form_data.content)
|
form_data.content = replace_imports(form_data.content)
|
||||||
toolkit_module, frontmatter = load_toolkit_module_by_id(
|
tools_module, frontmatter = load_tools_module_by_id(
|
||||||
id, content=form_data.content
|
id, content=form_data.content
|
||||||
)
|
)
|
||||||
form_data.meta.manifest = frontmatter
|
form_data.meta.manifest = frontmatter
|
||||||
|
|
||||||
TOOLS = request.app.state.TOOLS
|
TOOLS = request.app.state.TOOLS
|
||||||
TOOLS[id] = toolkit_module
|
TOOLS[id] = tools_module
|
||||||
|
|
||||||
specs = get_tools_specs(TOOLS[id])
|
specs = get_tools_specs(TOOLS[id])
|
||||||
|
|
||||||
|
|
@ -140,14 +176,14 @@ async def update_toolkit_by_id(
|
||||||
}
|
}
|
||||||
|
|
||||||
print(updated)
|
print(updated)
|
||||||
toolkit = Tools.update_tool_by_id(id, updated)
|
tools = Tools.update_tool_by_id(id, updated)
|
||||||
|
|
||||||
if toolkit:
|
if tools:
|
||||||
return toolkit
|
return tools
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"),
|
detail=ERROR_MESSAGES.DEFAULT("Error updating tools"),
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -158,14 +194,28 @@ async def update_toolkit_by_id(
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# DeleteToolkitById
|
# DeleteToolsById
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/id/{id}/delete", response_model=bool)
|
@router.delete("/id/{id}/delete", response_model=bool)
|
||||||
async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)):
|
async def delete_tools_by_id(
|
||||||
result = Tools.delete_tool_by_id(id)
|
request: Request, id: str, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
|
tools = Tools.get_tool_by_id(id)
|
||||||
|
if not tools:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
if tools.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = Tools.delete_tool_by_id(id)
|
||||||
if result:
|
if result:
|
||||||
TOOLS = request.app.state.TOOLS
|
TOOLS = request.app.state.TOOLS
|
||||||
if id in TOOLS:
|
if id in TOOLS:
|
||||||
|
|
@ -180,9 +230,9 @@ async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin
|
||||||
|
|
||||||
|
|
||||||
@router.get("/id/{id}/valves", response_model=Optional[dict])
|
@router.get("/id/{id}/valves", response_model=Optional[dict])
|
||||||
async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
|
async def get_tools_valves_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
toolkit = Tools.get_tool_by_id(id)
|
tools = Tools.get_tool_by_id(id)
|
||||||
if toolkit:
|
if tools:
|
||||||
try:
|
try:
|
||||||
valves = Tools.get_tool_valves_by_id(id)
|
valves = Tools.get_tool_valves_by_id(id)
|
||||||
return valves
|
return valves
|
||||||
|
|
@ -204,19 +254,19 @@ async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
|
||||||
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
|
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
|
||||||
async def get_toolkit_valves_spec_by_id(
|
async def get_tools_valves_spec_by_id(
|
||||||
request: Request, id: str, user=Depends(get_admin_user)
|
request: Request, id: str, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
toolkit = Tools.get_tool_by_id(id)
|
tools = Tools.get_tool_by_id(id)
|
||||||
if toolkit:
|
if tools:
|
||||||
if id in request.app.state.TOOLS:
|
if id in request.app.state.TOOLS:
|
||||||
toolkit_module = request.app.state.TOOLS[id]
|
tools_module = request.app.state.TOOLS[id]
|
||||||
else:
|
else:
|
||||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
tools_module, _ = load_tools_module_by_id(id)
|
||||||
request.app.state.TOOLS[id] = toolkit_module
|
request.app.state.TOOLS[id] = tools_module
|
||||||
|
|
||||||
if hasattr(toolkit_module, "Valves"):
|
if hasattr(tools_module, "Valves"):
|
||||||
Valves = toolkit_module.Valves
|
Valves = tools_module.Valves
|
||||||
return Valves.schema()
|
return Valves.schema()
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
|
|
@ -232,19 +282,19 @@ async def get_toolkit_valves_spec_by_id(
|
||||||
|
|
||||||
|
|
||||||
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
|
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
|
||||||
async def update_toolkit_valves_by_id(
|
async def update_tools_valves_by_id(
|
||||||
request: Request, id: str, form_data: dict, user=Depends(get_admin_user)
|
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
toolkit = Tools.get_tool_by_id(id)
|
tools = Tools.get_tool_by_id(id)
|
||||||
if toolkit:
|
if tools:
|
||||||
if id in request.app.state.TOOLS:
|
if id in request.app.state.TOOLS:
|
||||||
toolkit_module = request.app.state.TOOLS[id]
|
tools_module = request.app.state.TOOLS[id]
|
||||||
else:
|
else:
|
||||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
tools_module, _ = load_tools_module_by_id(id)
|
||||||
request.app.state.TOOLS[id] = toolkit_module
|
request.app.state.TOOLS[id] = tools_module
|
||||||
|
|
||||||
if hasattr(toolkit_module, "Valves"):
|
if hasattr(tools_module, "Valves"):
|
||||||
Valves = toolkit_module.Valves
|
Valves = tools_module.Valves
|
||||||
|
|
||||||
try:
|
try:
|
||||||
form_data = {k: v for k, v in form_data.items() if v is not None}
|
form_data = {k: v for k, v in form_data.items() if v is not None}
|
||||||
|
|
@ -276,9 +326,9 @@ async def update_toolkit_valves_by_id(
|
||||||
|
|
||||||
|
|
||||||
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
|
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
|
||||||
async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)):
|
async def get_tools_user_valves_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
toolkit = Tools.get_tool_by_id(id)
|
tools = Tools.get_tool_by_id(id)
|
||||||
if toolkit:
|
if tools:
|
||||||
try:
|
try:
|
||||||
user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id)
|
user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id)
|
||||||
return user_valves
|
return user_valves
|
||||||
|
|
@ -295,19 +345,19 @@ async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
|
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
|
||||||
async def get_toolkit_user_valves_spec_by_id(
|
async def get_tools_user_valves_spec_by_id(
|
||||||
request: Request, id: str, user=Depends(get_verified_user)
|
request: Request, id: str, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
toolkit = Tools.get_tool_by_id(id)
|
tools = Tools.get_tool_by_id(id)
|
||||||
if toolkit:
|
if tools:
|
||||||
if id in request.app.state.TOOLS:
|
if id in request.app.state.TOOLS:
|
||||||
toolkit_module = request.app.state.TOOLS[id]
|
tools_module = request.app.state.TOOLS[id]
|
||||||
else:
|
else:
|
||||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
tools_module, _ = load_tools_module_by_id(id)
|
||||||
request.app.state.TOOLS[id] = toolkit_module
|
request.app.state.TOOLS[id] = tools_module
|
||||||
|
|
||||||
if hasattr(toolkit_module, "UserValves"):
|
if hasattr(tools_module, "UserValves"):
|
||||||
UserValves = toolkit_module.UserValves
|
UserValves = tools_module.UserValves
|
||||||
return UserValves.schema()
|
return UserValves.schema()
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
|
|
@ -318,20 +368,20 @@ async def get_toolkit_user_valves_spec_by_id(
|
||||||
|
|
||||||
|
|
||||||
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
|
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
|
||||||
async def update_toolkit_user_valves_by_id(
|
async def update_tools_user_valves_by_id(
|
||||||
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
|
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
toolkit = Tools.get_tool_by_id(id)
|
tools = Tools.get_tool_by_id(id)
|
||||||
|
|
||||||
if toolkit:
|
if tools:
|
||||||
if id in request.app.state.TOOLS:
|
if id in request.app.state.TOOLS:
|
||||||
toolkit_module = request.app.state.TOOLS[id]
|
tools_module = request.app.state.TOOLS[id]
|
||||||
else:
|
else:
|
||||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
tools_module, _ = load_tools_module_by_id(id)
|
||||||
request.app.state.TOOLS[id] = toolkit_module
|
request.app.state.TOOLS[id] = tools_module
|
||||||
|
|
||||||
if hasattr(toolkit_module, "UserValves"):
|
if hasattr(tools_module, "UserValves"):
|
||||||
UserValves = toolkit_module.UserValves
|
UserValves = tools_module.UserValves
|
||||||
|
|
||||||
try:
|
try:
|
||||||
form_data = {k: v for k, v in form_data.items() if v is not None}
|
form_data = {k: v for k, v in form_data.items() if v is not None}
|
||||||
|
|
|
||||||
|
|
@ -31,21 +31,58 @@ async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)
|
||||||
return Users.get_users(skip, limit)
|
return Users.get_users(skip, limit)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# User Groups
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/groups")
|
||||||
|
async def get_user_groups(user=Depends(get_verified_user)):
|
||||||
|
return Users.get_user_groups(user.id)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# User Permissions
|
# User Permissions
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/permissions/user")
|
@router.get("/permissions")
|
||||||
|
async def get_user_permissisions(user=Depends(get_verified_user)):
|
||||||
|
return Users.get_user_groups(user.id)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# User Default Permissions
|
||||||
|
############################
|
||||||
|
class WorkspacePermissions(BaseModel):
|
||||||
|
models: bool
|
||||||
|
knowledge: bool
|
||||||
|
prompts: bool
|
||||||
|
tools: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ChatPermissions(BaseModel):
|
||||||
|
file_upload: bool
|
||||||
|
delete: bool
|
||||||
|
edit: bool
|
||||||
|
temporary: bool
|
||||||
|
|
||||||
|
|
||||||
|
class UserPermissions(BaseModel):
|
||||||
|
workspace: WorkspacePermissions
|
||||||
|
chat: ChatPermissions
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/default/permissions")
|
||||||
async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
|
async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
|
||||||
return request.app.state.config.USER_PERMISSIONS
|
return request.app.state.config.USER_PERMISSIONS
|
||||||
|
|
||||||
|
|
||||||
@router.post("/permissions/user")
|
@router.post("/default/permissions")
|
||||||
async def update_user_permissions(
|
async def update_user_permissions(
|
||||||
request: Request, form_data: dict, user=Depends(get_admin_user)
|
request: Request, form_data: UserPermissions, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
request.app.state.config.USER_PERMISSIONS = form_data
|
request.app.state.config.USER_PERMISSIONS = form_data.model_dump()
|
||||||
return request.app.state.config.USER_PERMISSIONS
|
return request.app.state.config.USER_PERMISSIONS
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ def replace_imports(content):
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
def load_toolkit_module_by_id(toolkit_id, content=None):
|
def load_tools_module_by_id(toolkit_id, content=None):
|
||||||
|
|
||||||
if content is None:
|
if content is None:
|
||||||
tool = Tools.get_tool_by_id(toolkit_id)
|
tool = Tools.get_tool_by_id(toolkit_id)
|
||||||
|
|
|
||||||
|
|
@ -739,12 +739,36 @@ DEFAULT_USER_ROLE = PersistentConfig(
|
||||||
os.getenv("DEFAULT_USER_ROLE", "pending"),
|
os.getenv("DEFAULT_USER_ROLE", "pending"),
|
||||||
)
|
)
|
||||||
|
|
||||||
USER_PERMISSIONS_CHAT_DELETION = (
|
|
||||||
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
|
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
|
||||||
|
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
|
||||||
|
== "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
USER_PERMISSIONS_CHAT_EDITING = (
|
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = (
|
||||||
os.environ.get("USER_PERMISSIONS_CHAT_EDITING", "True").lower() == "true"
|
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 = (
|
USER_PERMISSIONS_CHAT_TEMPORARY = (
|
||||||
|
|
@ -753,13 +777,20 @@ USER_PERMISSIONS_CHAT_TEMPORARY = (
|
||||||
|
|
||||||
USER_PERMISSIONS = PersistentConfig(
|
USER_PERMISSIONS = PersistentConfig(
|
||||||
"USER_PERMISSIONS",
|
"USER_PERMISSIONS",
|
||||||
"ui.user_permissions",
|
"user.permissions",
|
||||||
{
|
{
|
||||||
|
"workspace": {
|
||||||
|
"models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
|
||||||
|
"knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS,
|
||||||
|
"prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS,
|
||||||
|
"tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS,
|
||||||
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"deletion": USER_PERMISSIONS_CHAT_DELETION,
|
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
|
||||||
"editing": USER_PERMISSIONS_CHAT_EDITING,
|
"delete": USER_PERMISSIONS_CHAT_DELETE,
|
||||||
|
"edit": USER_PERMISSIONS_CHAT_EDIT,
|
||||||
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
|
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -785,18 +816,6 @@ DEFAULT_ARENA_MODEL = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ENABLE_MODEL_FILTER = PersistentConfig(
|
|
||||||
"ENABLE_MODEL_FILTER",
|
|
||||||
"model_filter.enable",
|
|
||||||
os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true",
|
|
||||||
)
|
|
||||||
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
|
|
||||||
MODEL_FILTER_LIST = PersistentConfig(
|
|
||||||
"MODEL_FILTER_LIST",
|
|
||||||
"model_filter.list",
|
|
||||||
[model.strip() for model in MODEL_FILTER_LIST.split(";")],
|
|
||||||
)
|
|
||||||
|
|
||||||
WEBHOOK_URL = PersistentConfig(
|
WEBHOOK_URL = PersistentConfig(
|
||||||
"WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
|
"WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import random
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from aiocache import cached
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import requests
|
import requests
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
|
|
@ -45,6 +46,7 @@ from open_webui.apps.openai.main import (
|
||||||
app as openai_app,
|
app as openai_app,
|
||||||
generate_chat_completion as generate_openai_chat_completion,
|
generate_chat_completion as generate_openai_chat_completion,
|
||||||
get_all_models as get_openai_models,
|
get_all_models as get_openai_models,
|
||||||
|
get_all_models_responses as get_openai_models_responses,
|
||||||
)
|
)
|
||||||
from open_webui.apps.retrieval.main import app as retrieval_app
|
from open_webui.apps.retrieval.main import app as retrieval_app
|
||||||
from open_webui.apps.retrieval.utils import get_rag_context, rag_template
|
from open_webui.apps.retrieval.utils import get_rag_context, rag_template
|
||||||
|
|
@ -70,13 +72,11 @@ from open_webui.config import (
|
||||||
DEFAULT_LOCALE,
|
DEFAULT_LOCALE,
|
||||||
ENABLE_ADMIN_CHAT_ACCESS,
|
ENABLE_ADMIN_CHAT_ACCESS,
|
||||||
ENABLE_ADMIN_EXPORT,
|
ENABLE_ADMIN_EXPORT,
|
||||||
ENABLE_MODEL_FILTER,
|
|
||||||
ENABLE_OLLAMA_API,
|
ENABLE_OLLAMA_API,
|
||||||
ENABLE_OPENAI_API,
|
ENABLE_OPENAI_API,
|
||||||
ENABLE_TAGS_GENERATION,
|
ENABLE_TAGS_GENERATION,
|
||||||
ENV,
|
ENV,
|
||||||
FRONTEND_BUILD_DIR,
|
FRONTEND_BUILD_DIR,
|
||||||
MODEL_FILTER_LIST,
|
|
||||||
OAUTH_PROVIDERS,
|
OAUTH_PROVIDERS,
|
||||||
ENABLE_SEARCH_QUERY,
|
ENABLE_SEARCH_QUERY,
|
||||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
|
@ -135,6 +135,7 @@ from open_webui.utils.utils import (
|
||||||
get_http_authorization_cred,
|
get_http_authorization_cred,
|
||||||
get_verified_user,
|
get_verified_user,
|
||||||
)
|
)
|
||||||
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
if SAFE_MODE:
|
if SAFE_MODE:
|
||||||
print("SAFE MODE ENABLED")
|
print("SAFE MODE ENABLED")
|
||||||
|
|
@ -183,7 +184,10 @@ async def lifespan(app: FastAPI):
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
docs_url="/docs" if ENV == "dev" else None, openapi_url="/openapi.json" if ENV == "dev" else None, redoc_url=None, lifespan=lifespan
|
docs_url="/docs" if ENV == "dev" else None,
|
||||||
|
openapi_url="/openapi.json" if ENV == "dev" else None,
|
||||||
|
redoc_url=None,
|
||||||
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.state.config = AppConfig()
|
app.state.config = AppConfig()
|
||||||
|
|
@ -191,27 +195,26 @@ app.state.config = AppConfig()
|
||||||
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
|
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
|
||||||
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
||||||
|
|
||||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
|
||||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
|
||||||
|
|
||||||
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||||
|
|
||||||
app.state.config.TASK_MODEL = TASK_MODEL
|
app.state.config.TASK_MODEL = TASK_MODEL
|
||||||
app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
|
app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
|
||||||
|
|
||||||
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
||||||
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
|
|
||||||
app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
|
app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
|
||||||
|
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
|
||||||
|
|
||||||
|
|
||||||
|
app.state.config.ENABLE_SEARCH_QUERY = ENABLE_SEARCH_QUERY
|
||||||
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||||
)
|
)
|
||||||
app.state.config.ENABLE_SEARCH_QUERY = ENABLE_SEARCH_QUERY
|
|
||||||
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
||||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||||
)
|
)
|
||||||
|
|
||||||
app.state.MODELS = {}
|
|
||||||
|
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
#
|
#
|
||||||
# ChatCompletion Middleware
|
# ChatCompletion Middleware
|
||||||
|
|
@ -219,26 +222,6 @@ app.state.MODELS = {}
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
|
|
||||||
def get_task_model_id(default_model_id):
|
|
||||||
# Set the task model
|
|
||||||
task_model_id = default_model_id
|
|
||||||
# Check if the user has a custom task model and use that model
|
|
||||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama":
|
|
||||||
if (
|
|
||||||
app.state.config.TASK_MODEL
|
|
||||||
and app.state.config.TASK_MODEL in app.state.MODELS
|
|
||||||
):
|
|
||||||
task_model_id = app.state.config.TASK_MODEL
|
|
||||||
else:
|
|
||||||
if (
|
|
||||||
app.state.config.TASK_MODEL_EXTERNAL
|
|
||||||
and app.state.config.TASK_MODEL_EXTERNAL in app.state.MODELS
|
|
||||||
):
|
|
||||||
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
|
||||||
|
|
||||||
return task_model_id
|
|
||||||
|
|
||||||
|
|
||||||
def get_filter_function_ids(model):
|
def get_filter_function_ids(model):
|
||||||
def get_priority(function_id):
|
def get_priority(function_id):
|
||||||
function = Functions.get_function_by_id(function_id)
|
function = Functions.get_function_by_id(function_id)
|
||||||
|
|
@ -368,8 +351,24 @@ async def get_content_from_response(response) -> Optional[str]:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_model_id(
|
||||||
|
default_model_id: str, task_model: str, task_model_external: str, models
|
||||||
|
) -> str:
|
||||||
|
# Set the task model
|
||||||
|
task_model_id = default_model_id
|
||||||
|
# Check if the user has a custom task model and use that model
|
||||||
|
if models[task_model_id]["owned_by"] == "ollama":
|
||||||
|
if task_model and task_model in models:
|
||||||
|
task_model_id = task_model
|
||||||
|
else:
|
||||||
|
if task_model_external and task_model_external in models:
|
||||||
|
task_model_id = task_model_external
|
||||||
|
|
||||||
|
return task_model_id
|
||||||
|
|
||||||
|
|
||||||
async def chat_completion_tools_handler(
|
async def chat_completion_tools_handler(
|
||||||
body: dict, user: UserModel, extra_params: dict
|
body: dict, user: UserModel, models, extra_params: dict
|
||||||
) -> tuple[dict, dict]:
|
) -> tuple[dict, dict]:
|
||||||
# If tool_ids field is present, call the functions
|
# If tool_ids field is present, call the functions
|
||||||
metadata = body.get("metadata", {})
|
metadata = body.get("metadata", {})
|
||||||
|
|
@ -383,14 +382,19 @@ async def chat_completion_tools_handler(
|
||||||
contexts = []
|
contexts = []
|
||||||
citations = []
|
citations = []
|
||||||
|
|
||||||
task_model_id = get_task_model_id(body["model"])
|
task_model_id = get_task_model_id(
|
||||||
|
body["model"],
|
||||||
|
app.state.config.TASK_MODEL,
|
||||||
|
app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
models,
|
||||||
|
)
|
||||||
tools = get_tools(
|
tools = get_tools(
|
||||||
webui_app,
|
webui_app,
|
||||||
tool_ids,
|
tool_ids,
|
||||||
user,
|
user,
|
||||||
{
|
{
|
||||||
**extra_params,
|
**extra_params,
|
||||||
"__model__": app.state.MODELS[task_model_id],
|
"__model__": models[task_model_id],
|
||||||
"__messages__": body["messages"],
|
"__messages__": body["messages"],
|
||||||
"__files__": metadata.get("files", []),
|
"__files__": metadata.get("files", []),
|
||||||
},
|
},
|
||||||
|
|
@ -414,7 +418,7 @@ async def chat_completion_tools_handler(
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = filter_pipeline(payload, user)
|
payload = filter_pipeline(payload, user, models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
@ -515,16 +519,16 @@ def is_chat_completion_request(request):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_body_and_model_and_user(request):
|
async def get_body_and_model_and_user(request, models):
|
||||||
# Read the original request body
|
# Read the original request body
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
body_str = body.decode("utf-8")
|
body_str = body.decode("utf-8")
|
||||||
body = json.loads(body_str) if body_str else {}
|
body = json.loads(body_str) if body_str else {}
|
||||||
|
|
||||||
model_id = body["model"]
|
model_id = body["model"]
|
||||||
if model_id not in app.state.MODELS:
|
if model_id not in models:
|
||||||
raise Exception("Model not found")
|
raise Exception("Model not found")
|
||||||
model = app.state.MODELS[model_id]
|
model = models[model_id]
|
||||||
|
|
||||||
user = get_current_user(
|
user = get_current_user(
|
||||||
request,
|
request,
|
||||||
|
|
@ -540,14 +544,27 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
log.debug(f"request.url.path: {request.url.path}")
|
log.debug(f"request.url.path: {request.url.path}")
|
||||||
|
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body, model, user = await get_body_and_model_and_user(request)
|
body, model, user = await get_body_and_model_and_user(request, models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
content={"detail": str(e)},
|
content={"detail": str(e)},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_info = Models.get_model_by_id(model["id"])
|
||||||
|
if user.role == "user":
|
||||||
|
if model_info and not has_access(
|
||||||
|
user.id, type="read", access_control=model_info.access_control
|
||||||
|
):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
content={"detail": "User does not have access to the model"},
|
||||||
|
)
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
"chat_id": body.pop("chat_id", None),
|
"chat_id": body.pop("chat_id", None),
|
||||||
"message_id": body.pop("id", None),
|
"message_id": body.pop("id", None),
|
||||||
|
|
@ -584,15 +601,20 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
||||||
content={"detail": str(e)},
|
content={"detail": str(e)},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tool_ids = body.pop("tool_ids", None)
|
||||||
|
files = body.pop("files", None)
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
**metadata,
|
**metadata,
|
||||||
"tool_ids": body.pop("tool_ids", None),
|
"tool_ids": tool_ids,
|
||||||
"files": body.pop("files", None),
|
"files": files,
|
||||||
}
|
}
|
||||||
body["metadata"] = metadata
|
body["metadata"] = metadata
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body, flags = await chat_completion_tools_handler(body, user, extra_params)
|
body, flags = await chat_completion_tools_handler(
|
||||||
|
body, user, models, extra_params
|
||||||
|
)
|
||||||
contexts.extend(flags.get("contexts", []))
|
contexts.extend(flags.get("contexts", []))
|
||||||
citations.extend(flags.get("citations", []))
|
citations.extend(flags.get("citations", []))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -689,10 +711,10 @@ app.add_middleware(ChatCompletionMiddleware)
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
|
|
||||||
def get_sorted_filters(model_id):
|
def get_sorted_filters(model_id, models):
|
||||||
filters = [
|
filters = [
|
||||||
model
|
model
|
||||||
for model in app.state.MODELS.values()
|
for model in models.values()
|
||||||
if "pipeline" in model
|
if "pipeline" in model
|
||||||
and "type" in model["pipeline"]
|
and "type" in model["pipeline"]
|
||||||
and model["pipeline"]["type"] == "filter"
|
and model["pipeline"]["type"] == "filter"
|
||||||
|
|
@ -708,12 +730,12 @@ def get_sorted_filters(model_id):
|
||||||
return sorted_filters
|
return sorted_filters
|
||||||
|
|
||||||
|
|
||||||
def filter_pipeline(payload, user):
|
def filter_pipeline(payload, user, models):
|
||||||
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
|
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
|
||||||
model_id = payload["model"]
|
model_id = payload["model"]
|
||||||
sorted_filters = get_sorted_filters(model_id)
|
|
||||||
|
|
||||||
model = app.state.MODELS[model_id]
|
sorted_filters = get_sorted_filters(model_id, models)
|
||||||
|
model = models[model_id]
|
||||||
|
|
||||||
if "pipeline" in model:
|
if "pipeline" in model:
|
||||||
sorted_filters.append(model)
|
sorted_filters.append(model)
|
||||||
|
|
@ -784,8 +806,11 @@ class PipelineMiddleware(BaseHTTPMiddleware):
|
||||||
content={"detail": "Not authenticated"},
|
content={"detail": "Not authenticated"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = filter_pipeline(data, user)
|
data = filter_pipeline(data, user, models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if len(e.args) > 1:
|
if len(e.args) > 1:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|
@ -864,16 +889,10 @@ async def commit_session_after_request(request: Request, call_next):
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def check_url(request: Request, call_next):
|
async def check_url(request: Request, call_next):
|
||||||
if len(app.state.MODELS) == 0:
|
|
||||||
await get_all_models()
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
start_time = int(time.time())
|
start_time = int(time.time())
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
process_time = int(time.time()) - start_time
|
process_time = int(time.time()) - start_time
|
||||||
response.headers["X-Process-Time"] = str(process_time)
|
response.headers["X-Process-Time"] = str(process_time)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -913,12 +932,10 @@ app.mount("/retrieval/api/v1", retrieval_app)
|
||||||
|
|
||||||
app.mount("/api/v1", webui_app)
|
app.mount("/api/v1", webui_app)
|
||||||
|
|
||||||
|
|
||||||
webui_app.state.EMBEDDING_FUNCTION = retrieval_app.state.EMBEDDING_FUNCTION
|
webui_app.state.EMBEDDING_FUNCTION = retrieval_app.state.EMBEDDING_FUNCTION
|
||||||
|
|
||||||
|
|
||||||
async def get_all_models():
|
async def get_all_base_models():
|
||||||
# TODO: Optimize this function
|
|
||||||
open_webui_models = []
|
open_webui_models = []
|
||||||
openai_models = []
|
openai_models = []
|
||||||
ollama_models = []
|
ollama_models = []
|
||||||
|
|
@ -944,9 +961,15 @@ async def get_all_models():
|
||||||
open_webui_models = await get_open_webui_models()
|
open_webui_models = await get_open_webui_models()
|
||||||
|
|
||||||
models = open_webui_models + openai_models + ollama_models
|
models = open_webui_models + openai_models + ollama_models
|
||||||
|
return models
|
||||||
|
|
||||||
|
|
||||||
|
@cached(ttl=1)
|
||||||
|
async def get_all_models():
|
||||||
|
models = await get_all_base_models()
|
||||||
|
|
||||||
# If there are no models, return an empty list
|
# If there are no models, return an empty list
|
||||||
if len([model for model in models if model["owned_by"] != "arena"]) == 0:
|
if len([model for model in models if not model.get("arena", False)]) == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
global_action_ids = [
|
global_action_ids = [
|
||||||
|
|
@ -965,15 +988,23 @@ async def get_all_models():
|
||||||
custom_model.id == model["id"]
|
custom_model.id == model["id"]
|
||||||
or custom_model.id == model["id"].split(":")[0]
|
or custom_model.id == model["id"].split(":")[0]
|
||||||
):
|
):
|
||||||
model["name"] = custom_model.name
|
if custom_model.is_active:
|
||||||
model["info"] = custom_model.model_dump()
|
model["name"] = custom_model.name
|
||||||
|
model["info"] = custom_model.model_dump()
|
||||||
|
|
||||||
action_ids = []
|
action_ids = []
|
||||||
if "info" in model and "meta" in model["info"]:
|
if "info" in model and "meta" in model["info"]:
|
||||||
action_ids.extend(model["info"]["meta"].get("actionIds", []))
|
action_ids.extend(
|
||||||
|
model["info"]["meta"].get("actionIds", [])
|
||||||
|
)
|
||||||
|
|
||||||
model["action_ids"] = action_ids
|
model["action_ids"] = action_ids
|
||||||
else:
|
else:
|
||||||
|
models.remove(model)
|
||||||
|
|
||||||
|
elif custom_model.is_active and (
|
||||||
|
custom_model.id not in [model["id"] for model in models]
|
||||||
|
):
|
||||||
owned_by = "openai"
|
owned_by = "openai"
|
||||||
pipe = None
|
pipe = None
|
||||||
action_ids = []
|
action_ids = []
|
||||||
|
|
@ -995,7 +1026,7 @@ async def get_all_models():
|
||||||
|
|
||||||
models.append(
|
models.append(
|
||||||
{
|
{
|
||||||
"id": custom_model.id,
|
"id": f"{custom_model.id}",
|
||||||
"name": custom_model.name,
|
"name": custom_model.name,
|
||||||
"object": "model",
|
"object": "model",
|
||||||
"created": custom_model.created_at,
|
"created": custom_model.created_at,
|
||||||
|
|
@ -1007,66 +1038,54 @@ async def get_all_models():
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
for model in models:
|
# Process action_ids to get the actions
|
||||||
action_ids = []
|
def get_action_items_from_module(module):
|
||||||
if "action_ids" in model:
|
actions = []
|
||||||
action_ids = model["action_ids"]
|
if hasattr(module, "actions"):
|
||||||
del model["action_ids"]
|
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
|
def get_function_module_by_id(function_id):
|
||||||
action_ids = list(set(action_ids))
|
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_ids = [
|
||||||
action_id for action_id in action_ids if action_id in enabled_action_ids
|
action_id
|
||||||
|
for action_id in list(set(model.pop("action_ids", []) + global_action_ids))
|
||||||
|
if action_id in enabled_action_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
model["actions"] = []
|
model["actions"] = []
|
||||||
for action_id in action_ids:
|
for action_id in action_ids:
|
||||||
action = Functions.get_function_by_id(action_id)
|
action_function = Functions.get_function_by_id(action_id)
|
||||||
if action is None:
|
if action_function is None:
|
||||||
raise Exception(f"Action not found: {action_id}")
|
raise Exception(f"Action not found: {action_id}")
|
||||||
|
|
||||||
if action_id in webui_app.state.FUNCTIONS:
|
function_module = get_function_module_by_id(action_id)
|
||||||
function_module = webui_app.state.FUNCTIONS[action_id]
|
model["actions"].extend(get_action_items_from_module(function_module))
|
||||||
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
|
|
||||||
|
|
||||||
return models
|
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 "pipeline" not in model or model["pipeline"].get("type", None) != "filter"
|
||||||
]
|
]
|
||||||
|
|
||||||
if app.state.config.ENABLE_MODEL_FILTER:
|
# Filter out models that the user does not have access to
|
||||||
if user.role == "user":
|
if user.role == "user":
|
||||||
models = list(
|
filtered_models = []
|
||||||
filter(
|
for model in models:
|
||||||
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
|
model_info = Models.get_model_by_id(model["id"])
|
||||||
models,
|
if model_info:
|
||||||
)
|
if has_access(
|
||||||
)
|
user.id, type="read", access_control=model_info.access_control
|
||||||
return {"data": models}
|
):
|
||||||
|
filtered_models.append(model)
|
||||||
|
else:
|
||||||
|
filtered_models.append(model)
|
||||||
|
models = filtered_models
|
||||||
|
|
||||||
return {"data": 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")
|
@app.post("/api/chat/completions")
|
||||||
async def generate_chat_completions(
|
async def generate_chat_completions(
|
||||||
form_data: dict, user=Depends(get_verified_user), bypass_filter: bool = False
|
form_data: dict, user=Depends(get_verified_user), bypass_filter: bool = False
|
||||||
):
|
):
|
||||||
model_id = form_data["model"]
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
if model_id not in app.state.MODELS:
|
model_id = form_data["model"]
|
||||||
|
if model_id not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not bypass_filter and app.state.config.ENABLE_MODEL_FILTER:
|
model = models[model_id]
|
||||||
if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
|
# Check if user has access to the model
|
||||||
|
if user.role == "user":
|
||||||
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
if not has_access(
|
||||||
|
user.id, type="read", access_control=model_info.access_control
|
||||||
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=403,
|
||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
model = app.state.MODELS[model_id]
|
|
||||||
|
|
||||||
if model["owned_by"] == "arena":
|
if model["owned_by"] == "arena":
|
||||||
model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
|
model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
|
||||||
filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")
|
filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")
|
||||||
|
|
@ -1161,14 +1198,18 @@ async def generate_chat_completions(
|
||||||
),
|
),
|
||||||
"selected_model_id": selected_model_id,
|
"selected_model_id": selected_model_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.get("pipe"):
|
if model.get("pipe"):
|
||||||
return await generate_function_chat_completion(form_data, user=user)
|
# Below does not require bypass_filter because this is the only route the uses this function and it is already bypassing the filter
|
||||||
|
return await generate_function_chat_completion(
|
||||||
|
form_data, user=user, models=models
|
||||||
|
)
|
||||||
if model["owned_by"] == "ollama":
|
if model["owned_by"] == "ollama":
|
||||||
# Using /ollama/api/chat endpoint
|
# Using /ollama/api/chat endpoint
|
||||||
form_data = convert_payload_openai_to_ollama(form_data)
|
form_data = convert_payload_openai_to_ollama(form_data)
|
||||||
form_data = GenerateChatCompletionForm(**form_data)
|
form_data = GenerateChatCompletionForm(**form_data)
|
||||||
response = await generate_ollama_chat_completion(
|
response = await generate_ollama_chat_completion(
|
||||||
form_data=form_data, user=user, bypass_filter=True
|
form_data=form_data, user=user, bypass_filter=bypass_filter
|
||||||
)
|
)
|
||||||
if form_data.stream:
|
if form_data.stream:
|
||||||
response.headers["content-type"] = "text/event-stream"
|
response.headers["content-type"] = "text/event-stream"
|
||||||
|
|
@ -1179,21 +1220,27 @@ async def generate_chat_completions(
|
||||||
else:
|
else:
|
||||||
return convert_response_ollama_to_openai(response)
|
return convert_response_ollama_to_openai(response)
|
||||||
else:
|
else:
|
||||||
return await generate_openai_chat_completion(form_data, user=user)
|
return await generate_openai_chat_completion(
|
||||||
|
form_data, user=user, bypass_filter=bypass_filter
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/chat/completed")
|
@app.post("/api/chat/completed")
|
||||||
async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
|
async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
|
||||||
|
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
data = form_data
|
data = form_data
|
||||||
model_id = data["model"]
|
model_id = data["model"]
|
||||||
if model_id not in app.state.MODELS:
|
if model_id not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
)
|
)
|
||||||
model = app.state.MODELS[model_id]
|
|
||||||
|
|
||||||
sorted_filters = get_sorted_filters(model_id)
|
model = models[model_id]
|
||||||
|
sorted_filters = get_sorted_filters(model_id, models)
|
||||||
if "pipeline" in model:
|
if "pipeline" in model:
|
||||||
sorted_filters = [model] + sorted_filters
|
sorted_filters = [model] + sorted_filters
|
||||||
|
|
||||||
|
|
@ -1368,14 +1415,18 @@ async def chat_action(action_id: str, form_data: dict, user=Depends(get_verified
|
||||||
detail="Action not found",
|
detail="Action not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
data = form_data
|
data = form_data
|
||||||
model_id = data["model"]
|
model_id = data["model"]
|
||||||
if model_id not in app.state.MODELS:
|
|
||||||
|
if model_id not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
)
|
)
|
||||||
model = app.state.MODELS[model_id]
|
model = models[model_id]
|
||||||
|
|
||||||
__event_emitter__ = get_event_emitter(
|
__event_emitter__ = get_event_emitter(
|
||||||
{
|
{
|
||||||
|
|
@ -1529,8 +1580,11 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
||||||
async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||||
print("generate_title")
|
print("generate_title")
|
||||||
|
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
model_id = form_data["model"]
|
model_id = form_data["model"]
|
||||||
if model_id not in app.state.MODELS:
|
if model_id not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
|
|
@ -1538,10 +1592,16 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||||
|
|
||||||
# Check if the user has a custom task model
|
# Check if the user has a custom task model
|
||||||
# If the user has a custom task model, use that model
|
# If the user has a custom task model, use that model
|
||||||
task_model_id = get_task_model_id(model_id)
|
task_model_id = get_task_model_id(
|
||||||
|
model_id,
|
||||||
|
app.state.config.TASK_MODEL,
|
||||||
|
app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
models,
|
||||||
|
)
|
||||||
|
|
||||||
print(task_model_id)
|
print(task_model_id)
|
||||||
|
|
||||||
model = app.state.MODELS[task_model_id]
|
model = models[task_model_id]
|
||||||
|
|
||||||
if app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "":
|
if app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "":
|
||||||
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
|
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||||
|
|
@ -1575,7 +1635,7 @@ Artificial Intelligence in Healthcare
|
||||||
"stream": False,
|
"stream": False,
|
||||||
**(
|
**(
|
||||||
{"max_tokens": 50}
|
{"max_tokens": 50}
|
||||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
if models[task_model_id]["owned_by"] == "ollama"
|
||||||
else {
|
else {
|
||||||
"max_completion_tokens": 50,
|
"max_completion_tokens": 50,
|
||||||
}
|
}
|
||||||
|
|
@ -1587,7 +1647,7 @@ Artificial Intelligence in Healthcare
|
||||||
|
|
||||||
# Handle pipeline filters
|
# Handle pipeline filters
|
||||||
try:
|
try:
|
||||||
payload = filter_pipeline(payload, user)
|
payload = filter_pipeline(payload, user, models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if len(e.args) > 1:
|
if len(e.args) > 1:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|
@ -1614,8 +1674,11 @@ async def generate_chat_tags(form_data: dict, user=Depends(get_verified_user)):
|
||||||
content={"detail": "Tags generation is disabled"},
|
content={"detail": "Tags generation is disabled"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
model_id = form_data["model"]
|
model_id = form_data["model"]
|
||||||
if model_id not in app.state.MODELS:
|
if model_id not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
|
|
@ -1623,7 +1686,12 @@ async def generate_chat_tags(form_data: dict, user=Depends(get_verified_user)):
|
||||||
|
|
||||||
# Check if the user has a custom task model
|
# Check if the user has a custom task model
|
||||||
# If the user has a custom task model, use that model
|
# If the user has a custom task model, use that model
|
||||||
task_model_id = get_task_model_id(model_id)
|
task_model_id = get_task_model_id(
|
||||||
|
model_id,
|
||||||
|
app.state.config.TASK_MODEL,
|
||||||
|
app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
models,
|
||||||
|
)
|
||||||
print(task_model_id)
|
print(task_model_id)
|
||||||
|
|
||||||
if app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "":
|
if app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "":
|
||||||
|
|
@ -1661,7 +1729,7 @@ JSON format: { "tags": ["tag1", "tag2", "tag3"] }
|
||||||
|
|
||||||
# Handle pipeline filters
|
# Handle pipeline filters
|
||||||
try:
|
try:
|
||||||
payload = filter_pipeline(payload, user)
|
payload = filter_pipeline(payload, user, models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if len(e.args) > 1:
|
if len(e.args) > 1:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|
@ -1688,8 +1756,11 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||||
detail=f"Search query generation is disabled",
|
detail=f"Search query generation is disabled",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
model_id = form_data["model"]
|
model_id = form_data["model"]
|
||||||
if model_id not in app.state.MODELS:
|
if model_id not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
|
|
@ -1697,10 +1768,15 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||||
|
|
||||||
# Check if the user has a custom task model
|
# Check if the user has a custom task model
|
||||||
# If the user has a custom task model, use that model
|
# If the user has a custom task model, use that model
|
||||||
task_model_id = get_task_model_id(model_id)
|
task_model_id = get_task_model_id(
|
||||||
|
model_id,
|
||||||
|
app.state.config.TASK_MODEL,
|
||||||
|
app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
models,
|
||||||
|
)
|
||||||
print(task_model_id)
|
print(task_model_id)
|
||||||
|
|
||||||
model = app.state.MODELS[task_model_id]
|
model = models[task_model_id]
|
||||||
|
|
||||||
if app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE != "":
|
if app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE != "":
|
||||||
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||||
|
|
@ -1727,7 +1803,7 @@ Search Query:"""
|
||||||
"stream": False,
|
"stream": False,
|
||||||
**(
|
**(
|
||||||
{"max_tokens": 30}
|
{"max_tokens": 30}
|
||||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
if models[task_model_id]["owned_by"] == "ollama"
|
||||||
else {
|
else {
|
||||||
"max_completion_tokens": 30,
|
"max_completion_tokens": 30,
|
||||||
}
|
}
|
||||||
|
|
@ -1738,7 +1814,7 @@ Search Query:"""
|
||||||
|
|
||||||
# Handle pipeline filters
|
# Handle pipeline filters
|
||||||
try:
|
try:
|
||||||
payload = filter_pipeline(payload, user)
|
payload = filter_pipeline(payload, user, models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if len(e.args) > 1:
|
if len(e.args) > 1:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|
@ -1760,8 +1836,11 @@ Search Query:"""
|
||||||
async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
|
async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
|
||||||
print("generate_emoji")
|
print("generate_emoji")
|
||||||
|
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
model_id = form_data["model"]
|
model_id = form_data["model"]
|
||||||
if model_id not in app.state.MODELS:
|
if model_id not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
|
|
@ -1769,10 +1848,15 @@ async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
|
||||||
|
|
||||||
# Check if the user has a custom task model
|
# Check if the user has a custom task model
|
||||||
# If the user has a custom task model, use that model
|
# If the user has a custom task model, use that model
|
||||||
task_model_id = get_task_model_id(model_id)
|
task_model_id = get_task_model_id(
|
||||||
|
model_id,
|
||||||
|
app.state.config.TASK_MODEL,
|
||||||
|
app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
models,
|
||||||
|
)
|
||||||
print(task_model_id)
|
print(task_model_id)
|
||||||
|
|
||||||
model = app.state.MODELS[task_model_id]
|
model = models[task_model_id]
|
||||||
|
|
||||||
template = '''
|
template = '''
|
||||||
Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
|
Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
|
||||||
|
|
@ -1794,7 +1878,7 @@ Message: """{{prompt}}"""
|
||||||
"stream": False,
|
"stream": False,
|
||||||
**(
|
**(
|
||||||
{"max_tokens": 4}
|
{"max_tokens": 4}
|
||||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
if models[task_model_id]["owned_by"] == "ollama"
|
||||||
else {
|
else {
|
||||||
"max_completion_tokens": 4,
|
"max_completion_tokens": 4,
|
||||||
}
|
}
|
||||||
|
|
@ -1806,7 +1890,7 @@ Message: """{{prompt}}"""
|
||||||
|
|
||||||
# Handle pipeline filters
|
# Handle pipeline filters
|
||||||
try:
|
try:
|
||||||
payload = filter_pipeline(payload, user)
|
payload = filter_pipeline(payload, user, models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if len(e.args) > 1:
|
if len(e.args) > 1:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|
@ -1828,8 +1912,11 @@ Message: """{{prompt}}"""
|
||||||
async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)):
|
async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)):
|
||||||
print("generate_moa_response")
|
print("generate_moa_response")
|
||||||
|
|
||||||
|
model_list = await get_all_models()
|
||||||
|
models = {model["id"]: model for model in model_list}
|
||||||
|
|
||||||
model_id = form_data["model"]
|
model_id = form_data["model"]
|
||||||
if model_id not in app.state.MODELS:
|
if model_id not in models:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
|
|
@ -1837,10 +1924,15 @@ async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)
|
||||||
|
|
||||||
# Check if the user has a custom task model
|
# Check if the user has a custom task model
|
||||||
# If the user has a custom task model, use that model
|
# If the user has a custom task model, use that model
|
||||||
task_model_id = get_task_model_id(model_id)
|
task_model_id = get_task_model_id(
|
||||||
|
model_id,
|
||||||
|
app.state.config.TASK_MODEL,
|
||||||
|
app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
models,
|
||||||
|
)
|
||||||
print(task_model_id)
|
print(task_model_id)
|
||||||
|
|
||||||
model = app.state.MODELS[task_model_id]
|
model = models[task_model_id]
|
||||||
|
|
||||||
template = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
|
template = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
|
||||||
|
|
||||||
|
|
@ -1867,7 +1959,7 @@ Responses from models: {{responses}}"""
|
||||||
log.debug(payload)
|
log.debug(payload)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = filter_pipeline(payload, user)
|
payload = filter_pipeline(payload, user, models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if len(e.args) > 1:
|
if len(e.args) > 1:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|
@ -1897,7 +1989,7 @@ Responses from models: {{responses}}"""
|
||||||
|
|
||||||
@app.get("/api/pipelines/list")
|
@app.get("/api/pipelines/list")
|
||||||
async def get_pipelines_list(user=Depends(get_admin_user)):
|
async def get_pipelines_list(user=Depends(get_admin_user)):
|
||||||
responses = await get_openai_models(raw=True)
|
responses = await get_openai_models_responses()
|
||||||
|
|
||||||
print(responses)
|
print(responses)
|
||||||
urlIdxs = [
|
urlIdxs = [
|
||||||
|
|
@ -2297,32 +2389,6 @@ async def get_app_config(request: Request):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/config/model/filter")
|
|
||||||
async def get_model_filter_config(user=Depends(get_admin_user)):
|
|
||||||
return {
|
|
||||||
"enabled": app.state.config.ENABLE_MODEL_FILTER,
|
|
||||||
"models": app.state.config.MODEL_FILTER_LIST,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ModelFilterConfigForm(BaseModel):
|
|
||||||
enabled: bool
|
|
||||||
models: list[str]
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/config/model/filter")
|
|
||||||
async def update_model_filter_config(
|
|
||||||
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
|
|
||||||
):
|
|
||||||
app.state.config.ENABLE_MODEL_FILTER = form_data.enabled
|
|
||||||
app.state.config.MODEL_FILTER_LIST = form_data.models
|
|
||||||
|
|
||||||
return {
|
|
||||||
"enabled": app.state.config.ENABLE_MODEL_FILTER,
|
|
||||||
"models": app.state.config.MODEL_FILTER_LIST,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: webhook endpoint should be under config endpoints
|
# TODO: webhook endpoint should be under config endpoints
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.tools import Tools
|
||||||
from open_webui.apps.webui.models.users import UserModel
|
from open_webui.apps.webui.models.users import UserModel
|
||||||
from open_webui.apps.webui.utils import load_toolkit_module_by_id
|
from open_webui.apps.webui.utils import load_tools_module_by_id
|
||||||
from open_webui.utils.schemas import json_schema_to_model
|
from open_webui.utils.schemas import json_schema_to_model
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
@ -32,15 +32,16 @@ def apply_extra_params_to_tool_function(
|
||||||
def get_tools(
|
def get_tools(
|
||||||
webui_app, tool_ids: list[str], user: UserModel, extra_params: dict
|
webui_app, tool_ids: list[str], user: UserModel, extra_params: dict
|
||||||
) -> dict[str, dict]:
|
) -> dict[str, dict]:
|
||||||
tools = {}
|
tools_dict = {}
|
||||||
|
|
||||||
for tool_id in tool_ids:
|
for tool_id in tool_ids:
|
||||||
toolkit = Tools.get_tool_by_id(tool_id)
|
tools = Tools.get_tool_by_id(tool_id)
|
||||||
if toolkit is None:
|
if tools is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
module = webui_app.state.TOOLS.get(tool_id, None)
|
module = webui_app.state.TOOLS.get(tool_id, None)
|
||||||
if module is None:
|
if module is None:
|
||||||
module, _ = load_toolkit_module_by_id(tool_id)
|
module, _ = load_tools_module_by_id(tool_id)
|
||||||
webui_app.state.TOOLS[tool_id] = module
|
webui_app.state.TOOLS[tool_id] = module
|
||||||
|
|
||||||
extra_params["__id__"] = tool_id
|
extra_params["__id__"] = tool_id
|
||||||
|
|
@ -53,11 +54,19 @@ def get_tools(
|
||||||
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
|
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
for spec in toolkit.specs:
|
for spec in tools.specs:
|
||||||
# TODO: Fix hack for OpenAI API
|
# TODO: Fix hack for OpenAI API
|
||||||
for val in spec.get("parameters", {}).get("properties", {}).values():
|
for val in spec.get("parameters", {}).get("properties", {}).values():
|
||||||
if val["type"] == "str":
|
if val["type"] == "str":
|
||||||
val["type"] = "string"
|
val["type"] = "string"
|
||||||
|
|
||||||
|
# Remove internal parameters
|
||||||
|
spec["parameters"]["properties"] = {
|
||||||
|
key: val
|
||||||
|
for key, val in spec["parameters"]["properties"].items()
|
||||||
|
if not key.startswith("__")
|
||||||
|
}
|
||||||
|
|
||||||
function_name = spec["name"]
|
function_name = spec["name"]
|
||||||
|
|
||||||
# convert to function that takes only model params and inserts custom params
|
# convert to function that takes only model params and inserts custom params
|
||||||
|
|
@ -77,13 +86,14 @@ def get_tools(
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: if collision, prepend toolkit name
|
# TODO: if collision, prepend toolkit name
|
||||||
if function_name in tools:
|
if function_name in tools_dict:
|
||||||
log.warning(f"Tool {function_name} already exists in another toolkit!")
|
log.warning(f"Tool {function_name} already exists in another tools!")
|
||||||
log.warning(f"Collision between {toolkit} and {tool_id}.")
|
log.warning(f"Collision between {tools} and {tool_id}.")
|
||||||
log.warning(f"Discarding {toolkit}.{function_name}")
|
log.warning(f"Discarding {tools}.{function_name}")
|
||||||
else:
|
else:
|
||||||
tools[function_name] = tool_dict
|
tools_dict[function_name] = tool_dict
|
||||||
return tools
|
|
||||||
|
return tools_dict
|
||||||
|
|
||||||
|
|
||||||
def doc_to_dict(docstring):
|
def doc_to_dict(docstring):
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime, timedelta
|
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
import jwt
|
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.apps.webui.models.users import Users
|
||||||
|
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import WEBUI_SECRET_KEY
|
from open_webui.env import WEBUI_SECRET_KEY
|
||||||
|
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Request, Response, status
|
from fastapi import Depends, HTTPException, Request, Response, status
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ passlib[bcrypt]==1.7.4
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
aiohttp==3.10.8
|
aiohttp==3.10.8
|
||||||
async-timeout
|
async-timeout
|
||||||
|
aiocache
|
||||||
|
|
||||||
sqlalchemy==2.0.32
|
sqlalchemy==2.0.32
|
||||||
alembic==1.13.2
|
alembic==1.13.2
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ dependencies = [
|
||||||
"requests==2.32.3",
|
"requests==2.32.3",
|
||||||
"aiohttp==3.10.8",
|
"aiohttp==3.10.8",
|
||||||
"async-timeout",
|
"async-timeout",
|
||||||
|
"aiocache",
|
||||||
|
|
||||||
"sqlalchemy==2.0.32",
|
"sqlalchemy==2.0.32",
|
||||||
"alembic==1.13.2",
|
"alembic==1.13.2",
|
||||||
|
|
|
||||||
163
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';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
export const getModels = async (token: string = '') => {
|
export const getModels = async (token: string = '', base: boolean = false) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/models${
|
||||||
const res = await fetch(`${WEBUI_BASE_URL}/api/models`, {
|
base ? '/base' : ''
|
||||||
|
}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
@ -16,36 +17,21 @@ export const getModels = async (token: string = '') => {
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
|
||||||
error = err;
|
error = err;
|
||||||
|
console.log(err);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
let models = res?.data ?? [];
|
let models = res?.data ?? [];
|
||||||
|
|
||||||
models = models
|
models = models
|
||||||
.filter((models) => models)
|
.filter((models) => models)
|
||||||
// Sort the models
|
// Sort the models
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
// Check if models have position property
|
|
||||||
const aHasPosition = a.info?.meta?.position !== undefined;
|
|
||||||
const bHasPosition = b.info?.meta?.position !== undefined;
|
|
||||||
|
|
||||||
// If both a and b have the position property
|
|
||||||
if (aHasPosition && bHasPosition) {
|
|
||||||
return a.info.meta.position - b.info.meta.position;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If only a has the position property, it should come first
|
|
||||||
if (aHasPosition) return -1;
|
|
||||||
|
|
||||||
// If only b has the position property, it should come first
|
|
||||||
if (bHasPosition) return 1;
|
|
||||||
|
|
||||||
// Compare case-insensitively by name for models without position property
|
// Compare case-insensitively by name for models without position property
|
||||||
const lowerA = a.name.toLowerCase();
|
const lowerA = a.name.toLowerCase();
|
||||||
const lowerB = b.name.toLowerCase();
|
const lowerB = b.name.toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
export const createNewKnowledge = async (token: string, name: string, description: string) => {
|
export const createNewKnowledge = async (token: string, name: string, description: string, accessControl: null|object) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/create`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/create`, {
|
||||||
|
|
@ -12,7 +12,8 @@ export const createNewKnowledge = async (token: string, name: string, descriptio
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: name,
|
name: name,
|
||||||
description: description
|
description: description,
|
||||||
|
access_control: accessControl
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|
@ -32,7 +33,7 @@ export const createNewKnowledge = async (token: string, name: string, descriptio
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getKnowledgeItems = async (token: string = '') => {
|
export const getKnowledgeBases = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/`, {
|
||||||
|
|
@ -63,6 +64,37 @@ export const getKnowledgeItems = async (token: string = '') => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getKnowledgeBaseList = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/list`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getKnowledgeById = async (token: string, id: string) => {
|
export const getKnowledgeById = async (token: string, id: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
@ -99,6 +131,7 @@ type KnowledgeUpdateForm = {
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
data?: object;
|
data?: object;
|
||||||
|
access_control?: null|object;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
|
export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
|
||||||
|
|
@ -114,7 +147,8 @@ export const updateKnowledgeById = async (token: string, id: string, form: Knowl
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: form?.name ? form.name : undefined,
|
name: form?.name ? form.name : undefined,
|
||||||
description: form?.description ? form.description : undefined,
|
description: form?.description ? form.description : undefined,
|
||||||
data: form?.data ? form.data : undefined
|
data: form?.data ? form.data : undefined,
|
||||||
|
access_control: form.access_control
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,7 @@
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
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`, {
|
export const getModels = async (token: string = '') => {
|
||||||
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 = '') => {
|
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/models`, {
|
||||||
|
|
@ -60,13 +32,79 @@ export const getModelInfos = async (token: string = '') => {
|
||||||
return res;
|
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) => {
|
export const getModelById = async (token: string, id: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
searchParams.append('id', id);
|
searchParams.append('id', id);
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models?${searchParams.toString()}`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
@ -95,13 +133,50 @@ export const getModelById = async (token: string, id: string) => {
|
||||||
return res;
|
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) => {
|
export const updateModelById = async (token: string, id: string, model: object) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
searchParams.append('id', id);
|
searchParams.append('id', id);
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models/update?${searchParams.toString()}`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
@ -137,7 +212,7 @@ export const deleteModelById = async (token: string, id: string) => {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
searchParams.append('id', id);
|
searchParams.append('id', id);
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models/delete?${searchParams.toString()}`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}/delete`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -211,10 +211,12 @@ export const getOllamaVersion = async (token: string, urlIdx?: number) => {
|
||||||
return res?.version ?? false;
|
return res?.version ?? false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOllamaModels = async (token: string = '') => {
|
export const getOllamaModels = async (token: string = '', urlIdx: null|number = null) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags${
|
||||||
|
urlIdx !== null ? `/${urlIdx}` : ''
|
||||||
|
}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
|
||||||
|
type PromptItem = {
|
||||||
|
command: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
access_control: null|object;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const createNewPrompt = async (
|
export const createNewPrompt = async (
|
||||||
token: string,
|
token: string,
|
||||||
command: string,
|
prompt: PromptItem
|
||||||
title: string,
|
|
||||||
content: string
|
|
||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
@ -16,9 +24,8 @@ export const createNewPrompt = async (
|
||||||
authorization: `Bearer ${token}`
|
authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
command: `/${command}`,
|
...prompt,
|
||||||
title: title,
|
command: `/${prompt.command}`,
|
||||||
content: content
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|
@ -69,6 +76,39 @@ export const getPrompts = async (token: string = '') => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getPromptList = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/list`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export const getPromptByCommand = async (token: string, command: string) => {
|
export const getPromptByCommand = async (token: string, command: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
@ -101,15 +141,15 @@ export const getPromptByCommand = async (token: string, command: string) => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const updatePromptByCommand = async (
|
export const updatePromptByCommand = async (
|
||||||
token: string,
|
token: string,
|
||||||
command: string,
|
prompt: PromptItem
|
||||||
title: string,
|
|
||||||
content: string
|
|
||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/update`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${prompt.command}/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
@ -117,9 +157,8 @@ export const updatePromptByCommand = async (
|
||||||
authorization: `Bearer ${token}`
|
authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
command: `/${command}`,
|
...prompt,
|
||||||
title: title,
|
command: `/${prompt.command}`,
|
||||||
content: content
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,39 @@ export const getTools = async (token: string = '') => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getToolList = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/list`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export const exportTools = async (token: string = '') => {
|
export const exportTools = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
import { getUserPosition } from '$lib/utils';
|
import { getUserPosition } from '$lib/utils';
|
||||||
|
|
||||||
export const getUserPermissions = async (token: string) => {
|
|
||||||
|
export const getUserGroups = async (token: string) => {
|
||||||
let error = null;
|
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',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -28,10 +29,39 @@ export const getUserPermissions = async (token: string) => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateUserPermissions = async (token: string, permissions: object) => {
|
|
||||||
|
|
||||||
|
export const getUserDefaultPermissions = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions/user`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/users/default/permissions`, {
|
||||||
|
method: 'GET',
|
||||||
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { WEBUI_NAME, config, functions, models } from '$lib/stores';
|
import { WEBUI_NAME, config, functions, models } from '$lib/stores';
|
||||||
import { onMount, getContext, tick } from 'svelte';
|
import { onMount, getContext, tick } from 'svelte';
|
||||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,13 +24,14 @@
|
||||||
import FunctionMenu from './Functions/FunctionMenu.svelte';
|
import FunctionMenu from './Functions/FunctionMenu.svelte';
|
||||||
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
||||||
import Switch from '../common/Switch.svelte';
|
import Switch from '../common/Switch.svelte';
|
||||||
import ValvesModal from './common/ValvesModal.svelte';
|
import ValvesModal from '../workspace/common/ValvesModal.svelte';
|
||||||
import ManifestModal from './common/ManifestModal.svelte';
|
import ManifestModal from '../workspace/common/ManifestModal.svelte';
|
||||||
import Heart from '../icons/Heart.svelte';
|
import Heart from '../icons/Heart.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||||
import Search from '../icons/Search.svelte';
|
import Search from '../icons/Search.svelte';
|
||||||
import Plus from '../icons/Plus.svelte';
|
import Plus from '../icons/Plus.svelte';
|
||||||
|
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
id: `${_function.id}_clone`,
|
id: `${_function.id}_clone`,
|
||||||
name: `${_function.name} (Clone)`
|
name: `${_function.name} (Clone)`
|
||||||
});
|
});
|
||||||
goto('/workspace/functions/create');
|
goto('/admin/functions/create');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -210,7 +210,7 @@
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
|
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
|
||||||
href="/workspace/functions/create"
|
href="/admin/functions/create"
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
<Plus className="size-3.5" />
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -225,7 +225,7 @@
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
||||||
href={`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`}
|
href={`/admin/functions/edit?id=${encodeURIComponent(func.id)}`}
|
||||||
>
|
>
|
||||||
<div class="flex items-center text-left">
|
<div class="flex items-center text-left">
|
||||||
<div class=" flex-1 self-center pl-1">
|
<div class=" flex-1 self-center pl-1">
|
||||||
|
|
@ -322,7 +322,7 @@
|
||||||
<FunctionMenu
|
<FunctionMenu
|
||||||
{func}
|
{func}
|
||||||
editHandler={() => {
|
editHandler={() => {
|
||||||
goto(`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`);
|
goto(`/admin/functions/edit?id=${encodeURIComponent(func.id)}`);
|
||||||
}}
|
}}
|
||||||
shareHandler={() => {
|
shareHandler={() => {
|
||||||
shareHandler(func);
|
shareHandler(func);
|
||||||
|
|
@ -452,40 +452,27 @@
|
||||||
|
|
||||||
{#if $config?.features.enable_community_sharing}
|
{#if $config?.features.enable_community_sharing}
|
||||||
<div class=" my-16">
|
<div class=" my-16">
|
||||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
<div class=" text-lg font-semibold mb-0.5 line-clamp-1">
|
||||||
{$i18n.t('Made by OpenWebUI Community')}
|
{$i18n.t('Made by OpenWebUI Community')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
class=" flex cursor-pointer items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-850 w-full mb-2 px-3.5 py-1.5 rounded-xl transition"
|
||||||
href="https://openwebui.com/#open-webui-community"
|
href="https://openwebui.com/#open-webui-community"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div class=" self-center w-10 flex-shrink-0">
|
|
||||||
<div
|
|
||||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-6"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" self-center">
|
<div class=" self-center">
|
||||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
|
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
|
||||||
<div class=" text-sm line-clamp-1">
|
<div class=" text-sm line-clamp-1">
|
||||||
{$i18n.t('Discover, download, and explore custom functions')}
|
{$i18n.t('Discover, download, and explore custom functions')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<ChevronRight />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -305,7 +305,7 @@ class Pipe:
|
||||||
<button
|
<button
|
||||||
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
goto('/workspace/functions');
|
goto('/admin/functions');
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
@ -327,7 +327,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll">
|
<div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll pr-1 scrollbar-hidden">
|
||||||
{#if selectedTab === 'general'}
|
{#if selectedTab === 'general'}
|
||||||
<General
|
<General
|
||||||
saveHandler={async () => {
|
saveHandler={async () => {
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,7 @@
|
||||||
<OllamaConnection
|
<OllamaConnection
|
||||||
bind:url
|
bind:url
|
||||||
bind:config={OLLAMA_API_CONFIGS[url]}
|
bind:config={OLLAMA_API_CONFIGS[url]}
|
||||||
|
{idx}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
updateOllamaHandler();
|
updateOllamaHandler();
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -4,15 +4,20 @@
|
||||||
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
|
||||||
import AddConnectionModal from './AddConnectionModal.svelte';
|
import AddConnectionModal from './AddConnectionModal.svelte';
|
||||||
|
|
||||||
|
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||||
|
import Wrench from '$lib/components/icons/Wrench.svelte';
|
||||||
|
import ManageOllamaModal from './ManageOllamaModal.svelte';
|
||||||
|
|
||||||
export let onDelete = () => {};
|
export let onDelete = () => {};
|
||||||
export let onSubmit = () => {};
|
export let onSubmit = () => {};
|
||||||
|
|
||||||
export let url = '';
|
export let url = '';
|
||||||
|
export let idx = 0;
|
||||||
export let config = {};
|
export let config = {};
|
||||||
|
|
||||||
|
let showManageModal = false;
|
||||||
let showConfigModal = false;
|
let showConfigModal = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -33,6 +38,8 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ManageOllamaModal bind:show={showManageModal} urlIdx={idx} />
|
||||||
|
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="w-full relative"
|
className="w-full relative"
|
||||||
|
|
@ -55,6 +62,18 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
|
<Tooltip content={$i18n.t('Manage')} className="self-start">
|
||||||
|
<button
|
||||||
|
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
|
||||||
|
on:click={() => {
|
||||||
|
showManageModal = true;
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Wrench />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Configure')} className="self-start">
|
<Tooltip content={$i18n.t('Configure')} className="self-start">
|
||||||
<button
|
<button
|
||||||
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
|
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
} from '$lib/apis/retrieval';
|
} from '$lib/apis/retrieval';
|
||||||
|
|
||||||
import { knowledge, models } from '$lib/stores';
|
import { knowledge, models } from '$lib/stores';
|
||||||
import { getKnowledgeItems } from '$lib/apis/knowledge';
|
import { getKnowledgeBases } from '$lib/apis/knowledge';
|
||||||
import { uploadDir, deleteAllFiles, deleteFileById } from '$lib/apis/files';
|
import { uploadDir, deleteAllFiles, deleteFileById } from '$lib/apis/files';
|
||||||
|
|
||||||
import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
|
|
@ -312,7 +312,7 @@
|
||||||
{#if embeddingEngine === 'openai'}
|
{#if embeddingEngine === 'openai'}
|
||||||
<div class="my-0.5 flex gap-2">
|
<div class="my-0.5 flex gap-2">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={OpenAIUrl}
|
bind:value={OpenAIUrl}
|
||||||
required
|
required
|
||||||
|
|
@ -376,19 +376,12 @@
|
||||||
{#if embeddingEngine === 'ollama'}
|
{#if embeddingEngine === 'ollama'}
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<select
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={embeddingModel}
|
bind:value={embeddingModel}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Set embedding model')}
|
||||||
required
|
required
|
||||||
>
|
/>
|
||||||
{#if !embeddingModel}
|
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
|
||||||
{/if}
|
|
||||||
{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
|
|
||||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
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>
|
<script>
|
||||||
import { getContext, tick, onMount } from 'svelte';
|
import { getContext, tick, onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { user } from '$lib/stores';
|
||||||
|
|
||||||
|
import { getUsers } from '$lib/apis/users';
|
||||||
|
|
||||||
import UserList from './Users/UserList.svelte';
|
import UserList from './Users/UserList.svelte';
|
||||||
import Groups from './Users/Groups.svelte';
|
import Groups from './Users/Groups.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let 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');
|
const containerElement = document.getElementById('users-tabs-container');
|
||||||
|
|
||||||
if (containerElement) {
|
if (containerElement) {
|
||||||
|
|
@ -25,7 +49,7 @@
|
||||||
<div class="flex flex-col lg:flex-row w-full h-full -mt-0.5 pb-2 lg:space-x-4">
|
<div class="flex flex-col lg:flex-row w-full h-full -mt-0.5 pb-2 lg:space-x-4">
|
||||||
<div
|
<div
|
||||||
id="users-tabs-container"
|
id="users-tabs-container"
|
||||||
class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
|
class=" flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===
|
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===
|
||||||
|
|
@ -78,9 +102,9 @@
|
||||||
|
|
||||||
<div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll">
|
<div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll">
|
||||||
{#if selectedTab === 'overview'}
|
{#if selectedTab === 'overview'}
|
||||||
<UserList />
|
<UserList {users} />
|
||||||
{:else if selectedTab === 'groups'}
|
{:else if selectedTab === 'groups'}
|
||||||
<Groups />
|
<Groups {users} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,30 @@
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { WEBUI_NAME, config, user, showSidebar } from '$lib/stores';
|
import { WEBUI_NAME, config, user, showSidebar, knowledge } from '$lib/stores';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
|
import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
|
||||||
|
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||||
|
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||||
|
import User from '$lib/components/icons/User.svelte';
|
||||||
|
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||||
|
import GroupModal from './Groups/EditGroupModal.svelte';
|
||||||
|
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||||
|
import GroupItem from './Groups/GroupItem.svelte';
|
||||||
|
import AddGroupModal from './Groups/AddGroupModal.svelte';
|
||||||
|
import { createNewGroup, getGroups } from '$lib/apis/groups';
|
||||||
|
import { getUserDefaultPermissions, updateUserDefaultPermissions } from '$lib/apis/users';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
|
||||||
|
export let users = [];
|
||||||
|
|
||||||
let groups = [];
|
let groups = [];
|
||||||
let filteredGroups;
|
let filteredGroups;
|
||||||
|
|
||||||
|
|
@ -31,20 +45,69 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
let search = '';
|
let search = '';
|
||||||
|
let defaultPermissions = {
|
||||||
|
workspace: {
|
||||||
|
models: false,
|
||||||
|
knowledge: false,
|
||||||
|
prompts: false,
|
||||||
|
tools: false
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
file_upload: true,
|
||||||
|
delete: true,
|
||||||
|
edit: true,
|
||||||
|
temporary: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let showCreateGroupModal = false;
|
let showCreateGroupModal = false;
|
||||||
|
let showDefaultPermissionsModal = false;
|
||||||
|
|
||||||
|
const setGroups = async () => {
|
||||||
|
groups = await getGroups(localStorage.token);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addGroupHandler = async (group) => {
|
||||||
|
const res = await createNewGroup(localStorage.token, group).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Group created successfully'));
|
||||||
|
groups = await getGroups(localStorage.token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDefaultPermissionsHandler = async (group) => {
|
||||||
|
console.log(group.permissions);
|
||||||
|
|
||||||
|
const res = await updateUserDefaultPermissions(localStorage.token, group.permissions).catch(
|
||||||
|
(error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Default permissions updated successfully'));
|
||||||
|
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user?.role !== 'admin') {
|
if ($user?.role !== 'admin') {
|
||||||
await goto('/');
|
await goto('/');
|
||||||
} else {
|
} else {
|
||||||
groups = [];
|
await setGroups();
|
||||||
|
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
|
||||||
}
|
}
|
||||||
loaded = true;
|
loaded = true;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
|
<AddGroupModal bind:show={showCreateGroupModal} onSubmit={addGroupHandler} />
|
||||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||||
{$i18n.t('Groups')}
|
{$i18n.t('Groups')}
|
||||||
|
|
@ -117,7 +180,58 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div></div>
|
<div>
|
||||||
|
<div class=" flex items-center gap-3 justify-between text-xs uppercase px-1 font-bold">
|
||||||
|
<div class="w-full">Group</div>
|
||||||
|
|
||||||
|
<div class="w-full">Users</div>
|
||||||
|
|
||||||
|
<div class="w-full"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="mt-1.5 border-gray-50 dark:border-gray-850" />
|
||||||
|
|
||||||
|
{#each filteredGroups as group}
|
||||||
|
<div class="my-2">
|
||||||
|
<GroupItem {group} {users} {setGroups} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<hr class="mb-2 border-gray-50 dark:border-gray-850" />
|
||||||
|
|
||||||
|
<GroupModal
|
||||||
|
bind:show={showDefaultPermissionsModal}
|
||||||
|
tabs={['permissions']}
|
||||||
|
bind:permissions={defaultPermissions}
|
||||||
|
custom={false}
|
||||||
|
onSubmit={updateDefaultPermissionsHandler}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-between rounded-lg w-full transition pt-1"
|
||||||
|
on:click={() => {
|
||||||
|
showDefaultPermissionsModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<div class="p-1.5 bg-black/5 dark:bg-white/10 rounded-full">
|
||||||
|
<UsersSolid className="size-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-left">
|
||||||
|
<div class=" text-sm font-medium">{$i18n.t('Default permissions')}</div>
|
||||||
|
|
||||||
|
<div class="flex text-xs mt-0.5">
|
||||||
|
{$i18n.t('applies to all users with the "user" role')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ChevronRight strokeWidth="2.5" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
149
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');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let loaded = false;
|
export let users = [];
|
||||||
let tab = '';
|
|
||||||
let users = [];
|
|
||||||
|
|
||||||
let search = '';
|
let search = '';
|
||||||
let selectedUser = null;
|
let selectedUser = null;
|
||||||
|
|
@ -65,14 +63,6 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if ($user?.role !== 'admin') {
|
|
||||||
await goto('/');
|
|
||||||
} else {
|
|
||||||
users = await getUsers(localStorage.token);
|
|
||||||
}
|
|
||||||
loaded = true;
|
|
||||||
});
|
|
||||||
let sortKey = 'created_at'; // default sort key
|
let sortKey = 'created_at'; // default sort key
|
||||||
let sortOrder = 'asc'; // default sort order
|
let sortOrder = 'asc'; // default sort order
|
||||||
|
|
||||||
|
|
@ -131,278 +121,301 @@
|
||||||
/>
|
/>
|
||||||
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
|
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
|
||||||
|
|
||||||
{#if loaded}
|
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
{$i18n.t('Users')}
|
||||||
{$i18n.t('Users')}
|
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||||
<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>
|
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<div class=" flex w-full space-x-2">
|
<div class=" flex w-full space-x-2">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div class=" self-center ml-1 mr-3">
|
<div class=" self-center ml-1 mr-3">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
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"
|
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"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</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>
|
||||||
|
<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>
|
<div>
|
||||||
<Tooltip content={$i18n.t('Add User')}>
|
<Tooltip content={$i18n.t('Add User')}>
|
||||||
<button
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
showAddUserModal = !showAddUserModal;
|
showAddUserModal = !showAddUserModal;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
<Plus className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
|
||||||
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
|
<thead
|
||||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
|
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
|
||||||
>
|
>
|
||||||
<thead
|
<tr class="">
|
||||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
|
<th
|
||||||
>
|
scope="col"
|
||||||
<tr class="">
|
class="px-3 py-1.5 cursor-pointer select-none"
|
||||||
<th
|
on:click={() => setSortKey('role')}
|
||||||
scope="col"
|
>
|
||||||
class="px-3 py-1.5 cursor-pointer select-none"
|
<div class="flex gap-1.5 items-center">
|
||||||
on:click={() => setSortKey('role')}
|
{$i18n.t('Role')}
|
||||||
>
|
|
||||||
<div class="flex gap-1.5 items-center">
|
|
||||||
{$i18n.t('Role')}
|
|
||||||
|
|
||||||
{#if sortKey === 'role'}
|
{#if sortKey === 'role'}
|
||||||
<span class="font-normal"
|
<span class="font-normal"
|
||||||
>{#if sortOrder === 'asc'}
|
>{#if sortOrder === 'asc'}
|
||||||
<ChevronUp className="size-2" />
|
|
||||||
{:else}
|
|
||||||
<ChevronDown className="size-2" />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span class="invisible">
|
|
||||||
<ChevronUp className="size-2" />
|
<ChevronUp className="size-2" />
|
||||||
</span>
|
{:else}
|
||||||
{/if}
|
<ChevronDown className="size-2" />
|
||||||
</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>
|
|
||||||
{/if}
|
{/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
|
<button
|
||||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
showEditUserModal = !showEditUserModal;
|
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;
|
selectedUser = user;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -417,49 +430,22 @@
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
|
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if user.role !== 'admin'}
|
<div class=" text-gray-500 text-xs mt-1.5 text-right">
|
||||||
<Tooltip content={$i18n.t('Delete User')}>
|
ⓘ {$i18n.t("Click on the user role button to change a user's role.")}
|
||||||
<button
|
</div>
|
||||||
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">
|
<Pagination bind:page count={users.length} />
|
||||||
ⓘ {$i18n.t("Click on the user role button to change a user's role.")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination bind:page count={users.length} />
|
|
||||||
{/if}
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@
|
||||||
mobile,
|
mobile,
|
||||||
showOverview,
|
showOverview,
|
||||||
chatTitle,
|
chatTitle,
|
||||||
showArtifacts
|
showArtifacts,
|
||||||
|
tools
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
convertMessagesToHistory,
|
convertMessagesToHistory,
|
||||||
|
|
@ -78,6 +79,7 @@
|
||||||
import ChatControls from './ChatControls.svelte';
|
import ChatControls from './ChatControls.svelte';
|
||||||
import EventConfirmDialog from '../common/ConfirmDialog.svelte';
|
import EventConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||||
import Placeholder from './Placeholder.svelte';
|
import Placeholder from './Placeholder.svelte';
|
||||||
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
|
||||||
export let chatIdProp = '';
|
export let chatIdProp = '';
|
||||||
|
|
||||||
|
|
@ -153,6 +155,26 @@
|
||||||
console.log('saveSessionSelectedModels', selectedModels, sessionStorage.selectedModels);
|
console.log('saveSessionSelectedModels', selectedModels, sessionStorage.selectedModels);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$: if (selectedModels) {
|
||||||
|
setToolIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
const setToolIds = async () => {
|
||||||
|
if (!$tools) {
|
||||||
|
tools.set(await getTools(localStorage.token));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedModels.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const model = $models.find((m) => m.id === selectedModels[0]);
|
||||||
|
if (model) {
|
||||||
|
selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) =>
|
||||||
|
$tools.find((t) => t.id === id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const showMessage = async (message) => {
|
const showMessage = async (message) => {
|
||||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||||
let _messageId = JSON.parse(JSON.stringify(message.id));
|
let _messageId = JSON.parse(JSON.stringify(message.id));
|
||||||
|
|
@ -480,8 +502,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(selectedModels);
|
|
||||||
|
|
||||||
await showControls.set(false);
|
await showControls.set(false);
|
||||||
await showCallOverlay.set(false);
|
await showCallOverlay.set(false);
|
||||||
await showOverview.set(false);
|
await showOverview.set(false);
|
||||||
|
|
@ -815,9 +835,12 @@
|
||||||
console.log('submitPrompt', userPrompt, $chatId);
|
console.log('submitPrompt', userPrompt, $chatId);
|
||||||
|
|
||||||
const messages = createMessagesList(history.currentId);
|
const messages = createMessagesList(history.currentId);
|
||||||
selectedModels = selectedModels.map((modelId) =>
|
const _selectedModels = selectedModels.map((modelId) =>
|
||||||
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
||||||
);
|
);
|
||||||
|
if (JSON.stringify(selectedModels) !== JSON.stringify(_selectedModels)) {
|
||||||
|
selectedModels = _selectedModels;
|
||||||
|
}
|
||||||
|
|
||||||
if (userPrompt === '') {
|
if (userPrompt === '') {
|
||||||
toast.error($i18n.t('Please enter a prompt'));
|
toast.error($i18n.t('Please enter a prompt'));
|
||||||
|
|
@ -2267,13 +2290,6 @@
|
||||||
bind:selectedToolIds
|
bind:selectedToolIds
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
bind:atSelectedModel
|
bind:atSelectedModel
|
||||||
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
|
|
||||||
const model = $models.find((m) => m.id === e);
|
|
||||||
if (model?.info?.meta?.toolIds ?? false) {
|
|
||||||
return [...new Set([...a, ...model.info.meta.toolIds])];
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}, [])}
|
|
||||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||||
{stopResponse}
|
{stopResponse}
|
||||||
{createMessagePair}
|
{createMessagePair}
|
||||||
|
|
@ -2311,13 +2327,6 @@
|
||||||
bind:selectedToolIds
|
bind:selectedToolIds
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
bind:atSelectedModel
|
bind:atSelectedModel
|
||||||
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
|
|
||||||
const model = $models.find((m) => m.id === e);
|
|
||||||
if (model?.info?.meta?.toolIds ?? false) {
|
|
||||||
return [...new Set([...a, ...model.info.meta.toolIds])];
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}, [])}
|
|
||||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||||
{stopResponse}
|
{stopResponse}
|
||||||
{createMessagePair}
|
{createMessagePair}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import { removeLastWordFromString } from '$lib/utils';
|
import { removeLastWordFromString } from '$lib/utils';
|
||||||
import { getPrompts } from '$lib/apis/prompts';
|
import { getPrompts } from '$lib/apis/prompts';
|
||||||
import { getKnowledgeItems } from '$lib/apis/knowledge';
|
import { getKnowledgeBases } from '$lib/apis/knowledge';
|
||||||
|
|
||||||
import Prompts from './Commands/Prompts.svelte';
|
import Prompts from './Commands/Prompts.svelte';
|
||||||
import Knowledge from './Commands/Knowledge.svelte';
|
import Knowledge from './Commands/Knowledge.svelte';
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
prompts.set(await getPrompts(localStorage.token));
|
prompts.set(await getPrompts(localStorage.token));
|
||||||
})(),
|
})(),
|
||||||
(async () => {
|
(async () => {
|
||||||
knowledge.set(await getKnowledgeItems(localStorage.token));
|
knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||||
})()
|
})()
|
||||||
]);
|
]);
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu } from 'bits-ui';
|
import { DropdownMenu } from 'bits-ui';
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount, tick } from 'svelte';
|
||||||
|
|
||||||
import { config, user, tools as _tools } from '$lib/stores';
|
import { config, user, tools as _tools } from '$lib/stores';
|
||||||
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
|
||||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
@ -11,17 +12,13 @@
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
||||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||||
import { getTools } from '$lib/apis/tools';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let uploadFilesHandler: Function;
|
export let uploadFilesHandler: Function;
|
||||||
|
|
||||||
export let availableToolIds: string[] = [];
|
|
||||||
export let selectedToolIds: string[] = [];
|
export let selectedToolIds: string[] = [];
|
||||||
|
|
||||||
export let webSearchEnabled: boolean;
|
export let webSearchEnabled: boolean;
|
||||||
|
|
||||||
export let onClose: Function;
|
export let onClose: Function;
|
||||||
|
|
||||||
let tools = {};
|
let tools = {};
|
||||||
|
|
@ -31,24 +28,17 @@
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (tools) {
|
|
||||||
selectedToolIds = Object.keys(tools).filter((toolId) => tools[toolId]?.enabled ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
console.log('init');
|
|
||||||
if ($_tools === null) {
|
if ($_tools === null) {
|
||||||
await _tools.set(await getTools(localStorage.token));
|
await _tools.set(await getTools(localStorage.token));
|
||||||
}
|
}
|
||||||
|
|
||||||
tools = $_tools.reduce((a, tool, i, arr) => {
|
tools = $_tools.reduce((a, tool, i, arr) => {
|
||||||
if (availableToolIds.includes(tool.id) || ($user?.role ?? 'user') === 'admin') {
|
a[tool.id] = {
|
||||||
a[tool.id] = {
|
name: tool.name,
|
||||||
name: tool.name,
|
description: tool.meta.description,
|
||||||
description: tool.meta.description,
|
enabled: selectedToolIds.includes(tool.id)
|
||||||
enabled: selectedToolIds.includes(tool.id)
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
return a;
|
return a;
|
||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
@ -97,7 +87,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex-shrink-0">
|
<div class=" flex-shrink-0">
|
||||||
<Switch state={tools[toolId].enabled} />
|
<Switch
|
||||||
|
state={tools[toolId].enabled}
|
||||||
|
on:change={async (e) => {
|
||||||
|
const state = e.detail;
|
||||||
|
await tick();
|
||||||
|
if (state) {
|
||||||
|
selectedToolIds = [...selectedToolIds, toolId];
|
||||||
|
} else {
|
||||||
|
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -729,7 +729,7 @@
|
||||||
|
|
||||||
{#if message.done}
|
{#if message.done}
|
||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
{#if $user.role === 'user' ? ($config?.permissions?.chat?.editing ?? true) : true}
|
{#if $user.role === 'user' ? ($user?.permissions?.chat?.edit ?? true) : true}
|
||||||
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
|
|
@ -1125,19 +1125,17 @@
|
||||||
showRateComment = false;
|
showRateComment = false;
|
||||||
regenerateResponse(message);
|
regenerateResponse(message);
|
||||||
|
|
||||||
(model?.actions ?? [])
|
(model?.actions ?? []).forEach((action) => {
|
||||||
.filter((action) => action?.__webui__ ?? false)
|
dispatch('action', {
|
||||||
.forEach((action) => {
|
id: action.id,
|
||||||
dispatch('action', {
|
event: {
|
||||||
id: action.id,
|
id: 'regenerate-response',
|
||||||
event: {
|
data: {
|
||||||
id: 'regenerate-response',
|
messageId: message.id
|
||||||
data: {
|
|
||||||
messageId: message.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
model: model
|
model: model
|
||||||
}))}
|
}))}
|
||||||
showTemporaryChatControl={$user.role === 'user'
|
showTemporaryChatControl={$user.role === 'user'
|
||||||
? ($config?.permissions?.chat?.temporary ?? true)
|
? ($user?.permissions?.chat?.temporary ?? true)
|
||||||
: true}
|
: true}
|
||||||
bind:value={selectedModel}
|
bind:value={selectedModel}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,15 @@
|
||||||
let selectedModelIdx = 0;
|
let selectedModelIdx = 0;
|
||||||
|
|
||||||
const fuse = new Fuse(
|
const fuse = new Fuse(
|
||||||
items
|
items.map((item) => {
|
||||||
.filter((item) => !item.model?.info?.meta?.hidden)
|
const _item = {
|
||||||
.map((item) => {
|
...item,
|
||||||
const _item = {
|
modelName: item.model?.name,
|
||||||
...item,
|
tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
|
||||||
modelName: item.model?.name,
|
desc: item.model?.info?.meta?.description
|
||||||
tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
|
};
|
||||||
desc: item.model?.info?.meta?.description
|
return _item;
|
||||||
};
|
}),
|
||||||
return _item;
|
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
keys: ['value', 'tags', 'modelName'],
|
keys: ['value', 'tags', 'modelName'],
|
||||||
threshold: 0.3
|
threshold: 0.3
|
||||||
|
|
@ -76,7 +74,7 @@
|
||||||
? fuse.search(searchValue).map((e) => {
|
? fuse.search(searchValue).map((e) => {
|
||||||
return e.item;
|
return e.item;
|
||||||
})
|
})
|
||||||
: items.filter((item) => !item.model?.info?.meta?.hidden);
|
: items;
|
||||||
|
|
||||||
const pullModelHandler = async () => {
|
const pullModelHandler = async () => {
|
||||||
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
||||||
|
|
@ -583,14 +581,3 @@
|
||||||
</slot>
|
</slot>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
<style>
|
|
||||||
.scrollbar-hidden:active::-webkit-scrollbar-thumb,
|
|
||||||
.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
|
|
||||||
.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
.scrollbar-hidden::-webkit-scrollbar-thumb {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
export let files = [];
|
export let files = [];
|
||||||
export let availableToolIds = [];
|
|
||||||
export let selectedToolIds = [];
|
export let selectedToolIds = [];
|
||||||
export let webSearchEnabled = false;
|
export let webSearchEnabled = false;
|
||||||
|
|
||||||
|
|
@ -200,7 +200,6 @@
|
||||||
bind:selectedToolIds
|
bind:selectedToolIds
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
bind:atSelectedModel
|
bind:atSelectedModel
|
||||||
{availableToolIds}
|
|
||||||
{transparentBackground}
|
{transparentBackground}
|
||||||
{stopResponse}
|
{stopResponse}
|
||||||
{createMessagePair}
|
{createMessagePair}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@
|
||||||
export let state = true;
|
export let state = true;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
$: dispatch('change', state);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Switch.Root
|
<Switch.Root
|
||||||
bind:checked={state}
|
bind:checked={state}
|
||||||
onCheckedChange={async (e) => {
|
|
||||||
await tick();
|
|
||||||
dispatch('change', e);
|
|
||||||
}}
|
|
||||||
class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] mx-[1px] transition {state
|
class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] mx-[1px] transition {state
|
||||||
? ' bg-emerald-600'
|
? ' bg-emerald-600'
|
||||||
: 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800"
|
: 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800"
|
||||||
|
|
|
||||||
19
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $user?.role === 'admin'}
|
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
|
||||||
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
|
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
|
||||||
<a
|
<a
|
||||||
class="flex-grow flex space-x-3 rounded-lg px-2 py-[7px] hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
class="flex-grow flex space-x-3 rounded-lg px-2 py-[7px] hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,22 @@
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import { WEBUI_NAME, knowledge } from '$lib/stores';
|
import { WEBUI_NAME, knowledge } from '$lib/stores';
|
||||||
|
import {
|
||||||
import { getKnowledgeItems, deleteKnowledgeById } from '$lib/apis/knowledge';
|
getKnowledgeBases,
|
||||||
|
deleteKnowledgeById,
|
||||||
import { blobToFile, transformFileName } from '$lib/utils';
|
getKnowledgeBaseList
|
||||||
|
} from '$lib/apis/knowledge';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
|
||||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
|
||||||
import Pencil from '../icons/Pencil.svelte';
|
|
||||||
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||||
import ItemMenu from './Knowledge/ItemMenu.svelte';
|
import ItemMenu from './Knowledge/ItemMenu.svelte';
|
||||||
import Badge from '../common/Badge.svelte';
|
import Badge from '../common/Badge.svelte';
|
||||||
import Search from '../icons/Search.svelte';
|
import Search from '../icons/Search.svelte';
|
||||||
import Plus from '../icons/Plus.svelte';
|
import Plus from '../icons/Plus.svelte';
|
||||||
|
import Spinner from '../common/Spinner.svelte';
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
let query = '';
|
let query = '';
|
||||||
let selectedItem = null;
|
let selectedItem = null;
|
||||||
|
|
@ -31,13 +33,21 @@
|
||||||
|
|
||||||
let fuse = null;
|
let fuse = null;
|
||||||
|
|
||||||
|
let knowledgeBases = [];
|
||||||
let filteredItems = [];
|
let filteredItems = [];
|
||||||
|
|
||||||
|
$: if (knowledgeBases) {
|
||||||
|
fuse = new Fuse(knowledgeBases, {
|
||||||
|
keys: ['name', 'description']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$: if (fuse) {
|
$: if (fuse) {
|
||||||
filteredItems = query
|
filteredItems = query
|
||||||
? fuse.search(query).map((e) => {
|
? fuse.search(query).map((e) => {
|
||||||
return e.item;
|
return e.item;
|
||||||
})
|
})
|
||||||
: $knowledge;
|
: knowledgeBases;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteHandler = async (item) => {
|
const deleteHandler = async (item) => {
|
||||||
|
|
@ -46,19 +56,15 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
knowledge.set(await getKnowledgeItems(localStorage.token));
|
knowledgeBases = await getKnowledgeBaseList(localStorage.token);
|
||||||
|
knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||||
toast.success($i18n.t('Knowledge deleted successfully.'));
|
toast.success($i18n.t('Knowledge deleted successfully.'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
knowledge.set(await getKnowledgeItems(localStorage.token));
|
knowledgeBases = await getKnowledgeBaseList(localStorage.token);
|
||||||
|
loaded = true;
|
||||||
knowledge.subscribe((value) => {
|
|
||||||
fuse = new Fuse(value, {
|
|
||||||
keys: ['name', 'description']
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -68,104 +74,110 @@
|
||||||
</title>
|
</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
{#if loaded}
|
||||||
bind:show={showDeleteConfirm}
|
<DeleteConfirmDialog
|
||||||
on:confirm={() => {
|
bind:show={showDeleteConfirm}
|
||||||
deleteHandler(selectedItem);
|
on:confirm={() => {
|
||||||
}}
|
deleteHandler(selectedItem);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
||||||
{$i18n.t('Knowledge')}
|
{$i18n.t('Knowledge')}
|
||||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
<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"
|
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
|
||||||
>{filteredItems.length}</span
|
>{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>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full space-x-2">
|
<div class="my-3 mb-5 grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
<div class="flex flex-1">
|
{#each filteredItems as item}
|
||||||
<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
|
<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"
|
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"
|
||||||
aria-label={$i18n.t('Create Knowledge')}
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
goto('/workspace/knowledge/create');
|
if (item?.meta?.document) {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t(
|
||||||
|
'Only collections can be edited, create a new knowledge base to edit/add documents.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
goto(`/workspace/knowledge/${item.id}`);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
<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>
|
</button>
|
||||||
</div>
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-3 mb-5 grid md:grid-cols-2 lg:grid-cols-3 gap-2">
|
<div class=" text-gray-500 text-xs mt-1 mb-2">
|
||||||
{#each filteredItems as item}
|
ⓘ {$i18n.t("Use '#' in the prompt input to load and include your knowledge.")}
|
||||||
<button
|
</div>
|
||||||
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"
|
{:else}
|
||||||
on:click={() => {
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
if (item?.meta?.document) {
|
<Spinner />
|
||||||
toast.error(
|
</div>
|
||||||
$i18n.t(
|
{/if}
|
||||||
'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>
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,16 @@
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import { createNewKnowledge, getKnowledgeItems } from '$lib/apis/knowledge';
|
import { createNewKnowledge, getKnowledgeBases } from '$lib/apis/knowledge';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { knowledge } from '$lib/stores';
|
import { knowledge } from '$lib/stores';
|
||||||
|
import AccessControl from '../common/AccessControl.svelte';
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
let name = '';
|
let name = '';
|
||||||
let description = '';
|
let description = '';
|
||||||
|
let accessControl = null;
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
@ -23,13 +25,18 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await createNewKnowledge(localStorage.token, name, description).catch((e) => {
|
const res = await createNewKnowledge(
|
||||||
|
localStorage.token,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
accessControl
|
||||||
|
).catch((e) => {
|
||||||
toast.error(e);
|
toast.error(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t('Knowledge created successfully.'));
|
toast.success($i18n.t('Knowledge created successfully.'));
|
||||||
knowledge.set(await getKnowledgeItems(localStorage.token));
|
knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||||
goto(`/workspace/knowledge/${res.id}`);
|
goto(`/workspace/knowledge/${res.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +110,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
|
||||||
|
<AccessControl bind:accessControl />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end mt-2">
|
<div class="flex justify-end mt-2">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
import {
|
import {
|
||||||
addFileToKnowledgeById,
|
addFileToKnowledgeById,
|
||||||
getKnowledgeById,
|
getKnowledgeById,
|
||||||
getKnowledgeItems,
|
getKnowledgeBases,
|
||||||
removeFileFromKnowledgeById,
|
removeFileFromKnowledgeById,
|
||||||
resetKnowledgeById,
|
resetKnowledgeById,
|
||||||
updateFileFromKnowledgeById,
|
updateFileFromKnowledgeById,
|
||||||
|
|
@ -27,18 +27,19 @@
|
||||||
import { processFile } from '$lib/apis/retrieval';
|
import { processFile } from '$lib/apis/retrieval';
|
||||||
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Files from './Collection/Files.svelte';
|
import Files from './KnowledgeBase/Files.svelte';
|
||||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||||
|
|
||||||
import AddContentMenu from './Collection/AddContentMenu.svelte';
|
import AddContentMenu from './KnowledgeBase/AddContentMenu.svelte';
|
||||||
import AddTextContentModal from './Collection/AddTextContentModal.svelte';
|
import AddTextContentModal from './KnowledgeBase/AddTextContentModal.svelte';
|
||||||
|
|
||||||
import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
|
import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
|
||||||
import RichTextInput from '$lib/components/common/RichTextInput.svelte';
|
import RichTextInput from '$lib/components/common/RichTextInput.svelte';
|
||||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||||
import Drawer from '$lib/components/common/Drawer.svelte';
|
import Drawer from '$lib/components/common/Drawer.svelte';
|
||||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||||
import MenuLines from '$lib/components/icons/MenuLines.svelte';
|
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||||
|
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||||
|
|
||||||
let largeScreen = true;
|
let largeScreen = true;
|
||||||
|
|
||||||
|
|
@ -62,6 +63,7 @@
|
||||||
|
|
||||||
let showAddTextContentModal = false;
|
let showAddTextContentModal = false;
|
||||||
let showSyncConfirmModal = false;
|
let showSyncConfirmModal = false;
|
||||||
|
let showAccessControlModal = false;
|
||||||
|
|
||||||
let inputFiles = null;
|
let inputFiles = null;
|
||||||
|
|
||||||
|
|
@ -420,14 +422,15 @@
|
||||||
|
|
||||||
const res = await updateKnowledgeById(localStorage.token, id, {
|
const res = await updateKnowledgeById(localStorage.token, id, {
|
||||||
name: knowledge.name,
|
name: knowledge.name,
|
||||||
description: knowledge.description
|
description: knowledge.description,
|
||||||
|
access_control: knowledge.access_control
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
toast.error(e);
|
toast.error(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t('Knowledge updated successfully'));
|
toast.success($i18n.t('Knowledge updated successfully'));
|
||||||
_knowledge.set(await getKnowledgeItems(localStorage.token));
|
_knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
@ -596,8 +599,63 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col w-full h-full max-h-[100dvh] mt-1" id="collection-container">
|
<div class="flex flex-col w-full h-full max-h-[100dvh] translate-y-1" id="collection-container">
|
||||||
{#if id && knowledge}
|
{#if id && knowledge}
|
||||||
|
<AccessControlModal
|
||||||
|
bind:show={showAccessControlModal}
|
||||||
|
bind:accessControl={knowledge.access_control}
|
||||||
|
onChange={() => {
|
||||||
|
changeDebounceHandler();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="w-full mb-2.5">
|
||||||
|
<div class=" flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center justify-between w-full px-0.5 mb-1">
|
||||||
|
<div class="w-full">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="text-left w-full font-semibold text-2xl font-primary bg-transparent outline-none"
|
||||||
|
bind:value={knowledge.name}
|
||||||
|
placeholder="Knowledge Name"
|
||||||
|
on:input={() => {
|
||||||
|
changeDebounceHandler();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="self-center">
|
||||||
|
<button
|
||||||
|
class="bg-gray-50 hover:bg-gray-100 text-black transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showAccessControlModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||||
|
|
||||||
|
<div class="text-sm font-medium flex-shrink-0">
|
||||||
|
{$i18n.t('Share')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full px-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="text-left text-xs w-full text-gray-500 bg-transparent outline-none"
|
||||||
|
bind:value={knowledge.description}
|
||||||
|
placeholder="Knowledge Description"
|
||||||
|
on:input={() => {
|
||||||
|
changeDebounceHandler();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
|
<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
|
||||||
<PaneGroup direction="horizontal">
|
<PaneGroup direction="horizontal">
|
||||||
<Pane
|
<Pane
|
||||||
|
|
@ -687,7 +745,17 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
|
<div
|
||||||
|
class="m-auto flex flex-col justify-center text-center text-gray-500 text-xs"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{$i18n.t('No content found')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-12 mt-2 text-center text-gray-200 dark:text-gray-700">
|
||||||
|
{$i18n.t('Drag and drop a file to upload or select a file to view')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -753,41 +821,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="m-auto pb-32">
|
<div></div>
|
||||||
<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>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
|
|
@ -8,12 +8,17 @@
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { onMount, getContext, tick } from 'svelte';
|
import { onMount, getContext, tick } from 'svelte';
|
||||||
|
|
||||||
import { WEBUI_NAME, config, mobile, models, settings, user } from '$lib/stores';
|
|
||||||
import { addNewModel, deleteModelById, getModelInfos, updateModelById } from '$lib/apis/models';
|
|
||||||
|
|
||||||
import { deleteModel } from '$lib/apis/ollama';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { WEBUI_NAME, config, mobile, models as _models, settings, user } from '$lib/stores';
|
||||||
|
import {
|
||||||
|
createNewModel,
|
||||||
|
deleteModelById,
|
||||||
|
getModels as getWorkspaceModels,
|
||||||
|
toggleModelById,
|
||||||
|
updateModelById
|
||||||
|
} from '$lib/apis/models';
|
||||||
|
|
||||||
import { getModels } from '$lib/apis';
|
import { getModels } from '$lib/apis';
|
||||||
|
|
||||||
|
|
@ -24,67 +29,52 @@
|
||||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||||
import Search from '../icons/Search.svelte';
|
import Search from '../icons/Search.svelte';
|
||||||
import Plus from '../icons/Plus.svelte';
|
import Plus from '../icons/Plus.svelte';
|
||||||
|
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||||
const i18n = getContext('i18n');
|
import Switch from '../common/Switch.svelte';
|
||||||
|
import Spinner from '../common/Spinner.svelte';
|
||||||
|
|
||||||
let shiftKey = false;
|
let shiftKey = false;
|
||||||
|
|
||||||
let showModelDeleteConfirm = false;
|
|
||||||
|
|
||||||
let localModelfiles = [];
|
|
||||||
|
|
||||||
let importFiles;
|
let importFiles;
|
||||||
let modelsImportInputElement: HTMLInputElement;
|
let modelsImportInputElement: HTMLInputElement;
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
let _models = [];
|
let models = [];
|
||||||
|
|
||||||
let filteredModels = [];
|
let filteredModels = [];
|
||||||
let selectedModel = null;
|
let selectedModel = null;
|
||||||
|
|
||||||
$: if (_models) {
|
let showModelDeleteConfirm = false;
|
||||||
filteredModels = _models
|
|
||||||
.filter((m) => m?.owned_by !== 'arena')
|
$: if (models) {
|
||||||
.filter(
|
filteredModels = models.filter(
|
||||||
(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
|
(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let sortable = null;
|
|
||||||
let searchValue = '';
|
let searchValue = '';
|
||||||
|
|
||||||
const deleteModelHandler = async (model) => {
|
const deleteModelHandler = async (model) => {
|
||||||
console.log(model.info);
|
const res = await deleteModelById(localStorage.token, model.id).catch((e) => {
|
||||||
if (!model?.info) {
|
toast.error(e);
|
||||||
toast.error(
|
|
||||||
$i18n.t('{{ owner }}: You cannot delete a base model', {
|
|
||||||
owner: model.owned_by.toUpperCase()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
|
|
||||||
const res = await deleteModelById(localStorage.token, model.id);
|
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
|
toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
await models.set(await getModels(localStorage.token));
|
await _models.set(await getModels(localStorage.token));
|
||||||
_models = $models;
|
models = await getWorkspaceModels(localStorage.token);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cloneModelHandler = async (model) => {
|
const cloneModelHandler = async (model) => {
|
||||||
if ((model?.info?.base_model_id ?? null) === null) {
|
sessionStorage.model = JSON.stringify({
|
||||||
toast.error($i18n.t('You cannot clone a base model'));
|
...model,
|
||||||
return;
|
id: `${model.id}-clone`,
|
||||||
} else {
|
name: `${model.name} (Clone)`
|
||||||
sessionStorage.model = JSON.stringify({
|
});
|
||||||
...model,
|
goto('/workspace/models/create');
|
||||||
id: `${model.id}-clone`,
|
|
||||||
name: `${model.name} (Clone)`
|
|
||||||
});
|
|
||||||
goto('/workspace/models/create');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const shareModelHandler = async (model) => {
|
const shareModelHandler = async (model) => {
|
||||||
|
|
@ -108,58 +98,6 @@
|
||||||
window.addEventListener('message', messageHandler, false);
|
window.addEventListener('message', messageHandler, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveToTopHandler = async (model) => {
|
|
||||||
// find models with position 0 and set them to 1
|
|
||||||
const topModels = _models.filter((m) => m.info?.meta?.position === 0);
|
|
||||||
for (const m of topModels) {
|
|
||||||
let info = m.info;
|
|
||||||
if (!info) {
|
|
||||||
info = {
|
|
||||||
id: m.id,
|
|
||||||
name: m.name,
|
|
||||||
meta: {
|
|
||||||
position: 1
|
|
||||||
},
|
|
||||||
params: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
info.meta = {
|
|
||||||
...info.meta,
|
|
||||||
position: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateModelById(localStorage.token, info.id, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = model.info;
|
|
||||||
|
|
||||||
if (!info) {
|
|
||||||
info = {
|
|
||||||
id: model.id,
|
|
||||||
name: model.name,
|
|
||||||
meta: {
|
|
||||||
position: 0
|
|
||||||
},
|
|
||||||
params: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
info.meta = {
|
|
||||||
...info.meta,
|
|
||||||
position: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await updateModelById(localStorage.token, info.id, info);
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
toast.success($i18n.t(`Model {{name}} is now at the top`, { name: info.id }));
|
|
||||||
}
|
|
||||||
|
|
||||||
await models.set(await getModels(localStorage.token));
|
|
||||||
_models = $models;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideModelHandler = async (model) => {
|
const hideModelHandler = async (model) => {
|
||||||
let info = model.info;
|
let info = model.info;
|
||||||
|
|
||||||
|
|
@ -192,8 +130,8 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await models.set(await getModels(localStorage.token));
|
await _models.set(await getModels(localStorage.token));
|
||||||
_models = $models;
|
models = await getWorkspaceModels(localStorage.token);
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadModels = async (models) => {
|
const downloadModels = async (models) => {
|
||||||
|
|
@ -210,60 +148,10 @@
|
||||||
saveAs(blob, `${model.id}-${Date.now()}.json`);
|
saveAs(blob, `${model.id}-${Date.now()}.json`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const positionChangeHandler = async () => {
|
|
||||||
// Get the new order of the models
|
|
||||||
const modelIds = Array.from(document.getElementById('model-list').children).map((child) =>
|
|
||||||
child.id.replace('model-item-', '')
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the position of the models
|
|
||||||
for (const [index, id] of modelIds.entries()) {
|
|
||||||
const model = $models.find((m) => m.id === id);
|
|
||||||
if (model) {
|
|
||||||
let info = model.info;
|
|
||||||
|
|
||||||
if (!info) {
|
|
||||||
info = {
|
|
||||||
id: model.id,
|
|
||||||
name: model.name,
|
|
||||||
meta: {
|
|
||||||
position: index
|
|
||||||
},
|
|
||||||
params: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
info.meta = {
|
|
||||||
...info.meta,
|
|
||||||
position: index
|
|
||||||
};
|
|
||||||
await updateModelById(localStorage.token, info.id, info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
await models.set(await getModels(localStorage.token));
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Legacy code to sync localModelfiles with models
|
models = await getWorkspaceModels(localStorage.token);
|
||||||
_models = $models;
|
|
||||||
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
|
|
||||||
|
|
||||||
if (localModelfiles) {
|
loaded = true;
|
||||||
console.log(localModelfiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$mobile) {
|
|
||||||
// SortableJS
|
|
||||||
sortable = new Sortable(document.getElementById('model-list'), {
|
|
||||||
animation: 150,
|
|
||||||
onUpdate: async (event) => {
|
|
||||||
console.log(event);
|
|
||||||
positionChangeHandler();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = (event) => {
|
const onKeyDown = (event) => {
|
||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
|
|
@ -299,356 +187,276 @@
|
||||||
</title>
|
</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<ModelDeleteConfirmDialog
|
{#if loaded}
|
||||||
bind:show={showModelDeleteConfirm}
|
<ModelDeleteConfirmDialog
|
||||||
on:confirm={() => {
|
bind:show={showModelDeleteConfirm}
|
||||||
deleteModelHandler(selectedModel);
|
on:confirm={() => {
|
||||||
}}
|
deleteModelHandler(selectedModel);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center md:self-center text-xl font-medium px-0.5">
|
<div class="flex items-center md:self-center text-xl font-medium px-0.5">
|
||||||
{$i18n.t('Models')}
|
{$i18n.t('Models')}
|
||||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
<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"
|
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
|
||||||
>{filteredModels.length}</span
|
>{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' : ''}"
|
|
||||||
>
|
>
|
||||||
<Tooltip
|
</div>
|
||||||
content={marked.parse(
|
</div>
|
||||||
model?.ollama?.digest
|
|
||||||
? `${model?.ollama?.digest} *(${model?.ollama?.modified_at})*`
|
<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">
|
||||||
className=" w-fit"
|
<Search className="size-3.5" />
|
||||||
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>
|
||||||
</a>
|
<input
|
||||||
<div class="flex flex-row gap-0.5 self-center">
|
class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
|
||||||
{#if shiftKey}
|
bind:value={searchValue}
|
||||||
<Tooltip
|
placeholder={$i18n.t('Search Models')}
|
||||||
content={(model?.info?.meta?.hidden ?? false)
|
/>
|
||||||
? $i18n.t('Show Model')
|
</div>
|
||||||
: $i18n.t('Hide Model')}
|
|
||||||
>
|
<div>
|
||||||
<button
|
<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"
|
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"
|
||||||
type="button"
|
href="/workspace/models/create"
|
||||||
on:click={() => {
|
>
|
||||||
|
<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);
|
hideModelHandler(model);
|
||||||
}}
|
}}
|
||||||
>
|
deleteHandler={() => {
|
||||||
{#if model?.info?.meta?.hidden ?? false}
|
selectedModel = model;
|
||||||
<svg
|
showModelDeleteConfirm = true;
|
||||||
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);
|
|
||||||
}}
|
}}
|
||||||
|
onClose={() => {}}
|
||||||
>
|
>
|
||||||
<GarbageBin />
|
<button
|
||||||
</button>
|
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
</Tooltip>
|
type="button"
|
||||||
{:else}
|
>
|
||||||
<a
|
<EllipsisHorizontal className="size-5" />
|
||||||
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"
|
</button>
|
||||||
type="button"
|
</ModelMenu>
|
||||||
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>
|
|
||||||
|
|
||||||
<ModelMenu
|
<div class="ml-1">
|
||||||
{model}
|
<Tooltip content={model.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
|
||||||
shareHandler={() => {
|
<Switch
|
||||||
shareModelHandler(model);
|
bind:state={model.is_active}
|
||||||
}}
|
on:change={async (e) => {
|
||||||
cloneHandler={() => {
|
toggleModelById(localStorage.token, model.id);
|
||||||
cloneModelHandler(model);
|
_models.set(await getModels(localStorage.token));
|
||||||
}}
|
}}
|
||||||
exportHandler={() => {
|
/>
|
||||||
exportModelHandler(model);
|
</Tooltip>
|
||||||
}}
|
</div>
|
||||||
moveToTopHandler={() => {
|
{/if}
|
||||||
moveToTopHandler(model);
|
</div>
|
||||||
}}
|
|
||||||
hideHandler={() => {
|
|
||||||
hideModelHandler(model);
|
|
||||||
}}
|
|
||||||
deleteHandler={() => {
|
|
||||||
selectedModel = model;
|
|
||||||
showModelDeleteConfirm = true;
|
|
||||||
}}
|
|
||||||
onClose={() => {}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<EllipsisHorizontal className="size-5" />
|
|
||||||
</button>
|
|
||||||
</ModelMenu>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" flex justify-end w-full mb-3">
|
|
||||||
<div class="flex space-x-1">
|
|
||||||
<input
|
|
||||||
id="models-import-input"
|
|
||||||
bind:this={modelsImportInputElement}
|
|
||||||
bind:files={importFiles}
|
|
||||||
type="file"
|
|
||||||
accept=".json"
|
|
||||||
hidden
|
|
||||||
on:change={() => {
|
|
||||||
console.log(importFiles);
|
|
||||||
|
|
||||||
let reader = new FileReader();
|
|
||||||
reader.onload = async (event) => {
|
|
||||||
let savedModels = JSON.parse(event.target.result);
|
|
||||||
console.log(savedModels);
|
|
||||||
|
|
||||||
for (const model of savedModels) {
|
|
||||||
if (model?.info ?? false) {
|
|
||||||
if ($models.find((m) => m.id === model.id)) {
|
|
||||||
await updateModelById(localStorage.token, model.id, model.info).catch((error) => {
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await addNewModel(localStorage.token, model.info).catch((error) => {
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await models.set(await getModels(localStorage.token));
|
|
||||||
_models = $models;
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsText(importFiles[0]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
|
||||||
on:click={() => {
|
|
||||||
modelsImportInputElement.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Models')}</div>
|
|
||||||
|
|
||||||
<div class=" self-center">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
|
||||||
on:click={async () => {
|
|
||||||
downloadModels($models);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Models')}</div>
|
|
||||||
|
|
||||||
<div class=" self-center">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if localModelfiles.length > 0}
|
{#if $user?.role === 'admin'}
|
||||||
<div class="flex">
|
<div class=" flex justify-end w-full mb-3">
|
||||||
<div class=" self-center text-sm font-medium mr-4">
|
|
||||||
{localModelfiles.length} Local Modelfiles Detected
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1">
|
<div class="flex space-x-1">
|
||||||
<button
|
<input
|
||||||
class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
|
id="models-import-input"
|
||||||
on:click={async () => {
|
bind:this={modelsImportInputElement}
|
||||||
downloadModels(localModelfiles);
|
bind:files={importFiles}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
hidden
|
||||||
|
on:change={() => {
|
||||||
|
console.log(importFiles);
|
||||||
|
|
||||||
localStorage.removeItem('modelfiles');
|
let reader = new FileReader();
|
||||||
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
|
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">
|
<div class=" self-center">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
viewBox="0 0 16 16"
|
||||||
viewBox="0 0 24 24"
|
fill="currentColor"
|
||||||
stroke-width="1.5"
|
class="w-3.5 h-3.5"
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
fill-rule="evenodd"
|
||||||
stroke-linejoin="round"
|
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"
|
||||||
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"
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -656,44 +464,35 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $config?.features.enable_community_sharing}
|
{#if $config?.features.enable_community_sharing}
|
||||||
<div class=" my-16">
|
<div class=" my-16">
|
||||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
<div class=" text-lg font-semibold mb-0.5 line-clamp-1">
|
||||||
{$i18n.t('Made by OpenWebUI Community')}
|
{$i18n.t('Made by OpenWebUI Community')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
|
{/if}
|
||||||
<a
|
{:else}
|
||||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
href="https://openwebui.com/#open-webui-community"
|
<Spinner />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
|
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-850 rounded-3xl"
|
||||||
type="button">{$i18n.t('Select Knowledge')}</button
|
type="button">{$i18n.t('Select Knowledge')}</button
|
||||||
>
|
>
|
||||||
</Selector>
|
</Selector>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
|
|
||||||
import { onMount, getContext, tick } from 'svelte';
|
import { onMount, getContext, tick } from 'svelte';
|
||||||
import { models, tools, functions, knowledge as knowledgeCollections } from '$lib/stores';
|
import { models, tools, functions, knowledge as knowledgeCollections, user } from '$lib/stores';
|
||||||
|
|
||||||
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
|
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
|
||||||
import Tags from '$lib/components/common/Tags.svelte';
|
import Tags from '$lib/components/common/Tags.svelte';
|
||||||
|
|
@ -16,14 +12,20 @@
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
import { getTools } from '$lib/apis/tools';
|
import { getTools } from '$lib/apis/tools';
|
||||||
import { getFunctions } from '$lib/apis/functions';
|
import { getFunctions } from '$lib/apis/functions';
|
||||||
import { getKnowledgeItems } from '$lib/apis/knowledge';
|
import { getKnowledgeBases } from '$lib/apis/knowledge';
|
||||||
|
import AccessControl from '../common/AccessControl.svelte';
|
||||||
|
import { stringify } from 'postcss';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let onSubmit: Function;
|
export let onSubmit: Function;
|
||||||
|
export let onBack: null | Function = null;
|
||||||
|
|
||||||
export let model = null;
|
export let model = null;
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
|
|
||||||
|
export let preset = true;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let success = false;
|
let success = false;
|
||||||
|
|
||||||
|
|
@ -77,12 +79,14 @@
|
||||||
let filterIds = [];
|
let filterIds = [];
|
||||||
let actionIds = [];
|
let actionIds = [];
|
||||||
|
|
||||||
|
let accessControl = null;
|
||||||
|
|
||||||
const addUsage = (base_model_id) => {
|
const addUsage = (base_model_id) => {
|
||||||
const baseModel = $models.find((m) => m.id === base_model_id);
|
const baseModel = $models.find((m) => m.id === base_model_id);
|
||||||
|
|
||||||
if (baseModel) {
|
if (baseModel) {
|
||||||
if (baseModel.owned_by === 'openai') {
|
if (baseModel.owned_by === 'openai') {
|
||||||
capabilities.usage = baseModel.info?.meta?.capabilities?.usage ?? false;
|
capabilities.usage = baseModel?.meta?.capabilities?.usage ?? false;
|
||||||
} else {
|
} else {
|
||||||
delete capabilities.usage;
|
delete capabilities.usage;
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +99,8 @@
|
||||||
|
|
||||||
info.id = id;
|
info.id = id;
|
||||||
info.name = name;
|
info.name = name;
|
||||||
|
|
||||||
|
info.access_control = accessControl;
|
||||||
info.meta.capabilities = capabilities;
|
info.meta.capabilities = capabilities;
|
||||||
|
|
||||||
if (knowledge.length > 0) {
|
if (knowledge.length > 0) {
|
||||||
|
|
@ -145,7 +151,7 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await tools.set(await getTools(localStorage.token));
|
await tools.set(await getTools(localStorage.token));
|
||||||
await functions.set(await getFunctions(localStorage.token));
|
await functions.set(await getFunctions(localStorage.token));
|
||||||
await knowledgeCollections.set(await getKnowledgeItems(localStorage.token));
|
await knowledgeCollections.set(await getKnowledgeBases(localStorage.token));
|
||||||
|
|
||||||
// Scroll to top 'workspace-container' element
|
// Scroll to top 'workspace-container' element
|
||||||
const workspaceContainer = document.getElementById('workspace-container');
|
const workspaceContainer = document.getElementById('workspace-container');
|
||||||
|
|
@ -154,38 +160,37 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model) {
|
if (model) {
|
||||||
|
console.log(model);
|
||||||
name = model.name;
|
name = model.name;
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
id = model.id;
|
id = model.id;
|
||||||
|
|
||||||
if (model.info.base_model_id) {
|
if (model.base_model_id) {
|
||||||
const base_model = $models
|
const base_model = $models
|
||||||
.filter((m) => !m?.preset && m?.owned_by !== 'arena')
|
.filter((m) => !m?.preset && !(m?.arena ?? false))
|
||||||
.find((m) =>
|
.find((m) => [model.base_model_id, `${model.base_model_id}:latest`].includes(m.id));
|
||||||
[model.info.base_model_id, `${model.info.base_model_id}:latest`].includes(m.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('base_model', base_model);
|
console.log('base_model', base_model);
|
||||||
|
|
||||||
if (base_model) {
|
if (base_model) {
|
||||||
model.info.base_model_id = base_model.id;
|
model.base_model_id = base_model.id;
|
||||||
} else {
|
} else {
|
||||||
model.info.base_model_id = null;
|
model.base_model_id = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
params = { ...params, ...model?.info?.params };
|
params = { ...params, ...model?.params };
|
||||||
params.stop = params?.stop
|
params.stop = params?.stop
|
||||||
? (typeof params.stop === 'string' ? params.stop.split(',') : (params?.stop ?? [])).join(
|
? (typeof params.stop === 'string' ? params.stop.split(',') : (params?.stop ?? [])).join(
|
||||||
','
|
','
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
toolIds = model?.info?.meta?.toolIds ?? [];
|
toolIds = model?.meta?.toolIds ?? [];
|
||||||
filterIds = model?.info?.meta?.filterIds ?? [];
|
filterIds = model?.meta?.filterIds ?? [];
|
||||||
actionIds = model?.info?.meta?.actionIds ?? [];
|
actionIds = model?.meta?.actionIds ?? [];
|
||||||
knowledge = (model?.info?.meta?.knowledge ?? []).map((item) => {
|
knowledge = (model?.meta?.knowledge ?? []).map((item) => {
|
||||||
if (item?.collection_name) {
|
if (item?.collection_name) {
|
||||||
return {
|
return {
|
||||||
id: item.collection_name,
|
id: item.collection_name,
|
||||||
|
|
@ -203,17 +208,22 @@
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
capabilities = { ...capabilities, ...(model?.info?.meta?.capabilities ?? {}) };
|
capabilities = { ...capabilities, ...(model?.meta?.capabilities ?? {}) };
|
||||||
if (model?.owned_by === 'openai') {
|
if (model?.owned_by === 'openai') {
|
||||||
capabilities.usage = false;
|
capabilities.usage = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accessControl = model?.access_control ?? null;
|
||||||
|
|
||||||
|
console.log(model?.access_control);
|
||||||
|
console.log(accessControl);
|
||||||
|
|
||||||
info = {
|
info = {
|
||||||
...info,
|
...info,
|
||||||
...JSON.parse(
|
...JSON.parse(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
model?.info
|
model
|
||||||
? model?.info
|
? model
|
||||||
: {
|
: {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
name: model.name
|
name: model.name
|
||||||
|
|
@ -230,6 +240,31 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
|
{#if onBack}
|
||||||
|
<button
|
||||||
|
class="flex space-x-1"
|
||||||
|
on:click={() => {
|
||||||
|
onBack();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="h-4 w-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center text-sm font-medium">{'Back'}</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="w-full max-h-full flex justify-center">
|
<div class="w-full max-h-full flex justify-center">
|
||||||
<input
|
<input
|
||||||
bind:this={filesInputElement}
|
bind:this={filesInputElement}
|
||||||
|
|
@ -298,7 +333,7 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if !edit || model}
|
{#if !edit || (edit && model)}
|
||||||
<form
|
<form
|
||||||
class="flex flex-col md:flex-row w-full gap-3 md:gap-6"
|
class="flex flex-col md:flex-row w-full gap-3 md:gap-6"
|
||||||
on:submit|preventDefault={() => {
|
on:submit|preventDefault={() => {
|
||||||
|
|
@ -308,7 +343,7 @@
|
||||||
<div class="self-center md:self-start flex justify-center my-2 flex-shrink-0">
|
<div class="self-center md:self-start flex justify-center my-2 flex-shrink-0">
|
||||||
<div class="self-center">
|
<div class="self-center">
|
||||||
<button
|
<button
|
||||||
class="rounded-2xl flex flex-shrink-0 items-center bg-white shadow-2xl group relative"
|
class="rounded-2xl flex flex-shrink-0 items-center bg-white shadow-xl group relative"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
filesInputElement.click();
|
filesInputElement.click();
|
||||||
|
|
@ -318,13 +353,13 @@
|
||||||
<img
|
<img
|
||||||
src={info.meta.profile_image_url}
|
src={info.meta.profile_image_url}
|
||||||
alt="model profile"
|
alt="model profile"
|
||||||
class="rounded-lg size-72 md:size-64 object-cover shrink-0"
|
class="rounded-lg size-72 md:size-60 object-cover shrink-0"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<img
|
<img
|
||||||
src="/static/favicon.png"
|
src="/static/favicon.png"
|
||||||
alt="model profile"
|
alt="model profile"
|
||||||
class=" rounded-lg size-72 md:size-64 object-cover shrink-0"
|
class=" rounded-lg size-72 md:size-60 object-cover shrink-0"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -383,7 +418,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !edit || model.preset}
|
{#if preset}
|
||||||
<div class="my-1">
|
<div class="my-1">
|
||||||
<div class=" text-sm font-semibold mb-1">{$i18n.t('Base Model (From)')}</div>
|
<div class=" text-sm font-semibold mb-1">{$i18n.t('Base Model (From)')}</div>
|
||||||
|
|
||||||
|
|
@ -441,7 +476,33 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-1.5" />
|
<div class="my-1">
|
||||||
|
<div class="">
|
||||||
|
<Tags
|
||||||
|
tags={info?.meta?.tags ?? []}
|
||||||
|
on:delete={(e) => {
|
||||||
|
const tagName = e.detail;
|
||||||
|
info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
|
||||||
|
}}
|
||||||
|
on:add={(e) => {
|
||||||
|
const tagName = e.detail;
|
||||||
|
if (!(info?.meta?.tags ?? null)) {
|
||||||
|
info.meta.tags = [{ name: tagName }];
|
||||||
|
} else {
|
||||||
|
info.meta.tags = [...info.meta.tags, { name: tagName }];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-2">
|
||||||
|
<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
|
||||||
|
<AccessControl bind:accessControl />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-50 dark:border-gray-850 my-1.5" />
|
||||||
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
|
|
@ -495,7 +556,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-1" />
|
<hr class=" border-gray-50 dark:border-gray-850 my-1" />
|
||||||
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<div class="flex w-full justify-between items-center">
|
<div class="flex w-full justify-between items-center">
|
||||||
|
|
@ -592,7 +653,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-1.5" />
|
<hr class=" border-gray-50 dark:border-gray-850 my-1.5" />
|
||||||
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<Knowledge bind:selectedKnowledge={knowledge} collections={$knowledgeCollections} />
|
<Knowledge bind:selectedKnowledge={knowledge} collections={$knowledgeCollections} />
|
||||||
|
|
@ -620,30 +681,6 @@
|
||||||
<Capabilities bind:capabilities />
|
<Capabilities bind:capabilities />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-1">
|
|
||||||
<div class="flex w-full justify-between items-center">
|
|
||||||
<div class=" self-center text-sm font-semibold">{$i18n.t('Tags')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2">
|
|
||||||
<Tags
|
|
||||||
tags={info?.meta?.tags ?? []}
|
|
||||||
on:delete={(e) => {
|
|
||||||
const tagName = e.detail;
|
|
||||||
info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
|
|
||||||
}}
|
|
||||||
on:add={(e) => {
|
|
||||||
const tagName = e.detail;
|
|
||||||
if (!(info?.meta?.tags ?? null)) {
|
|
||||||
info.meta.tags = [{ name: tagName }];
|
|
||||||
} else {
|
|
||||||
info.meta.tags = [...info.meta.tags, { name: tagName }];
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-2 text-gray-300 dark:text-gray-700">
|
<div class="my-2 text-gray-300 dark:text-gray-700">
|
||||||
<div class="flex w-full justify-between mb-2">
|
<div class="flex w-full justify-between mb-2">
|
||||||
<div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>
|
<div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>
|
||||||
|
|
@ -679,8 +716,8 @@
|
||||||
<div class="my-2 flex justify-end pb-20">
|
<div class="my-2 flex justify-end pb-20">
|
||||||
<button
|
<button
|
||||||
class=" text-sm px-3 py-2 transition rounded-lg {loading
|
class=" text-sm px-3 py-2 transition rounded-lg {loading
|
||||||
? ' cursor-not-allowed bg-white hover:bg-gray-100 text-black'
|
? ' cursor-not-allowed bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'
|
||||||
: ' bg-white hover:bg-gray-100 text-black'} flex w-full justify-center"
|
: 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let user;
|
||||||
export let model;
|
export let model;
|
||||||
|
|
||||||
export let shareHandler: Function;
|
export let shareHandler: Function;
|
||||||
export let cloneHandler: Function;
|
export let cloneHandler: Function;
|
||||||
export let exportHandler: Function;
|
export let exportHandler: Function;
|
||||||
|
|
||||||
export let moveToTopHandler: Function;
|
|
||||||
export let hideHandler: Function;
|
export let hideHandler: Function;
|
||||||
export let deleteHandler: Function;
|
export let deleteHandler: Function;
|
||||||
export let onClose: Function;
|
export let onClose: Function;
|
||||||
|
|
@ -82,69 +82,6 @@
|
||||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<DropdownMenu.Item
|
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
|
||||||
on:click={() => {
|
|
||||||
moveToTopHandler();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowUpCircle />
|
|
||||||
|
|
||||||
<div class="flex items-center">{$i18n.t('Move to Top')}</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
|
|
||||||
<DropdownMenu.Item
|
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
|
||||||
on:click={() => {
|
|
||||||
hideHandler();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if model?.info?.meta?.hidden ?? false}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
{#if model?.info?.meta?.hidden ?? false}
|
|
||||||
{$i18n.t('Show Model')}
|
|
||||||
{:else}
|
|
||||||
{$i18n.t('Hide Model')}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
|
|
||||||
|
|
@ -3,28 +3,39 @@
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { onMount, getContext } from 'svelte';
|
|
||||||
import { WEBUI_NAME, config, prompts } from '$lib/stores';
|
|
||||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createNewPrompt,
|
||||||
|
deletePromptByCommand,
|
||||||
|
getPrompts,
|
||||||
|
getPromptList
|
||||||
|
} from '$lib/apis/prompts';
|
||||||
|
|
||||||
import PromptMenu from './Prompts/PromptMenu.svelte';
|
import PromptMenu from './Prompts/PromptMenu.svelte';
|
||||||
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import Search from '../icons/Search.svelte';
|
import Search from '../icons/Search.svelte';
|
||||||
import Plus from '../icons/Plus.svelte';
|
import Plus from '../icons/Plus.svelte';
|
||||||
|
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||||
|
import Spinner from '../common/Spinner.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
let promptsImportInputElement: HTMLInputElement;
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
let importFiles = '';
|
let importFiles = '';
|
||||||
let query = '';
|
let query = '';
|
||||||
let promptsImportInputElement: HTMLInputElement;
|
|
||||||
|
let prompts = [];
|
||||||
|
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
let deletePrompt = null;
|
let deletePrompt = null;
|
||||||
|
|
||||||
let filteredItems = [];
|
let filteredItems = [];
|
||||||
$: filteredItems = $prompts.filter((p) => query === '' || p.command.includes(query));
|
$: filteredItems = prompts.filter((p) => query === '' || p.command.includes(query));
|
||||||
|
|
||||||
const shareHandler = async (prompt) => {
|
const shareHandler = async (prompt) => {
|
||||||
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
||||||
|
|
@ -59,8 +70,18 @@
|
||||||
const deleteHandler = async (prompt) => {
|
const deleteHandler = async (prompt) => {
|
||||||
const command = prompt.command;
|
const command = prompt.command;
|
||||||
await deletePromptByCommand(localStorage.token, command);
|
await deletePromptByCommand(localStorage.token, command);
|
||||||
await prompts.set(await getPrompts(localStorage.token));
|
await init();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
prompts = await getPromptList(localStorage.token);
|
||||||
|
await _prompts.set(await getPrompts(localStorage.token));
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await init();
|
||||||
|
loaded = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -69,251 +90,239 @@
|
||||||
</title>
|
</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
{#if loaded}
|
||||||
bind:show={showDeleteConfirm}
|
<DeleteConfirmDialog
|
||||||
title={$i18n.t('Delete prompt?')}
|
bind:show={showDeleteConfirm}
|
||||||
on:confirm={() => {
|
title={$i18n.t('Delete prompt?')}
|
||||||
deleteHandler(deletePrompt);
|
on:confirm={() => {
|
||||||
}}
|
deleteHandler(deletePrompt);
|
||||||
>
|
}}
|
||||||
<div class=" text-sm text-gray-500">
|
>
|
||||||
{$i18n.t('This will delete')} <span class=" font-semibold">{deletePrompt.command}</span>.
|
<div class=" text-sm text-gray-500">
|
||||||
</div>
|
{$i18n.t('This will delete')} <span class=" font-semibold">{deletePrompt.command}</span>.
|
||||||
</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
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DeleteConfirmDialog>
|
||||||
|
|
||||||
<div class=" flex w-full space-x-2">
|
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
||||||
<div class="flex flex-1">
|
<div class="flex justify-between items-center">
|
||||||
<div class=" self-center ml-1 mr-3">
|
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
||||||
<Search className="size-3.5" />
|
{$i18n.t('Prompts')}
|
||||||
|
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||||
|
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
|
||||||
|
>{filteredItems.length}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
|
||||||
bind:value={query}
|
|
||||||
placeholder={$i18n.t('Search Prompts')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class=" flex w-full space-x-2">
|
||||||
<a
|
<div class="flex flex-1">
|
||||||
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
|
<div class=" self-center ml-1 mr-3">
|
||||||
href="/workspace/prompts/create"
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
{:else}
|
||||||
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
<div class="mb-5">
|
<Spinner />
|
||||||
{#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>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import AccessControl from '../common/AccessControl.svelte';
|
||||||
|
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||||
|
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||||
|
|
||||||
export let onSubmit: Function;
|
export let onSubmit: Function;
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
|
|
@ -17,6 +20,10 @@
|
||||||
let command = '';
|
let command = '';
|
||||||
let content = '';
|
let content = '';
|
||||||
|
|
||||||
|
let accessControl = null;
|
||||||
|
|
||||||
|
let showAccessControlModal = false;
|
||||||
|
|
||||||
$: if (!edit) {
|
$: if (!edit) {
|
||||||
command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : '';
|
command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : '';
|
||||||
}
|
}
|
||||||
|
|
@ -28,7 +35,8 @@
|
||||||
await onSubmit({
|
await onSubmit({
|
||||||
title,
|
title,
|
||||||
command,
|
command,
|
||||||
content
|
content,
|
||||||
|
access_control: accessControl
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|
@ -54,10 +62,14 @@
|
||||||
|
|
||||||
command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command;
|
command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command;
|
||||||
content = prompt.content;
|
content = prompt.content;
|
||||||
|
|
||||||
|
accessControl = prompt?.access_control ?? null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<AccessControlModal bind:show={showAccessControlModal} bind:accessControl />
|
||||||
|
|
||||||
<div class="w-full max-h-full flex justify-center">
|
<div class="w-full max-h-full flex justify-center">
|
||||||
<form
|
<form
|
||||||
class="flex flex-col w-full mb-10"
|
class="flex flex-col w-full mb-10"
|
||||||
|
|
@ -76,13 +88,29 @@
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div>
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
class="text-2xl font-semibold w-full bg-transparent outline-none"
|
class="text-2xl font-semibold w-full bg-transparent outline-none"
|
||||||
placeholder={$i18n.t('Title')}
|
placeholder={$i18n.t('Title')}
|
||||||
bind:value={title}
|
bind:value={title}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="bg-gray-50 hover:bg-gray-100 text-black transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showAccessControlModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||||
|
|
||||||
|
<div class="text-sm font-medium flex-shrink-0">
|
||||||
|
{$i18n.t('Share')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-0.5 items-center text-xs text-gray-500">
|
<div class="flex gap-0.5 items-center text-xs text-gray-500">
|
||||||
|
|
@ -138,8 +166,8 @@
|
||||||
<div class="my-4 flex justify-end pb-20">
|
<div class="my-4 flex justify-end pb-20">
|
||||||
<button
|
<button
|
||||||
class=" text-sm w-full lg:w-fit px-4 py-2 transition rounded-lg {loading
|
class=" text-sm w-full lg:w-fit px-4 py-2 transition rounded-lg {loading
|
||||||
? ' cursor-not-allowed bg-white hover:bg-gray-100 text-black'
|
? ' cursor-not-allowed bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'
|
||||||
: ' bg-white hover:bg-gray-100 text-black'} flex justify-center"
|
: 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { WEBUI_NAME, config, prompts, tools } from '$lib/stores';
|
import { WEBUI_NAME, config, prompts, tools as _tools, user } from '$lib/stores';
|
||||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
deleteToolById,
|
deleteToolById,
|
||||||
exportTools,
|
exportTools,
|
||||||
getToolById,
|
getToolById,
|
||||||
|
getToolList,
|
||||||
getTools
|
getTools
|
||||||
} from '$lib/apis/tools';
|
} from '$lib/apis/tools';
|
||||||
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
|
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
|
||||||
|
|
@ -27,10 +28,13 @@
|
||||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||||
import Search from '../icons/Search.svelte';
|
import Search from '../icons/Search.svelte';
|
||||||
import Plus from '../icons/Plus.svelte';
|
import Plus from '../icons/Plus.svelte';
|
||||||
|
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||||
|
import Spinner from '../common/Spinner.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let shiftKey = false;
|
let shiftKey = false;
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
let toolsImportInputElement: HTMLInputElement;
|
let toolsImportInputElement: HTMLInputElement;
|
||||||
let importFiles;
|
let importFiles;
|
||||||
|
|
@ -44,8 +48,10 @@
|
||||||
|
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
|
|
||||||
|
let tools = [];
|
||||||
let filteredItems = [];
|
let filteredItems = [];
|
||||||
$: filteredItems = $tools.filter(
|
|
||||||
|
$: filteredItems = tools.filter(
|
||||||
(t) =>
|
(t) =>
|
||||||
query === '' ||
|
query === '' ||
|
||||||
t.name.toLowerCase().includes(query.toLowerCase()) ||
|
t.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
|
@ -117,11 +123,20 @@
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t('Tool deleted successfully'));
|
toast.success($i18n.t('Tool deleted successfully'));
|
||||||
tools.set(await getTools(localStorage.token));
|
|
||||||
|
init();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
const init = async () => {
|
||||||
|
tools = await getToolList(localStorage.token);
|
||||||
|
_tools.set(await getTools(localStorage.token));
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await init();
|
||||||
|
loaded = true;
|
||||||
|
|
||||||
const onKeyDown = (event) => {
|
const onKeyDown = (event) => {
|
||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
shiftKey = true;
|
shiftKey = true;
|
||||||
|
|
@ -156,347 +171,336 @@
|
||||||
</title>
|
</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
{#if loaded}
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
||||||
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
<div class="flex justify-between items-center">
|
||||||
{$i18n.t('Tools')}
|
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
||||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
{$i18n.t('Tools')}
|
||||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
|
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||||
>{filteredItems.length}</span
|
<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>
|
</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>
|
||||||
|
|
||||||
<div>
|
<div class=" flex w-full space-x-2">
|
||||||
<a
|
<div class="flex flex-1">
|
||||||
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
|
<div class=" self-center ml-1 mr-3">
|
||||||
href="/workspace/tools/create"
|
<Search className="size-3.5" />
|
||||||
>
|
</div>
|
||||||
<Plus className="size-3.5" />
|
<input
|
||||||
</a>
|
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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
{#each filteredItems as tool}
|
{#each filteredItems as tool}
|
||||||
<div
|
<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"
|
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="flex items-center text-left">
|
<a
|
||||||
<div class=" flex-1 self-center pl-1">
|
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
||||||
<div class=" font-semibold flex items-center gap-1.5">
|
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
|
||||||
<div
|
>
|
||||||
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
<div class="flex items-center text-left">
|
||||||
>
|
<div class=" flex-1 self-center pl-1">
|
||||||
TOOL
|
<div class=" font-semibold flex items-center gap-1.5">
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if tool?.meta?.manifest?.version}
|
|
||||||
<div
|
<div
|
||||||
class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
v{tool?.meta?.manifest?.version ?? ''}
|
TOOL
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="line-clamp-1">
|
{#if tool?.meta?.manifest?.version}
|
||||||
{tool.name}
|
<div
|
||||||
|
class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
v{tool?.meta?.manifest?.version ?? ''}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="line-clamp-1">
|
||||||
|
{tool.name}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-1.5 px-1">
|
<div class="flex gap-1.5 px-1">
|
||||||
<div class=" text-gray-500 text-xs font-medium flex-shrink-0">{tool.id}</div>
|
<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">
|
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||||
{tool.meta.description}
|
{tool.meta.description}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
<div class="flex flex-row gap-0.5 self-center">
|
||||||
<div class="flex flex-row gap-0.5 self-center">
|
{#if shiftKey}
|
||||||
{#if shiftKey}
|
<Tooltip content={$i18n.t('Delete')}>
|
||||||
<Tooltip content={$i18n.t('Delete')}>
|
<button
|
||||||
<button
|
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
type="button"
|
||||||
type="button"
|
on:click={() => {
|
||||||
on:click={() => {
|
deleteHandler(tool);
|
||||||
deleteHandler(tool);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<GarbageBin />
|
||||||
<GarbageBin />
|
</button>
|
||||||
</button>
|
</Tooltip>
|
||||||
</Tooltip>
|
{:else}
|
||||||
{:else}
|
{#if tool?.meta?.manifest?.funding_url ?? false}
|
||||||
{#if tool?.meta?.manifest?.funding_url ?? false}
|
<Tooltip content="Support">
|
||||||
<Tooltip content="Support">
|
<button
|
||||||
|
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTool = tool;
|
||||||
|
showManifestModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heart />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Tooltip content={$i18n.t('Valves')}>
|
||||||
<button
|
<button
|
||||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTool = tool;
|
selectedTool = tool;
|
||||||
showManifestModal = true;
|
showValvesModal = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Heart />
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Valves')}>
|
<ToolMenu
|
||||||
<button
|
editHandler={() => {
|
||||||
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"
|
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTool = tool;
|
|
||||||
showValvesModal = true;
|
|
||||||
}}
|
}}
|
||||||
|
shareHandler={() => {
|
||||||
|
shareHandler(tool);
|
||||||
|
}}
|
||||||
|
cloneHandler={() => {
|
||||||
|
cloneHandler(tool);
|
||||||
|
}}
|
||||||
|
exportHandler={() => {
|
||||||
|
exportHandler(tool);
|
||||||
|
}}
|
||||||
|
deleteHandler={async () => {
|
||||||
|
selectedTool = tool;
|
||||||
|
showDeleteConfirm = true;
|
||||||
|
}}
|
||||||
|
onClose={() => {}}
|
||||||
>
|
>
|
||||||
<svg
|
<button
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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"
|
||||||
fill="none"
|
type="button"
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-4"
|
|
||||||
>
|
>
|
||||||
<path
|
<EllipsisHorizontal className="size-5" />
|
||||||
stroke-linecap="round"
|
</button>
|
||||||
stroke-linejoin="round"
|
</ToolMenu>
|
||||||
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"
|
{/if}
|
||||||
/>
|
</div>
|
||||||
<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}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
{/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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $config?.features.enable_community_sharing}
|
{#if $user?.role === 'admin'}
|
||||||
<div class=" my-16">
|
<div class=" flex justify-end w-full mb-2">
|
||||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
<div class="flex space-x-2">
|
||||||
{$i18n.t('Made by OpenWebUI Community')}
|
<input
|
||||||
</div>
|
id="documents-import-input"
|
||||||
|
bind:this={toolsImportInputElement}
|
||||||
|
bind:files={importFiles}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
hidden
|
||||||
|
on:change={() => {
|
||||||
|
console.log(importFiles);
|
||||||
|
showConfirm = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<a
|
<button
|
||||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
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"
|
||||||
href="https://openwebui.com/#open-webui-community"
|
on:click={() => {
|
||||||
target="_blank"
|
toolsImportInputElement.click();
|
||||||
>
|
}}
|
||||||
<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
|
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Tools')}</div>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
<div class=" self-center">
|
||||||
fill="currentColor"
|
<svg
|
||||||
class="w-6"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
viewBox="0 0 16 16"
|
||||||
<path
|
fill="currentColor"
|
||||||
fill-rule="evenodd"
|
class="w-4 h-4"
|
||||||
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"
|
<path
|
||||||
/>
|
fill-rule="evenodd"
|
||||||
</svg>
|
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"
|
||||||
</div>
|
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>
|
||||||
|
|
||||||
<div class=" self-center">
|
<a
|
||||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div>
|
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"
|
||||||
<div class=" text-sm line-clamp-1">
|
href="https://openwebui.com/#open-webui-community"
|
||||||
{$i18n.t('Discover, download, and explore custom tools')}
|
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>
|
||||||
|
<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>
|
</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>
|
</div>
|
||||||
{/if}
|
{/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 Badge from '$lib/components/common/Badge.svelte';
|
||||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||||
|
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let formElement = null;
|
let formElement = null;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
let showConfirm = false;
|
let showConfirm = false;
|
||||||
|
let showAccessControlModal = false;
|
||||||
|
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
export let clone = false;
|
export let clone = false;
|
||||||
|
|
@ -25,6 +29,8 @@
|
||||||
description: ''
|
description: ''
|
||||||
};
|
};
|
||||||
export let content = '';
|
export let content = '';
|
||||||
|
export let accessControl = null;
|
||||||
|
|
||||||
let _content = '';
|
let _content = '';
|
||||||
|
|
||||||
$: if (content) {
|
$: if (content) {
|
||||||
|
|
@ -148,7 +154,8 @@ class Tools:
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
meta,
|
meta,
|
||||||
content
|
content,
|
||||||
|
access_control: accessControl
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -172,6 +179,8 @@ class Tools:
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<AccessControlModal bind:show={showAccessControlModal} bind:accessControl />
|
||||||
|
|
||||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||||
<div class="mx-auto w-full md:px-0 h-full">
|
<div class="mx-auto w-full md:px-0 h-full">
|
||||||
<form
|
<form
|
||||||
|
|
@ -203,11 +212,11 @@ class Tools:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Tooltip content={$i18n.t('e.g. My ToolKit')} placement="top-start">
|
<Tooltip content={$i18n.t('e.g. My Tools')} placement="top-start">
|
||||||
<input
|
<input
|
||||||
class="w-full text-2xl font-medium bg-transparent outline-none"
|
class="w-full text-2xl font-semibold bg-transparent outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Toolkit Name')}
|
placeholder={$i18n.t('Tool Name')}
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -215,7 +224,19 @@ class Tools:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Badge type="muted" content={$i18n.t('Tool')} />
|
<button
|
||||||
|
class="bg-gray-50 hover:bg-gray-100 text-black transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showAccessControlModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||||
|
|
||||||
|
<div class="text-sm font-medium flex-shrink-0">
|
||||||
|
{$i18n.t('Share')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -225,15 +246,11 @@ class Tools:
|
||||||
{id}
|
{id}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Tooltip
|
<Tooltip className="w-full" content={$i18n.t('e.g. my_tools')} placement="top-start">
|
||||||
className="w-full"
|
|
||||||
content={$i18n.t('e.g. my_toolkit')}
|
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
|
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Toolkit ID')}
|
placeholder={$i18n.t('Tool ID')}
|
||||||
bind:value={id}
|
bind:value={id}
|
||||||
required
|
required
|
||||||
disabled={edit}
|
disabled={edit}
|
||||||
|
|
@ -243,13 +260,13 @@ class Tools:
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="w-full self-center items-center flex"
|
className="w-full self-center items-center flex"
|
||||||
content={$i18n.t('e.g. A toolkit for performing various operations')}
|
content={$i18n.t('e.g. Tools for performing various operations')}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent outline-none"
|
class="w-full text-sm bg-transparent outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Toolkit Description')}
|
placeholder={$i18n.t('Tool Description')}
|
||||||
bind:value={meta.description}
|
bind:value={meta.description}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
193
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 { page } from '$app/stores';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
import { getKnowledgeItems } from '$lib/apis/knowledge';
|
import { getKnowledgeBases } from '$lib/apis/knowledge';
|
||||||
import { getFunctions } from '$lib/apis/functions';
|
import { getFunctions } from '$lib/apis/functions';
|
||||||
import { getModels, getVersionUpdates } from '$lib/apis';
|
import { getModels, getVersionUpdates } from '$lib/apis';
|
||||||
import { getAllTags } from '$lib/apis/chats';
|
import { getAllTags } from '$lib/apis/chats';
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,13 @@
|
||||||
href="/admin/evaluations">{$i18n.t('Evaluations')}</a
|
href="/admin/evaluations">{$i18n.t('Evaluations')}</a
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/admin/functions')
|
||||||
|
? ''
|
||||||
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
|
href="/admin/functions">{$i18n.t('Functions')}</a
|
||||||
|
>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/admin/settings')
|
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/admin/settings')
|
||||||
? ''
|
? ''
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { functions } from '$lib/stores';
|
import { functions } from '$lib/stores';
|
||||||
|
|
||||||
import { getFunctions } from '$lib/apis/functions';
|
import { getFunctions } from '$lib/apis/functions';
|
||||||
import Functions from '$lib/components/workspace/Functions.svelte';
|
import Functions from '$lib/components/admin/Functions.svelte';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { functions, models } from '$lib/stores';
|
import { functions, models } from '$lib/stores';
|
||||||
import { createNewFunction, getFunctions } from '$lib/apis/functions';
|
import { createNewFunction, getFunctions } from '$lib/apis/functions';
|
||||||
import FunctionEditor from '$lib/components/workspace/Functions/FunctionEditor.svelte';
|
import FunctionEditor from '$lib/components/admin/Functions/FunctionEditor.svelte';
|
||||||
import { getModels } from '$lib/apis';
|
import { getModels } from '$lib/apis';
|
||||||
import { compareVersion, extractFrontmatter } from '$lib/utils';
|
import { compareVersion, extractFrontmatter } from '$lib/utils';
|
||||||
import { WEBUI_VERSION } from '$lib/constants';
|
import { WEBUI_VERSION } from '$lib/constants';
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
import { functions, models } from '$lib/stores';
|
import { functions, models } from '$lib/stores';
|
||||||
import { updateFunctionById, getFunctions, getFunctionById } from '$lib/apis/functions';
|
import { updateFunctionById, getFunctions, getFunctionById } from '$lib/apis/functions';
|
||||||
|
|
||||||
import FunctionEditor from '$lib/components/workspace/Functions/FunctionEditor.svelte';
|
import FunctionEditor from '$lib/components/admin/Functions/FunctionEditor.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import { getModels } from '$lib/apis';
|
import { getModels } from '$lib/apis';
|
||||||
import { compareVersion, extractFrontmatter } from '$lib/utils';
|
import { compareVersion, extractFrontmatter } from '$lib/utils';
|
||||||
|
|
@ -15,11 +15,6 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import MenuLines from '$lib/components/icons/MenuLines.svelte';
|
import MenuLines from '$lib/components/icons/MenuLines.svelte';
|
||||||
import { getModels } from '$lib/apis';
|
|
||||||
import { getPrompts } from '$lib/apis/prompts';
|
|
||||||
import { getKnowledgeItems } from '$lib/apis/knowledge';
|
|
||||||
import { getTools } from '$lib/apis/tools';
|
|
||||||
import { getFunctions } from '$lib/apis/functions';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -27,7 +22,21 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user?.role !== 'admin') {
|
if ($user?.role !== 'admin') {
|
||||||
await goto('/');
|
if ($page.url.pathname.includes('/models') && !$user?.permissions?.workspace?.models) {
|
||||||
|
goto('/');
|
||||||
|
} else if (
|
||||||
|
$page.url.pathname.includes('/knowledge') &&
|
||||||
|
!$user?.permissions?.workspace?.knowledge
|
||||||
|
) {
|
||||||
|
goto('/');
|
||||||
|
} else if (
|
||||||
|
$page.url.pathname.includes('/prompts') &&
|
||||||
|
!$user?.permissions?.workspace?.prompts
|
||||||
|
) {
|
||||||
|
goto('/');
|
||||||
|
} else if ($page.url.pathname.includes('/tools') && !$user?.permissions?.workspace?.tools) {
|
||||||
|
goto('/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
|
|
@ -46,7 +55,7 @@
|
||||||
? 'md:max-w-[calc(100%-260px)]'
|
? 'md:max-w-[calc(100%-260px)]'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
<div class=" px-2.5 py-1 backdrop-blur-xl">
|
<div class=" px-2.5 pt-1 backdrop-blur-xl">
|
||||||
<div class=" flex items-center gap-1">
|
<div class=" flex items-center gap-1">
|
||||||
<div class="{$showSidebar ? 'md:hidden' : ''} self-center flex flex-none items-center">
|
<div class="{$showSidebar ? 'md:hidden' : ''} self-center flex flex-none items-center">
|
||||||
<button
|
<button
|
||||||
|
|
@ -67,50 +76,51 @@
|
||||||
<div
|
<div
|
||||||
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
|
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
|
||||||
>
|
>
|
||||||
<a
|
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models}
|
||||||
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/workspace/models')
|
<a
|
||||||
? ''
|
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes(
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
'/workspace/models'
|
||||||
href="/workspace/models">{$i18n.t('Models')}</a
|
)
|
||||||
>
|
? ''
|
||||||
|
: '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
|
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.knowledge}
|
||||||
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes(
|
<a
|
||||||
'/workspace/knowledge'
|
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"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
>
|
href="/workspace/knowledge"
|
||||||
{$i18n.t('Knowledge')}
|
>
|
||||||
</a>
|
{$i18n.t('Knowledge')}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<a
|
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.prompts}
|
||||||
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/workspace/prompts')
|
<a
|
||||||
? ''
|
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes(
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
'/workspace/prompts'
|
||||||
href="/workspace/prompts">{$i18n.t('Prompts')}</a
|
)
|
||||||
>
|
? ''
|
||||||
|
: '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
|
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.tools}
|
||||||
class="min-w-fit rounded-full p-1.5 {$page.url.pathname.includes('/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"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
>
|
href="/workspace/tools"
|
||||||
{$i18n.t('Tools')}
|
>
|
||||||
</a>
|
{$i18n.t('Tools')}
|
||||||
|
</a>
|
||||||
<a
|
{/if}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -118,7 +128,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" -mt-1 pb-1 px-[18px] flex-1 max-h-full overflow-y-auto" id="workspace-container">
|
<div class=" pb-1 px-[18px] flex-1 max-h-full overflow-y-auto" id="workspace-container">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { knowledge } from '$lib/stores';
|
import { knowledge } from '$lib/stores';
|
||||||
|
|
||||||
import { getKnowledgeItems } from '$lib/apis/knowledge';
|
import { getKnowledgeBases } from '$lib/apis/knowledge';
|
||||||
import Knowledge from '$lib/components/workspace/Knowledge.svelte';
|
import Knowledge from '$lib/components/workspace/Knowledge.svelte';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
(async () => {
|
(async () => {
|
||||||
knowledge.set(await getKnowledgeItems(localStorage.token));
|
knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||||
})()
|
})()
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import Collection from '$lib/components/workspace/Knowledge/Collection.svelte';
|
import KnowledgeBase from '$lib/components/workspace/Knowledge/KnowledgeBase.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Collection />
|
<KnowledgeBase />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import CreateCollection from '$lib/components/workspace/Knowledge/CreateCollection.svelte';
|
import CreateKnowledgeBase from '$lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CreateCollection />
|
<CreateKnowledgeBase />
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
|
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
import { addNewModel, getModelById, getModelInfos } from '$lib/apis/models';
|
import { createNewModel, getModelById } from '$lib/apis/models';
|
||||||
import { getModels } from '$lib/apis';
|
import { getModels } from '$lib/apis';
|
||||||
|
|
||||||
import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
|
import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelInfo) {
|
if (modelInfo) {
|
||||||
const res = await addNewModel(localStorage.token, {
|
const res = await createNewModel(localStorage.token, {
|
||||||
...modelInfo,
|
...modelInfo,
|
||||||
meta: {
|
meta: {
|
||||||
...modelInfo.meta,
|
...modelInfo.meta,
|
||||||
|
|
@ -31,6 +31,9 @@
|
||||||
: null
|
: null
|
||||||
},
|
},
|
||||||
params: { ...modelInfo.params }
|
params: { ...modelInfo.params }
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,20 @@
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
|
|
||||||
import { updateModelById } from '$lib/apis/models';
|
import { getModelById, updateModelById } from '$lib/apis/models';
|
||||||
|
|
||||||
import { getModels } from '$lib/apis';
|
import { getModels } from '$lib/apis';
|
||||||
import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
|
import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
|
||||||
|
|
||||||
let model = null;
|
let model = null;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
const _id = $page.url.searchParams.get('id');
|
const _id = $page.url.searchParams.get('id');
|
||||||
if (_id) {
|
if (_id) {
|
||||||
model = $models.find((m) => m.id === _id && m?.owned_by !== 'arena');
|
model = await getModelById(localStorage.token, _id).catch((e) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
goto('/workspace/models');
|
goto('/workspace/models');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { prompts } from '$lib/stores';
|
|
||||||
|
|
||||||
import { getPrompts } from '$lib/apis/prompts';
|
|
||||||
import Prompts from '$lib/components/workspace/Prompts.svelte';
|
import Prompts from '$lib/components/workspace/Prompts.svelte';
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
(async () => {
|
|
||||||
prompts.set(await getPrompts(localStorage.token));
|
|
||||||
})()
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $prompts !== null}
|
<Prompts />
|
||||||
<Prompts />
|
|
||||||
{/if}
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { prompts } from '$lib/stores';
|
import { prompts } from '$lib/stores';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import { createNewPrompt, getPrompts } from '$lib/apis/prompts';
|
import { createNewPrompt, getPrompts } from '$lib/apis/prompts';
|
||||||
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
|
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
|
||||||
|
|
||||||
let prompt = null;
|
let prompt = null;
|
||||||
const onSubmit = async ({ title, command, content }) => {
|
const onSubmit = async (_prompt) => {
|
||||||
const prompt = await createNewPrompt(localStorage.token, command, title, content).catch(
|
const prompt = await createNewPrompt(localStorage.token, _prompt).catch((error) => {
|
||||||
(error) => {
|
toast.error(error);
|
||||||
toast.error(error);
|
return null;
|
||||||
|
});
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
|
toast.success($i18n.t('Prompt created successfully'));
|
||||||
|
|
||||||
await prompts.set(await getPrompts(localStorage.token));
|
await prompts.set(await getPrompts(localStorage.token));
|
||||||
await goto('/workspace/prompts');
|
await goto('/workspace/prompts');
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +38,8 @@
|
||||||
prompt = {
|
prompt = {
|
||||||
title: _prompt.title,
|
title: _prompt.title,
|
||||||
command: _prompt.command,
|
command: _prompt.command,
|
||||||
content: _prompt.content
|
content: _prompt.content,
|
||||||
|
access_control: null
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -51,7 +53,8 @@
|
||||||
prompt = {
|
prompt = {
|
||||||
title: _prompt.title,
|
title: _prompt.title,
|
||||||
command: _prompt.command,
|
command: _prompt.command,
|
||||||
content: _prompt.content
|
content: _prompt.content,
|
||||||
|
access_control: null
|
||||||
};
|
};
|
||||||
sessionStorage.removeItem('prompt');
|
sessionStorage.removeItem('prompt');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { prompts } from '$lib/stores';
|
import { prompts } from '$lib/stores';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
import { getPrompts, updatePromptByCommand } from '$lib/apis/prompts';
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { getPromptByCommand, getPrompts, updatePromptByCommand } from '$lib/apis/prompts';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
|
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
|
||||||
|
|
||||||
let prompt = null;
|
let prompt = null;
|
||||||
const onSubmit = async ({ title, command, content }) => {
|
const onSubmit = async (_prompt) => {
|
||||||
const prompt = await updatePromptByCommand(localStorage.token, command, title, content).catch(
|
console.log(_prompt);
|
||||||
(error) => {
|
const prompt = await updatePromptByCommand(localStorage.token, _prompt).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
|
toast.success($i18n.t('Prompt updated successfully'));
|
||||||
await prompts.set(await getPrompts(localStorage.token));
|
await prompts.set(await getPrompts(localStorage.token));
|
||||||
await goto('/workspace/prompts');
|
await goto('/workspace/prompts');
|
||||||
}
|
}
|
||||||
|
|
@ -27,13 +29,20 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const command = $page.url.searchParams.get('command');
|
const command = $page.url.searchParams.get('command');
|
||||||
if (command) {
|
if (command) {
|
||||||
const _prompt = $prompts.filter((prompt) => prompt.command === command).at(0);
|
const _prompt = await getPromptByCommand(
|
||||||
|
localStorage.token,
|
||||||
|
command.replace(/\//g, '')
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
if (_prompt) {
|
if (_prompt) {
|
||||||
prompt = {
|
prompt = {
|
||||||
title: _prompt.title,
|
title: _prompt.title,
|
||||||
command: _prompt.command,
|
command: _prompt.command,
|
||||||
content: _prompt.content
|
content: _prompt.content,
|
||||||
|
access_control: _prompt?.access_control ?? null
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
goto('/workspace/prompts');
|
goto('/workspace/prompts');
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { tools } from '$lib/stores';
|
|
||||||
|
|
||||||
import { getTools } from '$lib/apis/tools';
|
|
||||||
import Tools from '$lib/components/workspace/Tools.svelte';
|
import Tools from '$lib/components/workspace/Tools.svelte';
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
(async () => {
|
|
||||||
tools.set(await getTools(localStorage.token));
|
|
||||||
})()
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $tools !== null}
|
<Tools />
|
||||||
<Tools />
|
|
||||||
{/if}
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue