mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +00:00
update with upstream dev 21-11-2025
This commit is contained in:
commit
b6947c9813
75 changed files with 1576 additions and 760 deletions
|
|
@ -1203,6 +1203,12 @@ DEFAULT_USER_ROLE = PersistentConfig(
|
|||
os.getenv("DEFAULT_USER_ROLE", "pending"),
|
||||
)
|
||||
|
||||
DEFAULT_GROUP_ID = PersistentConfig(
|
||||
"DEFAULT_GROUP_ID",
|
||||
"ui.default_group_id",
|
||||
os.environ.get("DEFAULT_GROUP_ID", ""),
|
||||
)
|
||||
|
||||
PENDING_USER_OVERLAY_TITLE = PersistentConfig(
|
||||
"PENDING_USER_OVERLAY_TITLE",
|
||||
"ui.pending_user_overlay_title",
|
||||
|
|
@ -1270,6 +1276,12 @@ USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT = (
|
|||
os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT", "False").lower() == "true"
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING = (
|
||||
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING", "False").lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get(
|
||||
"USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING", "False"
|
||||
|
|
@ -1277,8 +1289,10 @@ USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = (
|
|||
== "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING", "False").lower()
|
||||
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = (
|
||||
os.environ.get(
|
||||
"USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING", "False"
|
||||
).lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
|
|
@ -1289,6 +1303,11 @@ USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = (
|
|||
== "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING = (
|
||||
os.environ.get("USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING", "False").lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get(
|
||||
"USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING", "False"
|
||||
|
|
@ -1296,6 +1315,12 @@ USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = (
|
|||
== "true"
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING = (
|
||||
os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING", "False").lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get(
|
||||
"USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING", "False"
|
||||
|
|
@ -1304,6 +1329,17 @@ USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = (
|
|||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_NOTES_ALLOW_SHARING = (
|
||||
os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING", "False").lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING", "False").lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_CHAT_CONTROLS = (
|
||||
os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true"
|
||||
)
|
||||
|
|
@ -1425,10 +1461,15 @@ DEFAULT_USER_PERMISSIONS = {
|
|||
"tools_export": USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT,
|
||||
},
|
||||
"sharing": {
|
||||
"models": USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING,
|
||||
"public_models": USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING,
|
||||
"knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING,
|
||||
"public_knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING,
|
||||
"prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING,
|
||||
"public_prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING,
|
||||
"tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING,
|
||||
"public_tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING,
|
||||
"notes": USER_PERMISSIONS_NOTES_ALLOW_SHARING,
|
||||
"public_notes": USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
|
||||
},
|
||||
"chat": {
|
||||
|
|
@ -2145,6 +2186,11 @@ ENABLE_QDRANT_MULTITENANCY_MODE = (
|
|||
)
|
||||
QDRANT_COLLECTION_PREFIX = os.environ.get("QDRANT_COLLECTION_PREFIX", "open-webui")
|
||||
|
||||
WEAVIATE_HTTP_HOST = os.environ.get("WEAVIATE_HTTP_HOST", "")
|
||||
WEAVIATE_HTTP_PORT = int(os.environ.get("WEAVIATE_HTTP_PORT", "8080"))
|
||||
WEAVIATE_GRPC_PORT = int(os.environ.get("WEAVIATE_GRPC_PORT", "50051"))
|
||||
WEAVIATE_API_KEY = os.environ.get("WEAVIATE_API_KEY")
|
||||
|
||||
# OpenSearch
|
||||
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
|
||||
OPENSEARCH_SSL = os.environ.get("OPENSEARCH_SSL", "true").lower() == "true"
|
||||
|
|
@ -3499,6 +3545,11 @@ IMAGES_GEMINI_ENDPOINT_METHOD = PersistentConfig(
|
|||
os.getenv("IMAGES_GEMINI_ENDPOINT_METHOD", ""),
|
||||
)
|
||||
|
||||
ENABLE_IMAGE_EDIT = PersistentConfig(
|
||||
"ENABLE_IMAGE_EDIT",
|
||||
"images.edit.enable",
|
||||
os.environ.get("ENABLE_IMAGE_EDIT", "").lower() == "true",
|
||||
)
|
||||
|
||||
IMAGE_EDIT_ENGINE = PersistentConfig(
|
||||
"IMAGE_EDIT_ENGINE",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class ERROR_MESSAGES(str, Enum):
|
|||
)
|
||||
INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
|
||||
INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)."
|
||||
INVALID_PASSWORD = (
|
||||
INCORRECT_PASSWORD = (
|
||||
"The password provided is incorrect. Please check for typos and try again."
|
||||
)
|
||||
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
|
||||
|
|
@ -105,6 +105,10 @@ class ERROR_MESSAGES(str, Enum):
|
|||
)
|
||||
FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding."
|
||||
|
||||
INVALID_PASSWORD = lambda err="": (
|
||||
err if err else "The password does not meet the required validation criteria."
|
||||
)
|
||||
|
||||
|
||||
class TASKS(str, Enum):
|
||||
def __str__(self) -> str:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import shutil
|
|||
from uuid import uuid4
|
||||
from pathlib import Path
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import re
|
||||
|
||||
|
||||
import markdown
|
||||
from bs4 import BeautifulSoup
|
||||
|
|
@ -135,6 +137,9 @@ else:
|
|||
PACKAGE_DATA = {"version": "0.0.0"}
|
||||
|
||||
VERSION = PACKAGE_DATA["version"]
|
||||
|
||||
|
||||
DEPLOYMENT_ID = os.environ.get("DEPLOYMENT_ID", "")
|
||||
INSTANCE_ID = os.environ.get("INSTANCE_ID", str(uuid4()))
|
||||
|
||||
|
||||
|
|
@ -426,6 +431,17 @@ WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.environ.get(
|
|||
)
|
||||
|
||||
|
||||
ENABLE_PASSWORD_VALIDATION = (
|
||||
os.environ.get("ENABLE_PASSWORD_VALIDATION", "False").lower() == "true"
|
||||
)
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get(
|
||||
"PASSWORD_VALIDATION_REGEX_PATTERN",
|
||||
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$",
|
||||
)
|
||||
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(PASSWORD_VALIDATION_REGEX_PATTERN)
|
||||
|
||||
|
||||
BYPASS_MODEL_ACCESS_CONTROL = (
|
||||
os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ from open_webui.config import (
|
|||
IMAGES_GEMINI_API_BASE_URL,
|
||||
IMAGES_GEMINI_API_KEY,
|
||||
IMAGES_GEMINI_ENDPOINT_METHOD,
|
||||
ENABLE_IMAGE_EDIT,
|
||||
IMAGE_EDIT_ENGINE,
|
||||
IMAGE_EDIT_MODEL,
|
||||
IMAGE_EDIT_SIZE,
|
||||
|
|
@ -369,6 +370,7 @@ from open_webui.config import (
|
|||
BYPASS_ADMIN_ACCESS_CONTROL,
|
||||
USER_PERMISSIONS,
|
||||
DEFAULT_USER_ROLE,
|
||||
DEFAULT_GROUP_ID,
|
||||
PENDING_USER_OVERLAY_CONTENT,
|
||||
PENDING_USER_OVERLAY_TITLE,
|
||||
DEFAULT_PROMPT_SUGGESTIONS,
|
||||
|
|
@ -455,6 +457,7 @@ from open_webui.env import (
|
|||
SAFE_MODE,
|
||||
SRC_LOG_LEVELS,
|
||||
VERSION,
|
||||
DEPLOYMENT_ID,
|
||||
INSTANCE_ID,
|
||||
WEBUI_BUILD_HASH,
|
||||
WEBUI_SECRET_KEY,
|
||||
|
|
@ -762,6 +765,7 @@ app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
|
|||
|
||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
app.state.config.DEFAULT_GROUP_ID = DEFAULT_GROUP_ID
|
||||
|
||||
app.state.config.PENDING_USER_OVERLAY_CONTENT = PENDING_USER_OVERLAY_CONTENT
|
||||
app.state.config.PENDING_USER_OVERLAY_TITLE = PENDING_USER_OVERLAY_TITLE
|
||||
|
|
@ -1116,6 +1120,7 @@ app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
|
|||
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
|
||||
|
||||
|
||||
app.state.config.ENABLE_IMAGE_EDIT = ENABLE_IMAGE_EDIT
|
||||
app.state.config.IMAGE_EDIT_ENGINE = IMAGE_EDIT_ENGINE
|
||||
app.state.config.IMAGE_EDIT_MODEL = IMAGE_EDIT_MODEL
|
||||
app.state.config.IMAGE_EDIT_SIZE = IMAGE_EDIT_SIZE
|
||||
|
|
@ -1451,6 +1456,10 @@ async def get_models(
|
|||
if "pipeline" in model and model["pipeline"].get("type", None) == "filter":
|
||||
continue
|
||||
|
||||
# Remove profile image URL to reduce payload size
|
||||
if model.get("info", {}).get("meta", {}).get("profile_image_url"):
|
||||
model["info"]["meta"].pop("profile_image_url", None)
|
||||
|
||||
try:
|
||||
model_tags = [
|
||||
tag.get("name")
|
||||
|
|
@ -1986,6 +1995,7 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
|||
async def get_app_version():
|
||||
return {
|
||||
"version": VERSION,
|
||||
"deployment_id": DEPLOYMENT_ID,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import uuid
|
|||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from open_webui.models.chats import Chats
|
||||
from open_webui.models.users import User
|
||||
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
|
@ -92,6 +92,28 @@ class FeedbackForm(BaseModel):
|
|||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
role: str = "pending"
|
||||
|
||||
last_active_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class FeedbackUserResponse(FeedbackResponse):
|
||||
user: Optional[UserResponse] = None
|
||||
|
||||
|
||||
class FeedbackListResponse(BaseModel):
|
||||
items: list[FeedbackUserResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class FeedbackTable:
|
||||
def insert_new_feedback(
|
||||
self, user_id: str, form_data: FeedbackForm
|
||||
|
|
@ -143,6 +165,70 @@ class FeedbackTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_feedback_items(
|
||||
self, filter: dict = {}, skip: int = 0, limit: int = 30
|
||||
) -> FeedbackListResponse:
|
||||
with get_db() as db:
|
||||
query = db.query(Feedback, User).join(User, Feedback.user_id == User.id)
|
||||
|
||||
if filter:
|
||||
order_by = filter.get("order_by")
|
||||
direction = filter.get("direction")
|
||||
|
||||
if order_by == "username":
|
||||
if direction == "asc":
|
||||
query = query.order_by(User.name.asc())
|
||||
else:
|
||||
query = query.order_by(User.name.desc())
|
||||
elif order_by == "model_id":
|
||||
# it's stored in feedback.data['model_id']
|
||||
if direction == "asc":
|
||||
query = query.order_by(
|
||||
Feedback.data["model_id"].as_string().asc()
|
||||
)
|
||||
else:
|
||||
query = query.order_by(
|
||||
Feedback.data["model_id"].as_string().desc()
|
||||
)
|
||||
elif order_by == "rating":
|
||||
# it's stored in feedback.data['rating']
|
||||
if direction == "asc":
|
||||
query = query.order_by(
|
||||
Feedback.data["rating"].as_string().asc()
|
||||
)
|
||||
else:
|
||||
query = query.order_by(
|
||||
Feedback.data["rating"].as_string().desc()
|
||||
)
|
||||
elif order_by == "updated_at":
|
||||
if direction == "asc":
|
||||
query = query.order_by(Feedback.updated_at.asc())
|
||||
else:
|
||||
query = query.order_by(Feedback.updated_at.desc())
|
||||
|
||||
else:
|
||||
query = query.order_by(Feedback.created_at.desc())
|
||||
|
||||
# Count BEFORE pagination
|
||||
total = query.count()
|
||||
|
||||
if skip:
|
||||
query = query.offset(skip)
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
items = query.all()
|
||||
|
||||
feedbacks = []
|
||||
for feedback, user in items:
|
||||
feedback_model = FeedbackModel.model_validate(feedback)
|
||||
user_model = UserResponse.model_validate(user)
|
||||
feedbacks.append(
|
||||
FeedbackUserResponse(**feedback_model.model_dump(), user=user_model)
|
||||
)
|
||||
|
||||
return FeedbackListResponse(items=feedbacks, total=total)
|
||||
|
||||
def get_all_feedbacks(self) -> list[FeedbackModel]:
|
||||
with get_db() as db:
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ class GroupForm(BaseModel):
|
|||
name: str
|
||||
description: str
|
||||
permissions: Optional[dict] = None
|
||||
data: Optional[dict] = None
|
||||
|
||||
|
||||
class UserIdsForm(BaseModel):
|
||||
|
|
|
|||
|
|
@ -244,11 +244,9 @@ class ModelsTable:
|
|||
try:
|
||||
with get_db() as db:
|
||||
# update only the fields that are present in the model
|
||||
result = (
|
||||
db.query(Model)
|
||||
.filter_by(id=id)
|
||||
.update(model.model_dump(exclude={"id"}))
|
||||
)
|
||||
data = model.model_dump(exclude={"id"})
|
||||
result = db.query(Model).filter_by(id=id).update(data)
|
||||
|
||||
db.commit()
|
||||
|
||||
model = db.get(Model, id)
|
||||
|
|
|
|||
301
backend/open_webui/retrieval/vector/dbs/weaviate.py
Normal file
301
backend/open_webui/retrieval/vector/dbs/weaviate.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
import weaviate
|
||||
import re
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from open_webui.retrieval.vector.main import (
|
||||
VectorDBBase,
|
||||
VectorItem,
|
||||
SearchResult,
|
||||
GetResult,
|
||||
)
|
||||
from open_webui.retrieval.vector.utils import process_metadata
|
||||
from open_webui.config import WEAVIATE_HTTP_HOST, WEAVIATE_HTTP_PORT, WEAVIATE_GRPC_PORT, WEAVIATE_API_KEY
|
||||
|
||||
|
||||
def _convert_uuids_to_strings(obj: Any) -> Any:
|
||||
"""
|
||||
Recursively convert UUID objects to strings in nested data structures.
|
||||
|
||||
This function handles:
|
||||
- UUID objects -> string
|
||||
- Dictionaries with UUID values
|
||||
- Lists/Tuples with UUID values
|
||||
- Nested combinations of the above
|
||||
|
||||
Args:
|
||||
obj: Any object that might contain UUIDs
|
||||
|
||||
Returns:
|
||||
The same object structure with UUIDs converted to strings
|
||||
"""
|
||||
if isinstance(obj, uuid.UUID):
|
||||
return str(obj)
|
||||
elif isinstance(obj, dict):
|
||||
return {key: _convert_uuids_to_strings(value) for key, value in obj.items()}
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
return type(obj)(_convert_uuids_to_strings(item) for item in obj)
|
||||
elif isinstance(obj, (str, int, float, bool, type(None))):
|
||||
return obj
|
||||
else:
|
||||
return obj
|
||||
|
||||
|
||||
|
||||
|
||||
class WeaviateClient(VectorDBBase):
|
||||
def __init__(self):
|
||||
self.url = WEAVIATE_HTTP_HOST
|
||||
try:
|
||||
# Build connection parameters
|
||||
connection_params = {
|
||||
"host": WEAVIATE_HTTP_HOST,
|
||||
"port": WEAVIATE_HTTP_PORT,
|
||||
"grpc_port": WEAVIATE_GRPC_PORT,
|
||||
}
|
||||
|
||||
# Only add auth_credentials if WEAVIATE_API_KEY exists and is not empty
|
||||
if WEAVIATE_API_KEY:
|
||||
connection_params["auth_credentials"] = weaviate.classes.init.Auth.api_key(WEAVIATE_API_KEY)
|
||||
|
||||
self.client = weaviate.connect_to_local(**connection_params)
|
||||
self.client.connect()
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to connect to Weaviate: {e}") from e
|
||||
|
||||
def _sanitize_collection_name(self, collection_name: str) -> str:
|
||||
"""Sanitize collection name to be a valid Weaviate class name."""
|
||||
if not isinstance(collection_name, str) or not collection_name.strip():
|
||||
raise ValueError("Collection name must be a non-empty string")
|
||||
|
||||
# Requirements for a valid Weaviate class name:
|
||||
# The collection name must begin with a capital letter.
|
||||
# The name can only contain letters, numbers, and the underscore (_) character. Spaces are not allowed.
|
||||
|
||||
# Replace hyphens with underscores and keep only alphanumeric characters
|
||||
name = re.sub(r'[^a-zA-Z0-9_]', '', collection_name.replace("-", "_"))
|
||||
name = name.strip("_")
|
||||
|
||||
if not name:
|
||||
raise ValueError("Could not sanitize collection name to be a valid Weaviate class name")
|
||||
|
||||
# Ensure it starts with a letter and is capitalized
|
||||
if not name[0].isalpha():
|
||||
name = "C" + name
|
||||
|
||||
return name[0].upper() + name[1:]
|
||||
|
||||
def has_collection(self, collection_name: str) -> bool:
|
||||
sane_collection_name = self._sanitize_collection_name(collection_name)
|
||||
return self.client.collections.exists(sane_collection_name)
|
||||
|
||||
def delete_collection(self, collection_name: str) -> None:
|
||||
sane_collection_name = self._sanitize_collection_name(collection_name)
|
||||
if self.client.collections.exists(sane_collection_name):
|
||||
self.client.collections.delete(sane_collection_name)
|
||||
|
||||
def _create_collection(self, collection_name: str) -> None:
|
||||
self.client.collections.create(
|
||||
name=collection_name,
|
||||
vector_config=weaviate.classes.config.Configure.Vectors.self_provided(),
|
||||
properties=[
|
||||
weaviate.classes.config.Property(name="text", data_type=weaviate.classes.config.DataType.TEXT),
|
||||
]
|
||||
)
|
||||
|
||||
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
|
||||
sane_collection_name = self._sanitize_collection_name(collection_name)
|
||||
if not self.client.collections.exists(sane_collection_name):
|
||||
self._create_collection(sane_collection_name)
|
||||
|
||||
collection = self.client.collections.get(sane_collection_name)
|
||||
|
||||
with collection.batch.fixed_size(batch_size=100) as batch:
|
||||
for item in items:
|
||||
item_uuid = str(uuid.uuid4()) if not item["id"] else str(item["id"])
|
||||
|
||||
properties = {"text": item["text"]}
|
||||
if item["metadata"]:
|
||||
clean_metadata = _convert_uuids_to_strings(process_metadata(item["metadata"]))
|
||||
clean_metadata.pop("text", None)
|
||||
properties.update(clean_metadata)
|
||||
|
||||
batch.add_object(
|
||||
properties=properties,
|
||||
uuid=item_uuid,
|
||||
vector=item["vector"]
|
||||
)
|
||||
|
||||
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
|
||||
sane_collection_name = self._sanitize_collection_name(collection_name)
|
||||
if not self.client.collections.exists(sane_collection_name):
|
||||
self._create_collection(sane_collection_name)
|
||||
|
||||
collection = self.client.collections.get(sane_collection_name)
|
||||
|
||||
with collection.batch.fixed_size(batch_size=100) as batch:
|
||||
for item in items:
|
||||
item_uuid = str(item["id"]) if item["id"] else None
|
||||
|
||||
properties = {"text": item["text"]}
|
||||
if item["metadata"]:
|
||||
clean_metadata = _convert_uuids_to_strings(process_metadata(item["metadata"]))
|
||||
clean_metadata.pop("text", None)
|
||||
properties.update(clean_metadata)
|
||||
|
||||
batch.add_object(
|
||||
properties=properties,
|
||||
uuid=item_uuid,
|
||||
vector=item["vector"]
|
||||
)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
|
||||
) -> Optional[SearchResult]:
|
||||
sane_collection_name = self._sanitize_collection_name(collection_name)
|
||||
if not self.client.collections.exists(sane_collection_name):
|
||||
return None
|
||||
|
||||
collection = self.client.collections.get(sane_collection_name)
|
||||
|
||||
result_ids, result_documents, result_metadatas, result_distances = [], [], [], []
|
||||
|
||||
for vector_embedding in vectors:
|
||||
try:
|
||||
response = collection.query.near_vector(
|
||||
near_vector=vector_embedding,
|
||||
limit=limit,
|
||||
return_metadata=weaviate.classes.query.MetadataQuery(distance=True),
|
||||
)
|
||||
|
||||
ids = [str(obj.uuid) for obj in response.objects]
|
||||
documents = []
|
||||
metadatas = []
|
||||
distances = []
|
||||
|
||||
for obj in response.objects:
|
||||
properties = dict(obj.properties) if obj.properties else {}
|
||||
documents.append(properties.pop("text", ""))
|
||||
metadatas.append(_convert_uuids_to_strings(properties))
|
||||
|
||||
# Weaviate has cosine distance, 2 (worst) -> 0 (best). Re-ordering to 0 -> 1
|
||||
raw_distances = [obj.metadata.distance if obj.metadata and obj.metadata.distance else 2.0 for obj in response.objects]
|
||||
distances = [(2 - dist) / 2 for dist in raw_distances]
|
||||
|
||||
result_ids.append(ids)
|
||||
result_documents.append(documents)
|
||||
result_metadatas.append(metadatas)
|
||||
result_distances.append(distances)
|
||||
except Exception:
|
||||
result_ids.append([])
|
||||
result_documents.append([])
|
||||
result_metadatas.append([])
|
||||
result_distances.append([])
|
||||
|
||||
return SearchResult(
|
||||
**{
|
||||
"ids": result_ids,
|
||||
"documents": result_documents,
|
||||
"metadatas": result_metadatas,
|
||||
"distances": result_distances,
|
||||
}
|
||||
)
|
||||
|
||||
def query(
|
||||
self, collection_name: str, filter: Dict, limit: Optional[int] = None
|
||||
) -> Optional[GetResult]:
|
||||
sane_collection_name = self._sanitize_collection_name(collection_name)
|
||||
if not self.client.collections.exists(sane_collection_name):
|
||||
return None
|
||||
|
||||
collection = self.client.collections.get(sane_collection_name)
|
||||
|
||||
weaviate_filter = None
|
||||
if filter:
|
||||
for key, value in filter.items():
|
||||
prop_filter = weaviate.classes.query.Filter.by_property(name=key).equal(value)
|
||||
weaviate_filter = prop_filter if weaviate_filter is None else weaviate.classes.query.Filter.all_of([weaviate_filter, prop_filter])
|
||||
|
||||
try:
|
||||
response = collection.query.fetch_objects(filters=weaviate_filter, limit=limit)
|
||||
|
||||
ids = [str(obj.uuid) for obj in response.objects]
|
||||
documents = []
|
||||
metadatas = []
|
||||
|
||||
for obj in response.objects:
|
||||
properties = dict(obj.properties) if obj.properties else {}
|
||||
documents.append(properties.pop("text", ""))
|
||||
metadatas.append(_convert_uuids_to_strings(properties))
|
||||
|
||||
return GetResult(
|
||||
**{
|
||||
"ids": [ids],
|
||||
"documents": [documents],
|
||||
"metadatas": [metadatas],
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||
sane_collection_name = self._sanitize_collection_name(collection_name)
|
||||
if not self.client.collections.exists(sane_collection_name):
|
||||
return None
|
||||
|
||||
collection = self.client.collections.get(sane_collection_name)
|
||||
ids, documents, metadatas = [], [], []
|
||||
|
||||
try:
|
||||
for item in collection.iterator():
|
||||
ids.append(str(item.uuid))
|
||||
properties = dict(item.properties) if item.properties else {}
|
||||
documents.append(properties.pop("text", ""))
|
||||
metadatas.append(_convert_uuids_to_strings(properties))
|
||||
|
||||
if not ids:
|
||||
return None
|
||||
|
||||
return GetResult(
|
||||
**{
|
||||
"ids": [ids],
|
||||
"documents": [documents],
|
||||
"metadatas": [metadatas],
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def delete(
|
||||
self,
|
||||
collection_name: str,
|
||||
ids: Optional[List[str]] = None,
|
||||
filter: Optional[Dict] = None,
|
||||
) -> None:
|
||||
sane_collection_name = self._sanitize_collection_name(collection_name)
|
||||
if not self.client.collections.exists(sane_collection_name):
|
||||
return
|
||||
|
||||
collection = self.client.collections.get(sane_collection_name)
|
||||
|
||||
try:
|
||||
if ids:
|
||||
for item_id in ids:
|
||||
collection.data.delete_by_id(uuid=item_id)
|
||||
elif filter:
|
||||
weaviate_filter = None
|
||||
for key, value in filter.items():
|
||||
prop_filter = weaviate.classes.query.Filter.by_property(name=key).equal(value)
|
||||
weaviate_filter = prop_filter if weaviate_filter is None else weaviate.classes.query.Filter.all_of([weaviate_filter, prop_filter])
|
||||
|
||||
if weaviate_filter:
|
||||
collection.data.delete_many(where=weaviate_filter)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def reset(self) -> None:
|
||||
try:
|
||||
for collection_name in self.client.collections.list_all().keys():
|
||||
self.client.collections.delete(collection_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -67,6 +67,10 @@ class Vector:
|
|||
from open_webui.retrieval.vector.dbs.oracle23ai import Oracle23aiClient
|
||||
|
||||
return Oracle23aiClient()
|
||||
case VectorType.WEAVIATE:
|
||||
from open_webui.retrieval.vector.dbs.weaviate import WeaviateClient
|
||||
|
||||
return WeaviateClient()
|
||||
case _:
|
||||
raise ValueError(f"Unsupported vector type: {vector_type}")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,3 +11,4 @@ class VectorType(StrEnum):
|
|||
PGVECTOR = "pgvector"
|
||||
ORACLE23AI = "oracle23ai"
|
||||
S3VECTOR = "s3vector"
|
||||
WEAVIATE = "weaviate"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from pydantic import BaseModel
|
|||
|
||||
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.headers import include_user_info_headers
|
||||
from open_webui.config import (
|
||||
WHISPER_MODEL_AUTO_UPDATE,
|
||||
WHISPER_MODEL_DIR,
|
||||
|
|
@ -364,23 +365,17 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||
**(request.app.state.config.TTS_OPENAI_PARAMS or {}),
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {request.app.state.config.TTS_OPENAI_API_KEY}",
|
||||
}
|
||||
if ENABLE_FORWARD_USER_INFO_HEADERS:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
r = await session.post(
|
||||
url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
||||
json=payload,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {request.app.state.config.TTS_OPENAI_API_KEY}",
|
||||
**(
|
||||
{
|
||||
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
|
||||
"X-OpenWebUI-User-Id": user.id,
|
||||
"X-OpenWebUI-User-Email": user.email,
|
||||
"X-OpenWebUI-User-Role": user.role,
|
||||
}
|
||||
if ENABLE_FORWARD_USER_INFO_HEADERS
|
||||
else {}
|
||||
),
|
||||
},
|
||||
headers=headers,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
)
|
||||
|
||||
|
|
@ -570,7 +565,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||
return FileResponse(file_path)
|
||||
|
||||
|
||||
def transcription_handler(request, file_path, metadata):
|
||||
def transcription_handler(request, file_path, metadata, user=None):
|
||||
filename = os.path.basename(file_path)
|
||||
file_dir = os.path.dirname(file_path)
|
||||
id = filename.split(".")[0]
|
||||
|
|
@ -621,11 +616,15 @@ def transcription_handler(request, file_path, metadata):
|
|||
if language:
|
||||
payload["language"] = language
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {request.app.state.config.STT_OPENAI_API_KEY}"
|
||||
}
|
||||
if user and ENABLE_FORWARD_USER_INFO_HEADERS:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
r = requests.post(
|
||||
url=f"{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
|
||||
headers={
|
||||
"Authorization": f"Bearer {request.app.state.config.STT_OPENAI_API_KEY}"
|
||||
},
|
||||
headers=headers,
|
||||
files={"file": (filename, open(file_path, "rb"))},
|
||||
data=payload,
|
||||
)
|
||||
|
|
@ -1027,7 +1026,7 @@ def transcription_handler(request, file_path, metadata):
|
|||
)
|
||||
|
||||
|
||||
def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None):
|
||||
def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None, user=None):
|
||||
log.info(f"transcribe: {file_path} {metadata}")
|
||||
|
||||
if is_audio_conversion_required(file_path):
|
||||
|
|
@ -1054,7 +1053,7 @@ def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None
|
|||
with ThreadPoolExecutor() as executor:
|
||||
# Submit tasks for each chunk_path
|
||||
futures = [
|
||||
executor.submit(transcription_handler, request, chunk_path, metadata)
|
||||
executor.submit(transcription_handler, request, chunk_path, metadata, user)
|
||||
for chunk_path in chunk_paths
|
||||
]
|
||||
# Gather results as they complete
|
||||
|
|
@ -1189,7 +1188,7 @@ def transcription(
|
|||
if language:
|
||||
metadata = {"language": language}
|
||||
|
||||
result = transcribe(request, file_path, metadata)
|
||||
result = transcribe(request, file_path, metadata, user)
|
||||
|
||||
return {
|
||||
**result,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from pydantic import BaseModel
|
|||
|
||||
from open_webui.utils.misc import parse_duration, validate_email_format
|
||||
from open_webui.utils.auth import (
|
||||
validate_password,
|
||||
verify_password,
|
||||
decode_token,
|
||||
invalidate_token,
|
||||
|
|
@ -181,10 +182,14 @@ async def update_password(
|
|||
)
|
||||
|
||||
if user:
|
||||
try:
|
||||
validate_password(form_data.password)
|
||||
except Exception as e:
|
||||
raise HTTPException(400, detail=str(e))
|
||||
hashed = get_password_hash(form_data.new_password)
|
||||
return Auths.update_user_password_by_id(user.id, hashed)
|
||||
else:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_PASSWORD)
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INCORRECT_PASSWORD)
|
||||
else:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
|
||||
|
|
@ -627,16 +632,14 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
try:
|
||||
role = "admin" if not has_users else request.app.state.config.DEFAULT_USER_ROLE
|
||||
|
||||
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
|
||||
if len(form_data.password.encode("utf-8")) > 72:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.PASSWORD_TOO_LONG,
|
||||
)
|
||||
try:
|
||||
validate_password(form_data.password)
|
||||
except Exception as e:
|
||||
raise HTTPException(400, detail=str(e))
|
||||
|
||||
hashed = get_password_hash(form_data.password)
|
||||
|
||||
role = "admin" if not has_users else request.app.state.config.DEFAULT_USER_ROLE
|
||||
user = Auths.insert_new_auth(
|
||||
form_data.email.lower(),
|
||||
hashed,
|
||||
|
|
@ -691,7 +694,11 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
if not has_users:
|
||||
# Disable signup after the first user is created
|
||||
request.app.state.config.ENABLE_SIGNUP = False
|
||||
|
||||
|
||||
default_group_id = getattr(request.app.state.config, 'DEFAULT_GROUP_ID', "")
|
||||
if default_group_id and default_group_id:
|
||||
Groups.add_users_to_group(default_group_id, [user.id])
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"token_type": "Bearer",
|
||||
|
|
@ -805,6 +812,11 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
|
|||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
try:
|
||||
try:
|
||||
validate_password(form_data.password)
|
||||
except Exception as e:
|
||||
raise HTTPException(400, detail=str(e))
|
||||
|
||||
hashed = get_password_hash(form_data.password)
|
||||
user = Auths.insert_new_auth(
|
||||
form_data.email.lower(),
|
||||
|
|
@ -880,6 +892,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
|||
"ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS,
|
||||
"API_KEYS_ALLOWED_ENDPOINTS": request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS,
|
||||
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||
"DEFAULT_GROUP_ID": request.app.state.config.DEFAULT_GROUP_ID,
|
||||
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
|
||||
|
|
@ -900,6 +913,7 @@ class AdminConfig(BaseModel):
|
|||
ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS: bool
|
||||
API_KEYS_ALLOWED_ENDPOINTS: str
|
||||
DEFAULT_USER_ROLE: str
|
||||
DEFAULT_GROUP_ID: str
|
||||
JWT_EXPIRES_IN: str
|
||||
ENABLE_COMMUNITY_SHARING: bool
|
||||
ENABLE_MESSAGE_RATING: bool
|
||||
|
|
@ -914,7 +928,7 @@ class AdminConfig(BaseModel):
|
|||
@router.post("/admin/config")
|
||||
async def update_admin_config(
|
||||
request: Request, form_data: AdminConfig, user=Depends(get_admin_user)
|
||||
):
|
||||
):
|
||||
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
|
||||
request.app.state.config.WEBUI_URL = form_data.WEBUI_URL
|
||||
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
|
||||
|
|
@ -933,6 +947,8 @@ async def update_admin_config(
|
|||
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
|
||||
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
|
||||
|
||||
request.app.state.config.DEFAULT_GROUP_ID = form_data.DEFAULT_GROUP_ID
|
||||
|
||||
pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"
|
||||
|
||||
# Check if the input string matches the pattern
|
||||
|
|
@ -963,6 +979,7 @@ async def update_admin_config(
|
|||
"ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS,
|
||||
"API_KEYS_ALLOWED_ENDPOINTS": request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS,
|
||||
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||
"DEFAULT_GROUP_ID": request.app.state.config.DEFAULT_GROUP_ID,
|
||||
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from open_webui.models.feedbacks import (
|
|||
FeedbackModel,
|
||||
FeedbackResponse,
|
||||
FeedbackForm,
|
||||
FeedbackUserResponse,
|
||||
FeedbackListResponse,
|
||||
Feedbacks,
|
||||
)
|
||||
|
||||
|
|
@ -56,35 +58,10 @@ async def update_config(
|
|||
}
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
role: str = "pending"
|
||||
|
||||
last_active_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
||||
class FeedbackUserResponse(FeedbackResponse):
|
||||
user: Optional[UserResponse] = None
|
||||
|
||||
|
||||
@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse])
|
||||
@router.get("/feedbacks/all", response_model=list[FeedbackResponse])
|
||||
async def get_all_feedbacks(user=Depends(get_admin_user)):
|
||||
feedbacks = Feedbacks.get_all_feedbacks()
|
||||
|
||||
feedback_list = []
|
||||
for feedback in feedbacks:
|
||||
user = Users.get_user_by_id(feedback.user_id)
|
||||
feedback_list.append(
|
||||
FeedbackUserResponse(
|
||||
**feedback.model_dump(),
|
||||
user=UserResponse(**user.model_dump()) if user else None,
|
||||
)
|
||||
)
|
||||
return feedback_list
|
||||
return feedbacks
|
||||
|
||||
|
||||
@router.delete("/feedbacks/all")
|
||||
|
|
@ -111,6 +88,31 @@ async def delete_feedbacks(user=Depends(get_verified_user)):
|
|||
return success
|
||||
|
||||
|
||||
PAGE_ITEM_COUNT = 30
|
||||
|
||||
|
||||
@router.get("/feedbacks/list", response_model=FeedbackListResponse)
|
||||
async def get_feedbacks(
|
||||
order_by: Optional[str] = None,
|
||||
direction: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
limit = PAGE_ITEM_COUNT
|
||||
|
||||
page = max(1, page)
|
||||
skip = (page - 1) * limit
|
||||
|
||||
filter = {}
|
||||
if order_by:
|
||||
filter["order_by"] = order_by
|
||||
if direction:
|
||||
filter["direction"] = direction
|
||||
|
||||
result = Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/feedback", response_model=FeedbackModel)
|
||||
async def create_feedback(
|
||||
request: Request,
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ def process_uploaded_file(request, file, file_path, file_item, file_metadata, us
|
|||
)
|
||||
):
|
||||
file_path = Storage.get_file(file_path)
|
||||
result = transcribe(request, file_path, file_metadata)
|
||||
result = transcribe(request, file_path, file_metadata, user)
|
||||
|
||||
process_file(
|
||||
request,
|
||||
|
|
|
|||
|
|
@ -31,20 +31,32 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.get("/", response_model=list[GroupResponse])
|
||||
async def get_groups(user=Depends(get_verified_user)):
|
||||
async def get_groups(share: Optional[bool] = None, user=Depends(get_verified_user)):
|
||||
if user.role == "admin":
|
||||
groups = Groups.get_groups()
|
||||
else:
|
||||
groups = Groups.get_groups_by_member_id(user.id)
|
||||
|
||||
return [
|
||||
GroupResponse(
|
||||
**group.model_dump(),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id),
|
||||
group_list = []
|
||||
|
||||
for group in groups:
|
||||
if share is not None:
|
||||
# Check if the group has data and a config with share key
|
||||
if (
|
||||
group.data
|
||||
and "share" in group.data.get("config", {})
|
||||
and group.data["config"]["share"] != share
|
||||
):
|
||||
continue
|
||||
|
||||
group_list.append(
|
||||
GroupResponse(
|
||||
**group.model_dump(),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id),
|
||||
)
|
||||
)
|
||||
for group in groups
|
||||
if group
|
||||
]
|
||||
|
||||
return group_list
|
||||
|
||||
|
||||
############################
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ class ImagesConfig(BaseModel):
|
|||
IMAGES_GEMINI_API_KEY: str
|
||||
IMAGES_GEMINI_ENDPOINT_METHOD: str
|
||||
|
||||
ENABLE_IMAGE_EDIT: bool
|
||||
IMAGE_EDIT_ENGINE: str
|
||||
IMAGE_EDIT_MODEL: str
|
||||
IMAGE_EDIT_SIZE: Optional[str]
|
||||
|
|
@ -164,6 +165,7 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
|
|||
"IMAGES_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_GEMINI_API_BASE_URL,
|
||||
"IMAGES_GEMINI_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY,
|
||||
"IMAGES_GEMINI_ENDPOINT_METHOD": request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD,
|
||||
"ENABLE_IMAGE_EDIT": request.app.state.config.ENABLE_IMAGE_EDIT,
|
||||
"IMAGE_EDIT_ENGINE": request.app.state.config.IMAGE_EDIT_ENGINE,
|
||||
"IMAGE_EDIT_MODEL": request.app.state.config.IMAGE_EDIT_MODEL,
|
||||
"IMAGE_EDIT_SIZE": request.app.state.config.IMAGE_EDIT_SIZE,
|
||||
|
|
@ -253,6 +255,7 @@ async def update_config(
|
|||
)
|
||||
|
||||
# Edit Image
|
||||
request.app.state.config.ENABLE_IMAGE_EDIT = form_data.ENABLE_IMAGE_EDIT
|
||||
request.app.state.config.IMAGE_EDIT_ENGINE = form_data.IMAGE_EDIT_ENGINE
|
||||
request.app.state.config.IMAGE_EDIT_MODEL = form_data.IMAGE_EDIT_MODEL
|
||||
request.app.state.config.IMAGE_EDIT_SIZE = form_data.IMAGE_EDIT_SIZE
|
||||
|
|
@ -308,6 +311,7 @@ async def update_config(
|
|||
"IMAGES_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_GEMINI_API_BASE_URL,
|
||||
"IMAGES_GEMINI_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY,
|
||||
"IMAGES_GEMINI_ENDPOINT_METHOD": request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD,
|
||||
"ENABLE_IMAGE_EDIT": request.app.state.config.ENABLE_IMAGE_EDIT,
|
||||
"IMAGE_EDIT_ENGINE": request.app.state.config.IMAGE_EDIT_ENGINE,
|
||||
"IMAGE_EDIT_MODEL": request.app.state.config.IMAGE_EDIT_MODEL,
|
||||
"IMAGE_EDIT_SIZE": request.app.state.config.IMAGE_EDIT_SIZE,
|
||||
|
|
|
|||
|
|
@ -253,6 +253,7 @@ async def get_model_profile_image(id: str, user=Depends(get_verified_user)):
|
|||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return FileResponse(f"{STATIC_DIR}/favicon.png")
|
||||
else:
|
||||
return FileResponse(f"{STATIC_DIR}/favicon.png")
|
||||
|
|
@ -320,7 +321,7 @@ async def update_model_by_id(
|
|||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
model = Models.update_model_by_id(form_data.id, form_data)
|
||||
model = Models.update_model_by_id(form_data.id, ModelForm(**form_data.model_dump()))
|
||||
return model
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,12 @@ from open_webui.constants import ERROR_MESSAGES
|
|||
from open_webui.env import SRC_LOG_LEVELS, STATIC_DIR
|
||||
|
||||
|
||||
from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user
|
||||
from open_webui.utils.auth import (
|
||||
get_admin_user,
|
||||
get_password_hash,
|
||||
get_verified_user,
|
||||
validate_password,
|
||||
)
|
||||
from open_webui.utils.access_control import get_permissions, has_permission
|
||||
|
||||
|
||||
|
|
@ -178,10 +183,15 @@ class WorkspacePermissions(BaseModel):
|
|||
|
||||
|
||||
class SharingPermissions(BaseModel):
|
||||
public_models: bool = True
|
||||
public_knowledge: bool = True
|
||||
public_prompts: bool = True
|
||||
models: bool = False
|
||||
public_models: bool = False
|
||||
knowledge: bool = False
|
||||
public_knowledge: bool = False
|
||||
prompts: bool = False
|
||||
public_prompts: bool = False
|
||||
tools: bool = False
|
||||
public_tools: bool = True
|
||||
notes: bool = False
|
||||
public_notes: bool = True
|
||||
|
||||
|
||||
|
|
@ -497,8 +507,12 @@ async def update_user_by_id(
|
|||
)
|
||||
|
||||
if form_data.password:
|
||||
try:
|
||||
validate_password(form_data.password)
|
||||
except Exception as e:
|
||||
raise HTTPException(400, detail=str(e))
|
||||
|
||||
hashed = get_password_hash(form_data.password)
|
||||
log.debug(f"hashed: {hashed}")
|
||||
Auths.update_user_password_by_id(user_id, hashed)
|
||||
|
||||
Auths.update_email_by_id(user_id, form_data.email.lower())
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ from open_webui.models.users import Users
|
|||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
||||
from open_webui.env import (
|
||||
ENABLE_PASSWORD_VALIDATION,
|
||||
OFFLINE_MODE,
|
||||
LICENSE_BLOB,
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN,
|
||||
REDIS_KEY_PREFIX,
|
||||
pk,
|
||||
WEBUI_SECRET_KEY,
|
||||
|
|
@ -162,6 +164,20 @@ def get_password_hash(password: str) -> str:
|
|||
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
|
||||
|
||||
def validate_password(password: str) -> bool:
|
||||
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
|
||||
if len(password.encode("utf-8")) > 72:
|
||||
raise Exception(
|
||||
ERROR_MESSAGES.PASSWORD_TOO_LONG,
|
||||
)
|
||||
|
||||
if ENABLE_PASSWORD_VALIDATION:
|
||||
if not PASSWORD_VALIDATION_REGEX_PATTERN.match(password):
|
||||
raise Exception(ERROR_MESSAGES.INVALID_PASSWORD())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Verify a password against its hash"""
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -791,42 +791,13 @@ async def chat_image_generation_handler(
|
|||
input_images = get_last_images(message_list)
|
||||
|
||||
system_message_content = ""
|
||||
if len(input_images) == 0:
|
||||
# Create image(s)
|
||||
if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION:
|
||||
try:
|
||||
res = await generate_image_prompt(
|
||||
request,
|
||||
{
|
||||
"model": form_data["model"],
|
||||
"messages": form_data["messages"],
|
||||
},
|
||||
user,
|
||||
)
|
||||
|
||||
response = res["choices"][0]["message"]["content"]
|
||||
|
||||
try:
|
||||
bracket_start = response.find("{")
|
||||
bracket_end = response.rfind("}") + 1
|
||||
|
||||
if bracket_start == -1 or bracket_end == -1:
|
||||
raise Exception("No JSON object found in the response")
|
||||
|
||||
response = response[bracket_start:bracket_end]
|
||||
response = json.loads(response)
|
||||
prompt = response.get("prompt", [])
|
||||
except Exception as e:
|
||||
prompt = user_message
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
prompt = user_message
|
||||
|
||||
if len(input_images) > 0 and request.app.state.config.ENABLE_IMAGE_EDIT:
|
||||
# Edit image(s)
|
||||
try:
|
||||
images = await image_generations(
|
||||
images = await image_edits(
|
||||
request=request,
|
||||
form_data=CreateImageForm(**{"prompt": prompt}),
|
||||
form_data=EditImageForm(**{"prompt": prompt, "image": input_images}),
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
|
@ -874,12 +845,43 @@ async def chat_image_generation_handler(
|
|||
)
|
||||
|
||||
system_message_content = f"<context>Image generation was attempted but failed. The system is currently unable to generate the image. Tell the user that an error occurred: {error_message}</context>"
|
||||
|
||||
else:
|
||||
# Edit image(s)
|
||||
# Create image(s)
|
||||
if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION:
|
||||
try:
|
||||
res = await generate_image_prompt(
|
||||
request,
|
||||
{
|
||||
"model": form_data["model"],
|
||||
"messages": form_data["messages"],
|
||||
},
|
||||
user,
|
||||
)
|
||||
|
||||
response = res["choices"][0]["message"]["content"]
|
||||
|
||||
try:
|
||||
bracket_start = response.find("{")
|
||||
bracket_end = response.rfind("}") + 1
|
||||
|
||||
if bracket_start == -1 or bracket_end == -1:
|
||||
raise Exception("No JSON object found in the response")
|
||||
|
||||
response = response[bracket_start:bracket_end]
|
||||
response = json.loads(response)
|
||||
prompt = response.get("prompt", [])
|
||||
except Exception as e:
|
||||
prompt = user_message
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
prompt = user_message
|
||||
|
||||
try:
|
||||
images = await image_edits(
|
||||
images = await image_generations(
|
||||
request=request,
|
||||
form_data=EditImageForm(**{"prompt": prompt, "image": input_images}),
|
||||
form_data=CreateImageForm(**{"prompt": prompt}),
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ langchain-community==0.3.29
|
|||
|
||||
fake-useragent==2.2.0
|
||||
chromadb==1.1.0
|
||||
weaviate-client==4.17.0
|
||||
opensearch-py==2.8.0
|
||||
|
||||
transformers
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ all = [
|
|||
"elasticsearch==9.1.0",
|
||||
|
||||
"qdrant-client==1.14.3",
|
||||
"weaviate-client==4.17.0",
|
||||
"pymilvus==2.6.2",
|
||||
"pinecone==6.0.2",
|
||||
"oracledb==3.2.0",
|
||||
|
|
|
|||
|
|
@ -93,6 +93,45 @@ export const getAllFeedbacks = async (token: string = '') => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getFeedbackItems = async (token: string = '', orderBy, direction, page) => {
|
||||
let error = null;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (orderBy) searchParams.append('order_by', orderBy);
|
||||
if (direction) searchParams.append('direction', direction);
|
||||
if (page) searchParams.append('page', page.toString());
|
||||
|
||||
const res = await fetch(
|
||||
`${WEBUI_API_BASE_URL}/evaluations/feedbacks/list?${searchParams.toString()}`,
|
||||
{
|
||||
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.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const exportAllFeedbacks = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,15 @@ export const createNewGroup = async (token: string, group: object) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getGroups = async (token: string = '') => {
|
||||
export const getGroups = async (token: string = '', share?: boolean) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/groups/`, {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (share !== undefined) {
|
||||
searchParams.append('share', String(share));
|
||||
}
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/groups/?${searchParams.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
|
|||
|
|
@ -1425,7 +1425,7 @@ export const getVersion = async (token: string) => {
|
|||
throw error;
|
||||
}
|
||||
|
||||
return res?.version ?? null;
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getVersionUpdates = async (token: string) => {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@
|
|||
let feedbacks = [];
|
||||
|
||||
onMount(async () => {
|
||||
// TODO: feedbacks elo rating calculation should be done in the backend; remove below line later
|
||||
feedbacks = await getAllFeedbacks(localStorage.token);
|
||||
|
||||
loaded = true;
|
||||
|
||||
const containerElement = document.getElementById('users-tabs-container');
|
||||
|
|
@ -117,7 +119,7 @@
|
|||
{#if selectedTab === 'leaderboard'}
|
||||
<Leaderboard {feedbacks} />
|
||||
{:else if selectedTab === 'feedbacks'}
|
||||
<Feedbacks {feedbacks} />
|
||||
<Feedbacks />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import { onMount, getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { deleteFeedbackById, exportAllFeedbacks, getAllFeedbacks } from '$lib/apis/evaluations';
|
||||
import { deleteFeedbackById, exportAllFeedbacks, getFeedbackItems } from '$lib/apis/evaluations';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Download from '$lib/components/icons/Download.svelte';
|
||||
|
|
@ -23,78 +23,25 @@
|
|||
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { config } from '$lib/stores';
|
||||
|
||||
export let feedbacks = [];
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
let page = 1;
|
||||
$: paginatedFeedbacks = sortedFeedbacks.slice((page - 1) * 10, page * 10);
|
||||
let items = null;
|
||||
let total = null;
|
||||
|
||||
let orderBy: string = 'updated_at';
|
||||
let direction: 'asc' | 'desc' = 'desc';
|
||||
|
||||
type Feedback = {
|
||||
id: string;
|
||||
data: {
|
||||
rating: number;
|
||||
model_id: string;
|
||||
sibling_model_ids: string[] | null;
|
||||
reason: string;
|
||||
comment: string;
|
||||
tags: string[];
|
||||
};
|
||||
user: {
|
||||
name: string;
|
||||
profile_image_url: string;
|
||||
};
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type ModelStats = {
|
||||
rating: number;
|
||||
won: number;
|
||||
lost: number;
|
||||
};
|
||||
|
||||
function setSortKey(key: string) {
|
||||
const setSortKey = (key) => {
|
||||
if (orderBy === key) {
|
||||
direction = direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
orderBy = key;
|
||||
if (key === 'user' || key === 'model_id') {
|
||||
direction = 'asc';
|
||||
} else {
|
||||
direction = 'desc';
|
||||
}
|
||||
direction = 'asc';
|
||||
}
|
||||
page = 1;
|
||||
}
|
||||
|
||||
$: sortedFeedbacks = [...feedbacks].sort((a, b) => {
|
||||
let aVal, bVal;
|
||||
|
||||
switch (orderBy) {
|
||||
case 'user':
|
||||
aVal = a.user?.name || '';
|
||||
bVal = b.user?.name || '';
|
||||
return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
||||
case 'model_id':
|
||||
aVal = a.data.model_id || '';
|
||||
bVal = b.data.model_id || '';
|
||||
return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
||||
case 'rating':
|
||||
aVal = a.data.rating;
|
||||
bVal = b.data.rating;
|
||||
return direction === 'asc' ? aVal - bVal : bVal - aVal;
|
||||
case 'updated_at':
|
||||
aVal = a.updated_at;
|
||||
bVal = b.updated_at;
|
||||
return direction === 'asc' ? aVal - bVal : bVal - aVal;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let showFeedbackModal = false;
|
||||
let selectedFeedback = null;
|
||||
|
|
@ -115,13 +62,41 @@
|
|||
//
|
||||
//////////////////////
|
||||
|
||||
const getFeedbacks = async () => {
|
||||
try {
|
||||
const res = await getFeedbackItems(localStorage.token, orderBy, direction, page).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
items = res.items;
|
||||
total = res.total;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (page) {
|
||||
getFeedbacks();
|
||||
}
|
||||
|
||||
$: if (orderBy && direction) {
|
||||
getFeedbacks();
|
||||
}
|
||||
|
||||
const deleteFeedbackHandler = async (feedbackId: string) => {
|
||||
const response = await deleteFeedbackById(localStorage.token, feedbackId).catch((err) => {
|
||||
toast.error(err);
|
||||
return null;
|
||||
});
|
||||
if (response) {
|
||||
feedbacks = feedbacks.filter((f) => f.id !== feedbackId);
|
||||
toast.success($i18n.t('Feedback deleted successfully'));
|
||||
page = 1;
|
||||
getFeedbacks();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -169,256 +144,266 @@
|
|||
|
||||
<FeedbackModal bind:show={showFeedbackModal} {selectedFeedback} onClose={closeFeedbackModal} />
|
||||
|
||||
<div class="mt-0.5 mb-1 gap-1 flex flex-row justify-between">
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Feedback History')}
|
||||
{#if items === null || total === null}
|
||||
<div class="my-10">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-0.5 mb-1 gap-1 flex flex-row justify-between">
|
||||
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
|
||||
<div>
|
||||
{$i18n.t('Feedback History')}
|
||||
</div>
|
||||
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
|
||||
{total}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{feedbacks.length}</span>
|
||||
{#if total > 0}
|
||||
<div>
|
||||
<Tooltip content={$i18n.t('Export')}>
|
||||
<button
|
||||
class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
|
||||
on:click={() => {
|
||||
exportHandler();
|
||||
}}
|
||||
>
|
||||
<Download className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if feedbacks.length > 0}
|
||||
<div>
|
||||
<Tooltip content={$i18n.t('Export')}>
|
||||
<button
|
||||
class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
|
||||
on:click={() => {
|
||||
exportHandler();
|
||||
}}
|
||||
>
|
||||
<Download className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||
{#if (feedbacks ?? []).length === 0}
|
||||
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
|
||||
{$i18n.t('No feedbacks found')}
|
||||
</div>
|
||||
{:else}
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full">
|
||||
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
|
||||
<th
|
||||
scope="col"
|
||||
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
||||
on:click={() => setSortKey('user')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('User')}
|
||||
{#if orderBy === 'user'}
|
||||
<span class="font-normal">
|
||||
{#if direction === 'asc'}
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||
{#if (items ?? []).length === 0}
|
||||
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
|
||||
{$i18n.t('No feedbacks found')}
|
||||
</div>
|
||||
{:else}
|
||||
<table
|
||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full"
|
||||
>
|
||||
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
|
||||
<th
|
||||
scope="col"
|
||||
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
||||
on:click={() => setSortKey('user')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('User')}
|
||||
{#if orderBy === 'user'}
|
||||
<span class="font-normal">
|
||||
{#if direction === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="invisible">
|
||||
<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-2.5 py-2 cursor-pointer select-none"
|
||||
on:click={() => setSortKey('model_id')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center">
|
||||
{$i18n.t('Models')}
|
||||
{#if orderBy === 'model_id'}
|
||||
<span class="font-normal">
|
||||
{#if direction === '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-2.5 py-2 text-right cursor-pointer select-none w-fit"
|
||||
on:click={() => setSortKey('rating')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Result')}
|
||||
{#if orderBy === 'rating'}
|
||||
<span class="font-normal">
|
||||
{#if direction === '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-2.5 py-2 text-right cursor-pointer select-none w-0"
|
||||
on:click={() => setSortKey('updated_at')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Updated At')}
|
||||
{#if orderBy === 'updated_at'}
|
||||
<span class="font-normal">
|
||||
{#if direction === '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-2.5 py-2 text-right cursor-pointer select-none w-0"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="">
|
||||
{#each paginatedFeedbacks as feedback (feedback.id)}
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
on:click={() => openFeedbackModal(feedback)}
|
||||
>
|
||||
<td class=" py-0.5 text-right font-semibold">
|
||||
<div class="flex justify-center">
|
||||
<Tooltip content={feedback?.user?.name}>
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
src={feedback?.user?.profile_image_url ?? `${WEBUI_BASE_URL}/user.png`}
|
||||
alt={feedback?.user?.name}
|
||||
class="size-5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</th>
|
||||
|
||||
<td class=" py-1 pl-3 flex flex-col">
|
||||
<div class="flex flex-col items-start gap-0.5 h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
{#if feedback.data?.sibling_model_ids}
|
||||
<div class="font-semibold text-gray-600 dark:text-gray-400 flex-1">
|
||||
{feedback.data?.model_id}
|
||||
</div>
|
||||
|
||||
<Tooltip content={feedback.data.sibling_model_ids.join(', ')}>
|
||||
<div class=" text-[0.65rem] text-gray-600 dark:text-gray-400 line-clamp-1">
|
||||
{#if feedback.data.sibling_model_ids.length > 2}
|
||||
<!-- {$i18n.t('and {{COUNT}} more')} -->
|
||||
{feedback.data.sibling_model_ids.slice(0, 2).join(', ')}, {$i18n.t(
|
||||
'and {{COUNT}} more',
|
||||
{ COUNT: feedback.data.sibling_model_ids.length - 2 }
|
||||
)}
|
||||
{:else}
|
||||
{feedback.data.sibling_model_ids.join(', ')}
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<div
|
||||
class=" text-sm font-medium text-gray-600 dark:text-gray-400 flex-1 py-1.5"
|
||||
>
|
||||
{feedback.data?.model_id}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-2.5 py-2 cursor-pointer select-none"
|
||||
on:click={() => setSortKey('model_id')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center">
|
||||
{$i18n.t('Models')}
|
||||
{#if orderBy === 'model_id'}
|
||||
<span class="font-normal">
|
||||
{#if direction === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="invisible">
|
||||
<ChevronUp className="size-2" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</th>
|
||||
|
||||
{#if feedback?.data?.rating}
|
||||
<td class="px-3 py-1 text-right font-medium text-gray-900 dark:text-white w-max">
|
||||
<div class=" flex justify-end">
|
||||
{#if feedback?.data?.rating.toString() === '1'}
|
||||
<Badge type="info" content={$i18n.t('Won')} />
|
||||
{:else if feedback?.data?.rating.toString() === '0'}
|
||||
<Badge type="muted" content={$i18n.t('Draw')} />
|
||||
{:else if feedback?.data?.rating.toString() === '-1'}
|
||||
<Badge type="error" content={$i18n.t('Lost')} />
|
||||
{/if}
|
||||
<th
|
||||
scope="col"
|
||||
class="px-2.5 py-2 text-right cursor-pointer select-none w-fit"
|
||||
on:click={() => setSortKey('rating')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Result')}
|
||||
{#if orderBy === 'rating'}
|
||||
<span class="font-normal">
|
||||
{#if direction === '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-2.5 py-2 text-right cursor-pointer select-none w-0"
|
||||
on:click={() => setSortKey('updated_at')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Updated At')}
|
||||
{#if orderBy === 'updated_at'}
|
||||
<span class="font-normal">
|
||||
{#if direction === '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-2.5 py-2 text-right cursor-pointer select-none w-0"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="">
|
||||
{#each items as feedback (feedback.id)}
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
on:click={() => openFeedbackModal(feedback)}
|
||||
>
|
||||
<td class=" py-0.5 text-right font-semibold">
|
||||
<div class="flex justify-center">
|
||||
<Tooltip content={feedback?.user?.name}>
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
src={`${WEBUI_API_BASE_URL}/users/${feedback.user.id}/profile/image`}
|
||||
alt={feedback?.user?.name}
|
||||
class="size-5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
<td class=" px-3 py-1 text-right font-medium">
|
||||
{dayjs(feedback.updated_at * 1000).fromNow()}
|
||||
</td>
|
||||
<td class=" py-1 pl-3 flex flex-col">
|
||||
<div class="flex flex-col items-start gap-0.5 h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
{#if feedback.data?.sibling_model_ids}
|
||||
<div class="font-semibold text-gray-600 dark:text-gray-400 flex-1">
|
||||
{feedback.data?.model_id}
|
||||
</div>
|
||||
|
||||
<td class=" px-3 py-1 text-right font-semibold" on:click={(e) => e.stopPropagation()}>
|
||||
<FeedbackMenu
|
||||
on:delete={(e) => {
|
||||
deleteFeedbackHandler(feedback.id);
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
<Tooltip content={feedback.data.sibling_model_ids.join(', ')}>
|
||||
<div class=" text-[0.65rem] text-gray-600 dark:text-gray-400 line-clamp-1">
|
||||
{#if feedback.data.sibling_model_ids.length > 2}
|
||||
<!-- {$i18n.t('and {{COUNT}} more')} -->
|
||||
{feedback.data.sibling_model_ids.slice(0, 2).join(', ')}, {$i18n.t(
|
||||
'and {{COUNT}} more',
|
||||
{ COUNT: feedback.data.sibling_model_ids.length - 2 }
|
||||
)}
|
||||
{:else}
|
||||
{feedback.data.sibling_model_ids.join(', ')}
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<div
|
||||
class=" text-sm font-medium text-gray-600 dark:text-gray-400 flex-1 py-1.5"
|
||||
>
|
||||
{feedback.data?.model_id}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{#if feedback?.data?.rating}
|
||||
<td class="px-3 py-1 text-right font-medium text-gray-900 dark:text-white w-max">
|
||||
<div class=" flex justify-end">
|
||||
{#if feedback?.data?.rating.toString() === '1'}
|
||||
<Badge type="info" content={$i18n.t('Won')} />
|
||||
{:else if feedback?.data?.rating.toString() === '0'}
|
||||
<Badge type="muted" content={$i18n.t('Draw')} />
|
||||
{:else if feedback?.data?.rating.toString() === '-1'}
|
||||
<Badge type="error" content={$i18n.t('Lost')} />
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
<td class=" px-3 py-1 text-right font-medium">
|
||||
{dayjs(feedback.updated_at * 1000).fromNow()}
|
||||
</td>
|
||||
|
||||
<td class=" px-3 py-1 text-right font-semibold" on:click={(e) => e.stopPropagation()}>
|
||||
<FeedbackMenu
|
||||
on:delete={(e) => {
|
||||
deleteFeedbackHandler(feedback.id);
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal />
|
||||
</button>
|
||||
</FeedbackMenu>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if feedbacks.length > 0 && $config?.features?.enable_community_sharing}
|
||||
<div class=" flex flex-col justify-end w-full text-right gap-1">
|
||||
<div class="line-clamp-1 text-gray-500 text-xs">
|
||||
{$i18n.t('Help us create the best community leaderboard by sharing your feedback history!')}
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 ml-auto">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
class="flex text-xs items-center px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
|
||||
on:click={async () => {
|
||||
shareHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium line-clamp-1">
|
||||
{$i18n.t('Share to Open WebUI Community')}
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<CloudArrowUp className="size-3" strokeWidth="3" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
<EllipsisHorizontal />
|
||||
</button>
|
||||
</FeedbackMenu>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if feedbacks.length > 10}
|
||||
<Pagination bind:page count={feedbacks.length} perPage={10} />
|
||||
{#if total > 0 && $config?.features?.enable_community_sharing}
|
||||
<div class=" flex flex-col justify-end w-full text-right gap-1">
|
||||
<div class="line-clamp-1 text-gray-500 text-xs">
|
||||
{$i18n.t('Help us create the best community leaderboard by sharing your feedback history!')}
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 ml-auto">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
class="flex text-xs items-center px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
|
||||
on:click={async () => {
|
||||
shareHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium line-clamp-1">
|
||||
{$i18n.t('Share to Open WebUI Community')}
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<CloudArrowUp className="size-3" strokeWidth="3" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if total > 30}
|
||||
<Pagination bind:page count={total} perPage={30} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -339,16 +339,14 @@
|
|||
<div
|
||||
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
||||
>
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5 shrink-0 items-center">
|
||||
<div class=" gap-1">
|
||||
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
|
||||
<div>
|
||||
{$i18n.t('Leaderboard')}
|
||||
</div>
|
||||
|
||||
<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 mr-1.5"
|
||||
>{rankedModels.length}</span
|
||||
>
|
||||
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
|
||||
{rankedModels.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex space-x-2">
|
||||
|
|
@ -517,7 +515,7 @@
|
|||
<div class="flex items-center gap-2">
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
src={model?.info?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}`}
|
||||
alt={model.name}
|
||||
class="size-5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -757,18 +757,18 @@
|
|||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-between w-full mt-2">
|
||||
<div class="self-center text-xs font-medium">
|
||||
<Tooltip content={''} placement="top-start">
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
<div class=" flex flex-col w-full justify-between">
|
||||
<div class=" mb-1 text-xs font-medium">
|
||||
{$i18n.t('Parameters')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="">
|
||||
<Textarea
|
||||
bind:value={RAGConfig.DOCLING_PARAMS}
|
||||
placeholder={$i18n.t('Enter additional parameters in JSON format')}
|
||||
minSize={100}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex w-full items-center relative">
|
||||
<Textarea
|
||||
bind:value={RAGConfig.DOCLING_PARAMS}
|
||||
placeholder={$i18n.t('Enter additional parameters in JSON format')}
|
||||
minSize={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||
import ArenaModelModal from './ArenaModelModal.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
export let model;
|
||||
|
||||
let showModel = false;
|
||||
|
|
@ -27,7 +28,7 @@
|
|||
<div class="flex flex-col flex-1">
|
||||
<div class="flex gap-2.5 items-center">
|
||||
<img
|
||||
src={model.meta.profile_image_url}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}`}
|
||||
alt={model.name}
|
||||
class="size-8 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
updateLdapConfig,
|
||||
updateLdapServer
|
||||
} from '$lib/apis/auths';
|
||||
import { getGroups } from '$lib/apis/groups';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
|
@ -32,6 +33,7 @@
|
|||
|
||||
let adminConfig = null;
|
||||
let webhookUrl = '';
|
||||
let groups = [];
|
||||
|
||||
// LDAP
|
||||
let ENABLE_LDAP = false;
|
||||
|
|
@ -104,6 +106,9 @@
|
|||
})(),
|
||||
(async () => {
|
||||
LDAP_SERVER = await getLdapServer(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
groups = await getGroups(localStorage.token);
|
||||
})()
|
||||
]);
|
||||
|
||||
|
|
@ -299,6 +304,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" mb-2.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Default Group')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={adminConfig.DEFAULT_GROUP_ID}
|
||||
placeholder={$i18n.t('Select a group')}
|
||||
>
|
||||
<option value={""}>None</option>
|
||||
{#each groups as group}
|
||||
<option value={group.id}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" mb-2.5 flex w-full justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -888,23 +888,15 @@
|
|||
<div class="flex w-full justify-between items-center">
|
||||
<div class="text-xs pr-2">
|
||||
<div class="">
|
||||
{$i18n.t('Image Edit Engine')}
|
||||
{$i18n.t('Image Edit')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select
|
||||
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={config.IMAGE_EDIT_ENGINE}
|
||||
placeholder={$i18n.t('Select Engine')}
|
||||
>
|
||||
<option value="openai">{$i18n.t('Default (Open AI)')}</option>
|
||||
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
||||
<option value="gemini">{$i18n.t('Gemini')}</option>
|
||||
</select>
|
||||
<Switch bind:state={config.ENABLE_IMAGE_EDIT} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config.ENABLE_IMAGE_GENERATION}
|
||||
{#if config?.ENABLE_IMAGE_GENERATION && config?.ENABLE_IMAGE_EDIT}
|
||||
<div class="mb-2.5">
|
||||
<div class="flex w-full justify-between items-center">
|
||||
<div class="text-xs pr-2">
|
||||
|
|
@ -949,6 +941,26 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-2.5">
|
||||
<div class="flex w-full justify-between items-center">
|
||||
<div class="text-xs pr-2">
|
||||
<div class="">
|
||||
{$i18n.t('Image Edit Engine')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select
|
||||
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={config.IMAGE_EDIT_ENGINE}
|
||||
placeholder={$i18n.t('Select Engine')}
|
||||
>
|
||||
<option value="openai">{$i18n.t('Default (Open AI)')}</option>
|
||||
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
||||
<option value="gemini">{$i18n.t('Gemini')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config?.IMAGE_EDIT_ENGINE === 'openai'}
|
||||
<div class="mb-2.5">
|
||||
<div class="flex w-full justify-between items-center">
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
|
||||
import Eye from '$lib/components/icons/Eye.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
let shiftKey = false;
|
||||
|
||||
|
|
@ -334,7 +334,7 @@
|
|||
: 'opacity-50 dark:opacity-50'} "
|
||||
>
|
||||
<img
|
||||
src={model?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}`}
|
||||
alt="modelfile profile"
|
||||
class=" rounded-full w-full h-auto object-cover"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -105,11 +105,14 @@
|
|||
/>
|
||||
|
||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Groups')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
|
||||
<div>
|
||||
{$i18n.t('Groups')}
|
||||
</div>
|
||||
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{groups.length}</span>
|
||||
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
|
||||
{groups.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Display from './Display.svelte';
|
||||
import General from './General.svelte';
|
||||
import Permissions from './Permissions.svelte';
|
||||
import Users from './Users.svelte';
|
||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
export let name = '';
|
||||
export let description = '';
|
||||
export let data = {};
|
||||
|
||||
export let permissions = {
|
||||
workspace: {
|
||||
|
|
@ -49,10 +50,15 @@
|
|||
tools_export: false
|
||||
},
|
||||
sharing: {
|
||||
models: false,
|
||||
public_models: false,
|
||||
knowledge: false,
|
||||
public_knowledge: false,
|
||||
prompts: false,
|
||||
public_prompts: false,
|
||||
tools: false,
|
||||
public_tools: false,
|
||||
notes: false,
|
||||
public_notes: false
|
||||
},
|
||||
chat: {
|
||||
|
|
@ -92,6 +98,7 @@
|
|||
const group = {
|
||||
name,
|
||||
description,
|
||||
data,
|
||||
permissions
|
||||
};
|
||||
|
||||
|
|
@ -106,6 +113,7 @@
|
|||
name = group.name;
|
||||
description = group.description;
|
||||
permissions = group?.permissions ?? {};
|
||||
data = group?.data ?? {};
|
||||
|
||||
userCount = group?.member_count ?? 0;
|
||||
}
|
||||
|
|
@ -236,9 +244,10 @@
|
|||
<div class="flex-1 mt-1 lg:mt-1 lg:h-[30rem] lg:max-h-[30rem] flex flex-col">
|
||||
<div class="w-full h-full overflow-y-auto scrollbar-hidden">
|
||||
{#if selectedTab == 'general'}
|
||||
<Display
|
||||
<General
|
||||
bind:name
|
||||
bind:description
|
||||
bind:data
|
||||
{edit}
|
||||
onDelete={() => {
|
||||
showDeleteConfirmDialog = true;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
import { getContext } from 'svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let name = '';
|
||||
export let color = '';
|
||||
export let description = '';
|
||||
export let data = {};
|
||||
|
||||
export let edit = false;
|
||||
export let onDelete: Function = () => {};
|
||||
|
|
@ -63,6 +65,34 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-850 my-1" />
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Setting')}</div>
|
||||
|
||||
<div>
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Allow Group Sharing')}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 p-1">
|
||||
<Switch
|
||||
tooltip={true}
|
||||
state={data?.config?.share ?? true}
|
||||
on:change={(e) => {
|
||||
if (data?.config?.share) {
|
||||
data.config.share = e.detail;
|
||||
} else {
|
||||
data.config = { ...(data?.config ?? {}), share: e.detail };
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if edit}
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Actions')}</div>
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
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';
|
||||
import EditGroupModal from './EditGroupModal.svelte';
|
||||
|
||||
export let group = {
|
||||
name: 'Admins',
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<GroupModal
|
||||
<EditGroupModal
|
||||
bind:show={showEdit}
|
||||
edit
|
||||
{group}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,15 @@
|
|||
tools_export: false
|
||||
},
|
||||
sharing: {
|
||||
models: false,
|
||||
public_models: false,
|
||||
knowledge: false,
|
||||
public_knowledge: false,
|
||||
prompts: false,
|
||||
public_prompts: false,
|
||||
tools: false,
|
||||
public_tools: false,
|
||||
notes: false,
|
||||
public_notes: false
|
||||
},
|
||||
chat: {
|
||||
|
|
@ -217,11 +222,11 @@
|
|||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Models Public Sharing')}
|
||||
{$i18n.t('Models Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_models} />
|
||||
<Switch bind:state={permissions.sharing.models} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_models && !permissions.sharing.public_models}
|
||||
{#if defaultPermissions?.sharing?.models && !permissions.sharing.models}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
|
|
@ -230,14 +235,32 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if permissions.sharing.models}
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Models Public Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_models} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_models && !permissions.sharing.public_models}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Knowledge Public Sharing')}
|
||||
{$i18n.t('Knowledge Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_knowledge} />
|
||||
<Switch bind:state={permissions.sharing.knowledge} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_knowledge && !permissions.sharing.public_knowledge}
|
||||
{#if defaultPermissions?.sharing?.knowledge && !permissions.sharing.knowledge}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
|
|
@ -246,14 +269,32 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if permissions.sharing.knowledge}
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Knowledge Public Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_knowledge} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_knowledge && !permissions.sharing.public_knowledge}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Prompts Public Sharing')}
|
||||
{$i18n.t('Prompts Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_prompts} />
|
||||
<Switch bind:state={permissions.sharing.prompts} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_prompts && !permissions.sharing.public_prompts}
|
||||
{#if defaultPermissions?.sharing?.prompts && !permissions.sharing.prompts}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
|
|
@ -262,14 +303,32 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if permissions.sharing.prompts}
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Prompts Public Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_prompts} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_prompts && !permissions.sharing.public_prompts}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Tools Public Sharing')}
|
||||
{$i18n.t('Tools Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_tools} />
|
||||
<Switch bind:state={permissions.sharing.tools} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_tools && !permissions.sharing.public_tools}
|
||||
{#if defaultPermissions?.sharing?.tools && !permissions.sharing.tools}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
|
|
@ -278,14 +337,32 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if permissions.sharing.tools}
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Tools Public Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_tools} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_tools && !permissions.sharing.public_tools}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Notes Public Sharing')}
|
||||
{$i18n.t('Notes Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_notes} />
|
||||
<Switch bind:state={permissions.sharing.notes} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_notes && !permissions.sharing.public_notes}
|
||||
{#if defaultPermissions?.sharing?.notes && !permissions.sharing.notes}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
|
|
@ -293,6 +370,24 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if permissions.sharing.notes}
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Notes Public Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_notes} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_notes && !permissions.sharing.public_notes}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_NAME, config, user, showSidebar } from '$lib/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
|
@ -154,27 +154,28 @@
|
|||
<div
|
||||
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
||||
>
|
||||
<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 gap-2">
|
||||
<div class="flex-shrink-0">
|
||||
{$i18n.t('Users')}
|
||||
</div>
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||
|
||||
{#if ($config?.license_metadata?.seats ?? null) !== null}
|
||||
{#if total > $config?.license_metadata?.seats}
|
||||
<span class="text-lg font-medium text-red-500"
|
||||
>{total} of {$config?.license_metadata?.seats}
|
||||
<span class="text-sm font-normal">{$i18n.t('available users')}</span></span
|
||||
>
|
||||
<div>
|
||||
{#if ($config?.license_metadata?.seats ?? null) !== null}
|
||||
{#if total > $config?.license_metadata?.seats}
|
||||
<span class="text-lg font-medium text-red-500"
|
||||
>{total} of {$config?.license_metadata?.seats}
|
||||
<span class="text-sm font-normal">{$i18n.t('available users')}</span></span
|
||||
>
|
||||
{:else}
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
|
||||
>{total} of {$config?.license_metadata?.seats}
|
||||
<span class="text-sm font-normal">{$i18n.t('available users')}</span></span
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
|
||||
>{total} of {$config?.license_metadata?.seats}
|
||||
<span class="text-sm font-normal">{$i18n.t('available users')}</span></span
|
||||
>
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{total}</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{total}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
|
|
@ -361,11 +362,7 @@
|
|||
<div class="flex items-center">
|
||||
<img
|
||||
class="rounded-full w-6 h-6 object-cover mr-2.5 flex-shrink-0"
|
||||
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
|
||||
: `${WEBUI_BASE_URL}/user.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
|
||||
alt="user"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -171,8 +171,7 @@
|
|||
</div>
|
||||
{:else if item.type === 'model'}
|
||||
<img
|
||||
src={item?.data?.info?.meta?.profile_image_url ??
|
||||
`${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${item.id}&lang=${$i18n.language}`}
|
||||
alt={item?.data?.name ?? item.id}
|
||||
class="rounded-full size-5 items-center mr-2"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@
|
|||
{#if message?.reply_to_message?.user}
|
||||
<div class="relative text-xs mb-1">
|
||||
<div
|
||||
class="absolute h-3 w-7 left-[18px] top-2 rounded-tl-lg border-t-2 border-l-2 border-gray-300 dark:border-gray-500 z-0"
|
||||
class="absolute h-3 w-7 left-[18px] top-2 rounded-tl-lg border-t-[1.5px] border-l-[1.5px] border-gray-200 dark:border-gray-700 z-0"
|
||||
></div>
|
||||
|
||||
<button
|
||||
|
|
@ -185,8 +185,7 @@
|
|||
/>
|
||||
{:else}
|
||||
<img
|
||||
src={message.reply_to_message.user?.profile_image_url ??
|
||||
`${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${message.reply_to_message.user?.id}/profile/image`}
|
||||
alt={message.reply_to_message.user?.name ?? $i18n.t('Unknown User')}
|
||||
class="size-4 ml-0.5 rounded-full object-cover"
|
||||
/>
|
||||
|
|
@ -220,7 +219,7 @@
|
|||
{:else}
|
||||
<ProfilePreview user={message.user}>
|
||||
<ProfileImage
|
||||
src={message.user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${message.user.id}/profile/image`}
|
||||
className={'size-8 ml-0.5'}
|
||||
/>
|
||||
</ProfilePreview>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext, onMount } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let user = null;
|
||||
</script>
|
||||
|
|
@ -11,8 +11,7 @@
|
|||
<div class=" flex gap-3.5 w-full py-3 px-3 items-center">
|
||||
<div class=" items-center flex shrink-0">
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${user?.id}/profile/image`}
|
||||
class=" size-12 object-cover rounded-xl"
|
||||
alt="profile"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import PencilSquare from '../icons/PencilSquare.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import Sidebar from '../icons/Sidebar.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -80,7 +81,7 @@
|
|||
>
|
||||
<div class=" self-center">
|
||||
<img
|
||||
src={$user?.profile_image_url}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
||||
class="size-6 object-cover rounded-full"
|
||||
alt="User profile"
|
||||
draggable="false"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { config, user, models as _models, temporaryChatEnabled } from '$lib/stores';
|
||||
|
|
@ -53,11 +53,7 @@
|
|||
placement="right"
|
||||
>
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
|
||||
class=" size-[2.7rem] rounded-full border-[1px] border-gray-100 dark:border-none"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
|
|
|
|||
|
|
@ -1069,14 +1069,9 @@
|
|||
<div class="flex items-center justify-between w-full">
|
||||
<div class="pl-[1px] flex items-center gap-2 text-sm dark:text-gray-500">
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
alt="model profile"
|
||||
class="size-3.5 max-w-[28px] object-cover rounded-full"
|
||||
src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
|
||||
?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${$models.find((model) => model.id === atSelectedModel.id).id}&lang=${$i18n.language}`}
|
||||
/>
|
||||
<div class="translate-y-[0.5px]">
|
||||
<span class="">{atSelectedModel.name}</span>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
|
||||
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -759,14 +760,8 @@
|
|||
? ' size-16'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-14'
|
||||
: 'size-12'} transition-all rounded-full {(model?.info?.meta
|
||||
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
|
||||
? ' bg-cover bg-center bg-no-repeat'
|
||||
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
|
||||
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
|
||||
'/static/favicon.png'
|
||||
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
|
||||
: ''}
|
||||
: 'size-12'} transition-all rounded-full bg-cover bg-center bg-no-repeat"
|
||||
style={`background-image: url('${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model?.id}&lang=${$i18n.language}&voice=true');`}
|
||||
/>
|
||||
{/if}
|
||||
<!-- navbar -->
|
||||
|
|
@ -841,14 +836,8 @@
|
|||
? 'size-48'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-44'
|
||||
: 'size-40'} transition-all rounded-full {(model?.info?.meta
|
||||
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
|
||||
? ' bg-cover bg-center bg-no-repeat'
|
||||
: 'bg-black dark:bg-white'} "
|
||||
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
|
||||
'/static/favicon.png'
|
||||
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
|
||||
: ''}
|
||||
: 'size-40'} transition-all rounded-full bg-cover bg-center bg-no-repeat"
|
||||
style={`background-image: url('${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model?.id}&lang=${$i18n.language}&voice=true');`}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { tick, getContext } from 'svelte';
|
||||
|
||||
import { models } from '$lib/stores';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
>
|
||||
<div class="flex text-black dark:text-gray-100 line-clamp-1">
|
||||
<img
|
||||
src={model?.info?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
|
||||
alt={model?.name ?? model.id}
|
||||
class="rounded-full size-5 items-center mr-2"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -272,14 +272,6 @@
|
|||
}}
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- <ProfileImage
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/favicon.png`)}
|
||||
className={'size-5 assistant-message-profile-image'}
|
||||
/> -->
|
||||
|
||||
<div class="-translate-y-[1px]">
|
||||
{model ? `${model.name}` : history.messages[_messageId]?.model}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
</script>
|
||||
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
aria-hidden="true"
|
||||
src={src === ''
|
||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
removeDetails,
|
||||
removeAllDetails
|
||||
} from '$lib/utils';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
|
|
@ -627,10 +627,7 @@
|
|||
>
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex mt-1 `}>
|
||||
<ProfileImage
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/favicon.png`)}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
|
||||
className={'size-8 assistant-message-profile-image'}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -124,10 +124,7 @@
|
|||
{#if !($settings?.chatBubble ?? true)}
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 mt-1`}>
|
||||
<ProfileImage
|
||||
src={message.user
|
||||
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
|
||||
`${WEBUI_BASE_URL}/user.png`)
|
||||
: (user?.profile_image_url ?? `${WEBUI_BASE_URL}/user.png`)}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
|
||||
className={'size-8 user-message-profile-image'}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import dayjs from '$lib/dayjs';
|
||||
|
||||
import { mobile, settings, user } from '$lib/stores';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { copyToClipboard, sanitizeResponseContent } from '$lib/utils';
|
||||
|
|
@ -77,8 +77,7 @@
|
|||
<div class="flex items-center min-w-fit">
|
||||
<Tooltip content={$user?.role === 'admin' ? (item?.value ?? '') : ''} placement="top-start">
|
||||
<img
|
||||
src={item.model?.info?.meta?.profile_image_url ??
|
||||
`${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${item.model.id}&lang=${$i18n.language}`}
|
||||
alt="Model"
|
||||
class="rounded-full size-5 flex items-center"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
import ChatPlus from '../icons/ChatPlus.svelte';
|
||||
import ChatCheck from '../icons/ChatCheck.svelte';
|
||||
import Knobs from '../icons/Knobs.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -242,7 +243,7 @@
|
|||
<div class=" self-center">
|
||||
<span class="sr-only">{$i18n.t('User menu')}</span>
|
||||
<img
|
||||
src={$user?.profile_image_url}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
||||
class="size-6 object-cover rounded-full"
|
||||
alt=""
|
||||
draggable="false"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import ProfileImage from '../Messages/ProfileImage.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Heart from '$lib/components/icons/Heart.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
type $$Props = NodeProps;
|
||||
export let data: $$Props['data'];
|
||||
</script>
|
||||
|
|
@ -21,7 +24,7 @@
|
|||
{#if data.message.role === 'user'}
|
||||
<div class="flex w-full">
|
||||
<ProfileImage
|
||||
src={data.user?.profile_image_url ?? `${WEBUI_BASE_URL}/user.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${data.user.id}/profile/image`}
|
||||
className={'size-5 -translate-y-[1px]'}
|
||||
/>
|
||||
<div class="ml-2">
|
||||
|
|
@ -41,7 +44,7 @@
|
|||
{:else}
|
||||
<div class="flex w-full">
|
||||
<ProfileImage
|
||||
src={data?.model?.info?.meta?.profile_image_url ?? ''}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${data.model.id}&lang=${$i18n.language}`}
|
||||
className={'size-5 -translate-y-[1px]'}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
currentChatPage
|
||||
} from '$lib/stores';
|
||||
import { sanitizeResponseContent, extractCurlyBraceWords } from '$lib/utils';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import Suggestions from './Suggestions.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
|
@ -121,11 +121,7 @@
|
|||
}}
|
||||
>
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model?.id}&lang=${$i18n.language}`}
|
||||
class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
|
||||
aria-hidden="true"
|
||||
draggable="false"
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
import dayjs from 'dayjs';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import calendar from 'dayjs/plugin/calendar'
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(calendar);
|
||||
|
||||
import { deleteChatById } from '$lib/apis/chats';
|
||||
|
||||
|
|
@ -242,7 +244,14 @@
|
|||
|
||||
<div class="basis-2/5 flex items-center justify-end">
|
||||
<div class="hidden sm:flex text-gray-500 dark:text-gray-400 text-xs">
|
||||
{dayjs(chat?.updated_at * 1000).calendar()}
|
||||
{$i18n.t(dayjs(chat?.updated_at * 1000).calendar(null, {
|
||||
sameDay: '[Today]',
|
||||
nextDay: '[Tomorrow]',
|
||||
nextWeek: 'dddd',
|
||||
lastDay: '[Yesterday]',
|
||||
lastWeek: '[Last] dddd',
|
||||
sameElse: 'L' // use localized format, otherwise dayjs.calendar() defaults to DD/MM/YYYY
|
||||
}))}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pl-2.5 text-gray-600 dark:text-gray-300">
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import Spinner from '../common/Spinner.svelte';
|
||||
|
||||
import dayjs from '$lib/dayjs';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import calendar from 'dayjs/plugin/calendar';
|
||||
import Loader from '../common/Loader.svelte';
|
||||
import { createMessagesList } from '$lib/utils';
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
import PencilSquare from '../icons/PencilSquare.svelte';
|
||||
import PageEdit from '../icons/PageEdit.svelte';
|
||||
dayjs.extend(calendar);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export let show = false;
|
||||
export let onClose = () => {};
|
||||
|
|
@ -387,7 +389,14 @@
|
|||
</div>
|
||||
|
||||
<div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs">
|
||||
{dayjs(chat?.updated_at * 1000).calendar()}
|
||||
{$i18n.t(dayjs(chat?.updated_at * 1000).calendar(null, {
|
||||
sameDay: '[Today]',
|
||||
nextDay: '[Tomorrow]',
|
||||
nextWeek: 'dddd',
|
||||
lastDay: '[Yesterday]',
|
||||
lastWeek: '[Last] dddd',
|
||||
sameElse: 'L' // use localized format, otherwise dayjs.calendar() defaults to DD/MM/YYYY
|
||||
}))}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
importChat
|
||||
} from '$lib/apis/chats';
|
||||
import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import ArchivedChatsModal from './ArchivedChatsModal.svelte';
|
||||
import UserMenu from './Sidebar/UserMenu.svelte';
|
||||
|
|
@ -537,7 +537,7 @@
|
|||
|
||||
{#if !$mobile && !$showSidebar}
|
||||
<div
|
||||
class=" py-2 px-1.5 flex flex-col justify-between text-black dark:text-white hover:bg-gray-50/30 dark:hover:bg-gray-950/30 h-full z-10 transition-all border-e-[0.5px] border-gray-50 dark:border-gray-850"
|
||||
class=" pt-[7px] pb-2 px-1.5 flex flex-col justify-between text-black dark:text-white hover:bg-gray-50/30 dark:hover:bg-gray-950/30 h-full z-10 transition-all border-e-[0.5px] border-gray-50 dark:border-gray-850"
|
||||
id="sidebar"
|
||||
>
|
||||
<button
|
||||
|
|
@ -559,7 +559,6 @@
|
|||
>
|
||||
<div class=" self-center flex items-center justify-center size-9">
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src="{WEBUI_BASE_URL}/static/favicon.png"
|
||||
class="sidebar-new-chat-icon size-6 rounded-full group-hover:hidden"
|
||||
alt=""
|
||||
|
|
@ -571,7 +570,7 @@
|
|||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="-mt-[0.5px]">
|
||||
<div class="">
|
||||
<Tooltip content={$i18n.t('New Chat')} placement="right">
|
||||
<a
|
||||
|
|
@ -594,7 +593,7 @@
|
|||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<div>
|
||||
<Tooltip content={$i18n.t('Search')} placement="right">
|
||||
<button
|
||||
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
|
||||
|
|
@ -694,7 +693,7 @@
|
|||
>
|
||||
<div class=" self-center flex items-center justify-center size-9">
|
||||
<img
|
||||
src={$user?.profile_image_url}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
||||
class=" size-6 object-cover rounded-full"
|
||||
alt={$i18n.t('Open User Profile Menu')}
|
||||
aria-label={$i18n.t('Open User Profile Menu')}
|
||||
|
|
@ -789,7 +788,7 @@
|
|||
}}
|
||||
>
|
||||
<div class="pb-1.5">
|
||||
<div class="px-[7px] 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
|
||||
id="sidebar-new-chat-button"
|
||||
class="group grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
||||
|
|
@ -810,7 +809,7 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="px-[7px] 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">
|
||||
<button
|
||||
id="sidebar-search-button"
|
||||
class="group grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
||||
|
|
@ -832,7 +831,7 @@
|
|||
</div>
|
||||
|
||||
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
|
||||
<div class="px-[7px] 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
|
||||
id="sidebar-notes-button"
|
||||
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
|
|
@ -853,7 +852,7 @@
|
|||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
|
||||
<div class="px-[7px] 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
|
||||
id="sidebar-workspace-button"
|
||||
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
|
|
@ -1233,7 +1232,7 @@
|
|||
>
|
||||
<div class=" self-center mr-3">
|
||||
<img
|
||||
src={$user?.profile_image_url}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
||||
class=" size-6 object-cover rounded-full"
|
||||
alt={$i18n.t('Open User Profile Menu')}
|
||||
aria-label={$i18n.t('Open User Profile Menu')}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import PinSlash from '$lib/components/icons/PinSlash.svelte';
|
||||
|
|
@ -36,8 +36,7 @@
|
|||
>
|
||||
<div class="self-center shrink-0">
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={model?.info?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
|
||||
class=" size-5 rounded-full -translate-x-[0.5px]"
|
||||
alt="logo"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -116,7 +116,8 @@
|
|||
<AccessControl
|
||||
bind:accessControl
|
||||
accessRoles={['read', 'write']}
|
||||
allowPublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
|
||||
share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -546,14 +546,42 @@
|
|||
e.preventDefault();
|
||||
dragged = false;
|
||||
|
||||
const handleUploadingFileFolder = (items) => {
|
||||
for (const item of items) {
|
||||
if (item.isFile) {
|
||||
item.file((file) => {
|
||||
uploadFileHandler(file);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not sure why you have to call webkitGetAsEntry and isDirectory seperate, but it won't work if you try item.webkitGetAsEntry().isDirectory
|
||||
const wkentry = item.webkitGetAsEntry();
|
||||
const isDirectory = wkentry.isDirectory;
|
||||
if (isDirectory) {
|
||||
// Read the directory
|
||||
wkentry.createReader().readEntries(
|
||||
(entries) => {
|
||||
handleUploadingFileFolder(entries);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error reading directory entries:', error);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
toast.info($i18n.t('Uploading file...'));
|
||||
uploadFileHandler(item.getAsFile());
|
||||
toast.success($i18n.t('File uploaded!'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (e.dataTransfer?.types?.includes('Files')) {
|
||||
if (e.dataTransfer?.files) {
|
||||
const inputFiles = e.dataTransfer?.files;
|
||||
const inputItems = e.dataTransfer?.items;
|
||||
|
||||
if (inputFiles && inputFiles.length > 0) {
|
||||
for (const file of inputFiles) {
|
||||
await uploadFileHandler(file);
|
||||
}
|
||||
if (inputItems && inputItems.length > 0) {
|
||||
handleUploadingFileFolder(inputItems);
|
||||
} else {
|
||||
toast.error($i18n.t(`File not found.`));
|
||||
}
|
||||
|
|
@ -682,7 +710,8 @@
|
|||
<AccessControlModal
|
||||
bind:show={showAccessControlModal}
|
||||
bind:accessControl={knowledge.access_control}
|
||||
allowPublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
|
||||
share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
|
||||
onChange={() => {
|
||||
changeDebounceHandler();
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
const i18n = getContext('i18n');
|
||||
|
||||
import { WEBUI_NAME, config, mobile, models as _models, settings, user } from '$lib/stores';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import {
|
||||
createNewModel,
|
||||
deleteModelById,
|
||||
|
|
@ -438,13 +438,12 @@
|
|||
<div class="self-center pl-0.5">
|
||||
<div class="flex bg-white rounded-2xl">
|
||||
<div
|
||||
class="{model.is_active ? '' : 'opacity-50 dark:opacity-50'} {model.meta
|
||||
.profile_image_url !== `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
? 'bg-transparent'
|
||||
: 'bg-white'} rounded-2xl"
|
||||
class="{model.is_active
|
||||
? ''
|
||||
: 'opacity-50 dark:opacity-50'} bg-transparent rounded-2xl"
|
||||
>
|
||||
<img
|
||||
src={model?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
|
||||
alt="modelfile profile"
|
||||
class=" rounded-2xl size-12 object-cover"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -748,7 +748,8 @@
|
|||
<AccessControl
|
||||
bind:accessControl
|
||||
accessRoles={['read', 'write']}
|
||||
allowPublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin'}
|
||||
share={$user?.permissions?.sharing?.models || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@
|
|||
bind:show={showAccessControlModal}
|
||||
bind:accessControl
|
||||
accessRoles={['read', 'write']}
|
||||
allowPublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin'}
|
||||
share={$user?.permissions?.sharing?.prompts || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin'}
|
||||
/>
|
||||
|
||||
<div class="w-full max-h-full flex justify-center">
|
||||
|
|
|
|||
|
|
@ -189,7 +189,8 @@ class Tools:
|
|||
bind:show={showAccessControlModal}
|
||||
bind:accessControl
|
||||
accessRoles={['read', 'write']}
|
||||
allowPublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin'}
|
||||
share={$user?.permissions?.sharing?.tools || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin'}
|
||||
/>
|
||||
|
||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||
|
|
|
|||
|
|
@ -15,17 +15,18 @@
|
|||
export let accessRoles = ['read'];
|
||||
export let accessControl = {};
|
||||
|
||||
export let allowPublic = true;
|
||||
export let share = true;
|
||||
export let sharePublic = true;
|
||||
|
||||
let selectedGroupId = '';
|
||||
let groups = [];
|
||||
|
||||
$: if (!allowPublic && accessControl === null) {
|
||||
$: if (!sharePublic && accessControl === null) {
|
||||
initPublicAccess();
|
||||
}
|
||||
|
||||
const initPublicAccess = () => {
|
||||
if (!allowPublic && accessControl === null) {
|
||||
if (!sharePublic && accessControl === null) {
|
||||
accessControl = {
|
||||
read: {
|
||||
group_ids: [],
|
||||
|
|
@ -41,7 +42,7 @@
|
|||
};
|
||||
|
||||
onMount(async () => {
|
||||
groups = await getGroups(localStorage.token);
|
||||
groups = await getGroups(localStorage.token, true);
|
||||
|
||||
if (accessControl === null) {
|
||||
initPublicAccess();
|
||||
|
|
@ -125,7 +126,7 @@
|
|||
}}
|
||||
>
|
||||
<option class=" text-gray-700" value="private" selected>{$i18n.t('Private')}</option>
|
||||
{#if allowPublic}
|
||||
{#if share && sharePublic}
|
||||
<option class=" text-gray-700" value="public" selected>{$i18n.t('Public')}</option>
|
||||
{/if}
|
||||
</select>
|
||||
|
|
@ -140,48 +141,50 @@
|
|||
</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="mb-1">
|
||||
<div class="flex w-full">
|
||||
<div class="flex flex-1 items-center">
|
||||
<div class="w-full px-0.5">
|
||||
<select
|
||||
class="outline-hidden bg-transparent text-sm rounded-lg block w-full pr-10 max-w-full
|
||||
{#if share}
|
||||
{#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="mb-1">
|
||||
<div class="flex w-full">
|
||||
<div class="flex flex-1 items-center">
|
||||
<div class="w-full px-0.5">
|
||||
<select
|
||||
class="outline-hidden bg-transparent text-sm rounded-lg block w-full pr-10 max-w-full
|
||||
{selectedGroupId ? '' : 'text-gray-500'}
|
||||
dark:placeholder-gray-500"
|
||||
bind:value={selectedGroupId}
|
||||
on:change={() => {
|
||||
if (selectedGroupId !== '') {
|
||||
accessControl.read.group_ids = [
|
||||
...(accessControl?.read?.group_ids ?? []),
|
||||
selectedGroupId
|
||||
];
|
||||
bind:value={selectedGroupId}
|
||||
on:change={() => {
|
||||
if (selectedGroupId !== '') {
|
||||
accessControl.read.group_ids = [
|
||||
...(accessControl?.read?.group_ids ?? []),
|
||||
selectedGroupId
|
||||
];
|
||||
|
||||
selectedGroupId = '';
|
||||
onChange(accessControl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option class=" text-gray-700" value="" disabled selected
|
||||
>{$i18n.t('Select a group')}</option
|
||||
selectedGroupId = '';
|
||||
onChange(accessControl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#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>
|
||||
<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"
|
||||
|
|
@ -192,80 +195,81 @@
|
|||
</button>
|
||||
</Tooltip>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-700/10 mt-1.5 mb-2.5 w-full" />
|
||||
<hr class=" border-gray-100 dark:border-gray-700/10 mt-1.5 mb-2.5 w-full" />
|
||||
|
||||
<div class="flex flex-col gap-2 mb-1 px-0.5">
|
||||
{#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 class="flex flex-col gap-2 mb-1 px-0.5">
|
||||
{#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>
|
||||
{group.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-end items-center gap-0.5">
|
||||
<button
|
||||
class=""
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (accessRoles.includes('write')) {
|
||||
if ((accessControl?.write?.group_ids ?? []).includes(group.id)) {
|
||||
accessControl.write.group_ids = (
|
||||
accessControl?.write?.group_ids ?? []
|
||||
).filter((group_id) => group_id !== group.id);
|
||||
} else {
|
||||
accessControl.write.group_ids = [
|
||||
...(accessControl?.write?.group_ids ?? []),
|
||||
group.id
|
||||
];
|
||||
<div class="w-full flex justify-end items-center gap-0.5">
|
||||
<button
|
||||
class=""
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (accessRoles.includes('write')) {
|
||||
if ((accessControl?.write?.group_ids ?? []).includes(group.id)) {
|
||||
accessControl.write.group_ids = (
|
||||
accessControl?.write?.group_ids ?? []
|
||||
).filter((group_id) => group_id !== group.id);
|
||||
} else {
|
||||
accessControl.write.group_ids = [
|
||||
...(accessControl?.write?.group_ids ?? []),
|
||||
group.id
|
||||
];
|
||||
}
|
||||
onChange(accessControl);
|
||||
}
|
||||
onChange(accessControl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if (accessControl?.write?.group_ids ?? []).includes(group.id)}
|
||||
<Badge type={'success'} content={$i18n.t('Write')} />
|
||||
{:else}
|
||||
<Badge type={'info'} content={$i18n.t('Read')} />
|
||||
{/if}
|
||||
</button>
|
||||
}}
|
||||
>
|
||||
{#if (accessControl?.write?.group_ids ?? []).includes(group.id)}
|
||||
<Badge type={'success'} content={$i18n.t('Write')} />
|
||||
{:else}
|
||||
<Badge type={'info'} content={$i18n.t('Read')} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<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
|
||||
);
|
||||
accessControl.write.group_ids = (
|
||||
accessControl?.write?.group_ids ?? []
|
||||
).filter((id) => id !== group.id);
|
||||
onChange(accessControl);
|
||||
}}
|
||||
>
|
||||
<XMark />
|
||||
</button>
|
||||
<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);
|
||||
accessControl.write.group_ids = (
|
||||
accessControl?.write?.group_ids ?? []
|
||||
).filter((id) => id !== group.id);
|
||||
onChange(accessControl);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{/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}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
export let show = false;
|
||||
export let accessControl = {};
|
||||
export let accessRoles = ['read'];
|
||||
export let allowPublic = true;
|
||||
|
||||
export let share = true;
|
||||
export let sharePublic = true;
|
||||
|
||||
export let onChange = () => {};
|
||||
</script>
|
||||
|
|
@ -31,7 +33,7 @@
|
|||
</div>
|
||||
|
||||
<div class="w-full px-5 pb-4 dark:text-white">
|
||||
<AccessControl bind:accessControl {onChange} {accessRoles} {allowPublic} />
|
||||
<AccessControl bind:accessControl {onChange} {accessRoles} {share} {sharePublic} />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -101,5 +101,6 @@ import 'dayjs/locale/yo';
|
|||
import 'dayjs/locale/zh';
|
||||
import 'dayjs/locale/zh-tw';
|
||||
import 'dayjs/locale/et';
|
||||
import 'dayjs/locale/en-gb'
|
||||
|
||||
export default dayjs;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@
|
|||
"Advanced Parameters": "고급 매개변수",
|
||||
"Advanced parameters for MinerU parsing (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)": "",
|
||||
"Advanced Params": "고급 매개변수",
|
||||
"After updating or changing the embedding model, you must reindex the knowledge base for the changes to take effect. You can do this using the \"Reindex\" button below.": "",
|
||||
"After updating or changing the embedding model, you must reindex the knowledge base for the changes to take effect. You can do this using the \"Reindex\" button below.": "임베딩 모델을 업데이트하거나 변경 후 변경 사항을 적용하려면 지식 베이스를 다시 인덱싱해야 합니다. 아래의 \"재색인\" 버튼을 사용하여 수행할 수 있습니다.",
|
||||
"AI": "",
|
||||
"All": "전체",
|
||||
"All chats have been unarchived.": "모든 채팅이 보관 해제되었습니다.",
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
"Ask": "질문",
|
||||
"Ask a question": "질문하기",
|
||||
"Assistant": "어시스턴트",
|
||||
"Attach File From Knowledge": "",
|
||||
"Attach File From Knowledge": "지식 기반에서 파일 첨부",
|
||||
"Attach Knowledge": "지식 기반 첨부",
|
||||
"Attach Notes": "노트 첨부",
|
||||
"Attach Webpage": "웹페이지 첨부",
|
||||
|
|
@ -233,7 +233,7 @@
|
|||
"Chat Background Image": "채팅 배경 이미지",
|
||||
"Chat Bubble UI": "버블형 채팅 UI",
|
||||
"Chat Controls": "채팅 제어",
|
||||
"Chat Conversation": "",
|
||||
"Chat Conversation": "채팅 대화",
|
||||
"Chat direction": "채팅 방향",
|
||||
"Chat ID": "채팅 ID",
|
||||
"Chat moved successfully": "채팅 이동 성공",
|
||||
|
|
@ -279,7 +279,7 @@
|
|||
"cloud": "",
|
||||
"CMU ARCTIC speaker embedding name": "",
|
||||
"Code Block": "코드 블록",
|
||||
"Code Editor": "",
|
||||
"Code Editor": "코드 편집기",
|
||||
"Code execution": "코드 실행",
|
||||
"Code Execution": "코드 실행",
|
||||
"Code Execution Engine": "코드 실행 엔진",
|
||||
|
|
@ -337,8 +337,8 @@
|
|||
"Copied to clipboard": "클립보드에 복사되었습니다",
|
||||
"Copy": "복사",
|
||||
"Copy Formatted Text": "서식 있는 텍스트 복사",
|
||||
"Copy Last Code Block": "",
|
||||
"Copy Last Response": "",
|
||||
"Copy Last Code Block": "마지막 코드 블록 복사",
|
||||
"Copy Last Response": "마지막 응답 복사",
|
||||
"Copy link": "링크 복사",
|
||||
"Copy Link": "링크 복사",
|
||||
"Copy to clipboard": "클립보드에 복사",
|
||||
|
|
@ -347,13 +347,13 @@
|
|||
"Create": "생성",
|
||||
"Create a knowledge base": "지식 기반 생성",
|
||||
"Create a model": "모델 생성",
|
||||
"Create a new note": "",
|
||||
"Create a new note": "새 노트 생성",
|
||||
"Create Account": "계정 생성",
|
||||
"Create Admin Account": "관리자 계정 생성",
|
||||
"Create Channel": "채널 생성",
|
||||
"Create Folder": "폴더 생성",
|
||||
"Create Group": "그룹 생성",
|
||||
"Create Image": "",
|
||||
"Create Image": "이미지 생성",
|
||||
"Create Knowledge": "지식 생성",
|
||||
"Create Model": "모델 생성",
|
||||
"Create new key": "새로운 키 생성",
|
||||
|
|
@ -502,7 +502,7 @@
|
|||
"Edit Connection": "연결 편집",
|
||||
"Edit Default Permissions": "기본 권한 편집",
|
||||
"Edit Folder": "폴더 편집",
|
||||
"Edit Image": "",
|
||||
"Edit Image": "이미지 편집",
|
||||
"Edit Last Message": "",
|
||||
"Edit Memory": "메모리 편집",
|
||||
"Edit User": "사용자 편집",
|
||||
|
|
@ -592,8 +592,8 @@
|
|||
"Enter Kagi Search API Key": "Kagi Search API 키 입력",
|
||||
"Enter Key Behavior": "키 동작 입력",
|
||||
"Enter language codes": "언어 코드 입력",
|
||||
"Enter MinerU API Key": "",
|
||||
"Enter Mistral API Base URL": "",
|
||||
"Enter MinerU API Key": "MinerU API 키 입력",
|
||||
"Enter Mistral API Base URL": "Mistral API Base URL 입력",
|
||||
"Enter Mistral API Key": "Mistral API 키 입력",
|
||||
"Enter Model ID": "모델 ID 입력",
|
||||
"Enter model tag (e.g. {{modelTag}})": "모델 태그 입력(예: {{modelTag}})",
|
||||
|
|
@ -718,8 +718,8 @@
|
|||
"Failed to load chat preview": "채팅 미리보기 로드 실패",
|
||||
"Failed to load file content.": "파일 내용 로드 실패.",
|
||||
"Failed to move chat": "채팅 이동 실패",
|
||||
"Failed to read clipboard contents": "클립보드 내용 가져오기를 실패하였습니다.",
|
||||
"Failed to render diagram": "",
|
||||
"Failed to read clipboard contents": "클립보드 내용 가져오기를 실패하였습니다",
|
||||
"Failed to render diagram": "다이어그램을 표시할 수 없습니다",
|
||||
"Failed to render visualization": "",
|
||||
"Failed to save connections": "연결 저장 실패",
|
||||
"Failed to save conversation": "대화 저장 실패",
|
||||
|
|
@ -753,7 +753,7 @@
|
|||
"Firecrawl API Base URL": "Firecrawl API 기본 URL",
|
||||
"Firecrawl API Key": "Firecrawl API 키",
|
||||
"Floating Quick Actions": "",
|
||||
"Focus Chat Input": "",
|
||||
"Focus Chat Input": "채팅 입력창에 포커스",
|
||||
"Folder": "폴더",
|
||||
"Folder Background Image": "폴더 배경 이미지",
|
||||
"Folder deleted successfully": "성공적으로 폴더가 삭제되었습니다",
|
||||
|
|
@ -774,8 +774,8 @@
|
|||
"Format Lines": "줄 서식",
|
||||
"Format the lines in the output. Defaults to False. If set to True, the lines will be formatted to detect inline math and styles.": "출력되는 줄에 서식을 적용합니다. 기본값은 False입니다. 이 옵션을 True로 하면, 인라인 수식 및 스타일을 감지하도록 줄에 서식이 적용됩니다.",
|
||||
"Format your variables using brackets like this:": "변수를 다음과 같이 괄호를 사용하여 생성하세요",
|
||||
"Formatting may be inconsistent from source.": "",
|
||||
"Forwards system user OAuth access token to authenticate": "",
|
||||
"Formatting may be inconsistent from source.": "출처에서의 서식이 일관되지 않을 수 있습니다.",
|
||||
"Forwards system user OAuth access token to authenticate": "인증을 위해 시스템 사용자 OAuth 액세스 토큰을 전달합니다.",
|
||||
"Forwards system user session credentials to authenticate": "인증을 위해 시스템 사용자 세션 자격 증명 전달",
|
||||
"Full Context Mode": "전체 컨텍스트 모드",
|
||||
"Function": "함수",
|
||||
|
|
@ -803,7 +803,7 @@
|
|||
"Generate an image": "이미지 생성",
|
||||
"Generate Image": "이미지 생성",
|
||||
"Generate Message Pair": "",
|
||||
"Generated Image": "",
|
||||
"Generated Image": "생성된 이미지",
|
||||
"Generating search query": "검색 쿼리 생성",
|
||||
"Generating...": "생성 중...",
|
||||
"Get information on {{name}} in the UI": "UI에서 {{name}} 정보 확인",
|
||||
|
|
@ -865,7 +865,7 @@
|
|||
"Image Max Compression Size width": "이미지 최대 압축 크기 너비",
|
||||
"Image Prompt Generation": "이미지 프롬프트 생성",
|
||||
"Image Prompt Generation Prompt": "이미지 프롬프트를 생성하기 위한 프롬프트",
|
||||
"Image Size": "",
|
||||
"Image Size": "이미지 크기",
|
||||
"Images": "이미지",
|
||||
"Import": "가져오기",
|
||||
"Import Chats": "채팅 가져오기",
|
||||
|
|
@ -963,7 +963,7 @@
|
|||
"Leave empty to include all models from \"{{url}}/api/tags\" endpoint": "\"{{url}}/api/tags\" 엔드포인트의 모든 모델을 포함하려면 비워 두세요",
|
||||
"Leave empty to include all models from \"{{url}}/models\" endpoint": "\"{{url}}/models\" 엔드포인트의 모든 모델을 포함하려면 비워 두세요",
|
||||
"Leave empty to include all models or select specific models": "비워두면 모든 모델이 포함되며, 특정 모델을 선택할 수도 있습니다.",
|
||||
"Leave empty to use the default model (voxtral-mini-latest).": "",
|
||||
"Leave empty to use the default model (voxtral-mini-latest).": "비워두면 기본 모델(voxtral-mini-latest)을 사용합니다.",
|
||||
"Leave empty to use the default prompt, or enter a custom prompt": "기본 프롬프트를 사용하기 위해 빈칸으로 남겨두거나, 커스텀 프롬프트를 입력하세요",
|
||||
"Leave model field empty to use the default model.": "기본 모델을 사용하려면 모델 필드를 비워 두세요.",
|
||||
"Legacy": "",
|
||||
|
|
@ -1017,14 +1017,14 @@
|
|||
"Memory updated successfully": "성공적으로 메모리가 업데이트되었습니다",
|
||||
"Merge Responses": "응답들 결합하기",
|
||||
"Merged Response": "결합된 응답",
|
||||
"Message": "",
|
||||
"Message": "메시지",
|
||||
"Message rating should be enabled to use this feature": "이 기능을 사용하려면 메시지 평가가 활성화되어야합니다",
|
||||
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "링크 생성 후에 보낸 메시지는 공유되지 않습니다. URL이 있는 사용자는 공유된 채팅을 볼 수 있습니다.",
|
||||
"Microsoft OneDrive": "",
|
||||
"Microsoft OneDrive (personal)": "Microsoft OneDrive (개인용)",
|
||||
"Microsoft OneDrive (work/school)": "Microsoft OneDrive (회사/학교용)",
|
||||
"MinerU": "",
|
||||
"MinerU API Key required for Cloud API mode.": "",
|
||||
"MinerU API Key required for Cloud API mode.": "클라우드 API 모드를 사용하려면 MinerU API 키가 필요합니다.",
|
||||
"Mistral OCR": "",
|
||||
"Mistral OCR API Key required.": "Mistral OCR API Key가 필요합니다.",
|
||||
"MistralAI": "",
|
||||
|
|
@ -1048,7 +1048,7 @@
|
|||
"Model ID is required.": "모델 ID가 필요합니다",
|
||||
"Model IDs": "모델 IDs",
|
||||
"Model Name": "모델 이름",
|
||||
"Model name already exists, please choose a different one": "",
|
||||
"Model name already exists, please choose a different one": "이 모델 이름은 이미 존재합니다. 다른 이름을 선택해주세요.",
|
||||
"Model Name is required.": "모델 이름이 필요합니다",
|
||||
"Model not selected": "모델이 선택되지 않았습니다.",
|
||||
"Model Params": "모델 매개변수",
|
||||
|
|
@ -1060,7 +1060,7 @@
|
|||
"Models": "모델",
|
||||
"Models Access": "모델 접근",
|
||||
"Models configuration saved successfully": "모델 구성이 성공적으로 저장되었습니다",
|
||||
"Models imported successfully": "",
|
||||
"Models imported successfully": "모델을 성공적으로 가져왔습니다.",
|
||||
"Models Public Sharing": "모델 공개 공유",
|
||||
"Mojeek Search API Key": "Mojeek Search API 키",
|
||||
"More": "더보기",
|
||||
|
|
@ -1246,7 +1246,7 @@
|
|||
"Please enter a valid URL.": "올바른 URL을 입력하세요.",
|
||||
"Please fill in all fields.": "모두 빈칸없이 채워주세요",
|
||||
"Please register the OAuth client": "OAuth clith를 등록해주세요",
|
||||
"Please save the connection to persist the OAuth client information and do not change the ID": "",
|
||||
"Please save the connection to persist the OAuth client information and do not change the ID": "OAuth 클라이언트 정보를 저장하려면 연결을 저장하고 ID를 변경하지 마세요.",
|
||||
"Please select a model first.": "먼저 모델을 선택하세요.",
|
||||
"Please select a model.": "모델을 선택하세요.",
|
||||
"Please select a reason": "이유를 선택해주세요",
|
||||
|
|
@ -1257,7 +1257,7 @@
|
|||
"Prefer not to say": "언급하고 싶지 않습니다.",
|
||||
"Prefix ID": "Prefix ID",
|
||||
"Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable": "Prefix ID는 모델 ID에 접두사를 추가하여 다른 연결과의 충돌을 방지하는 데 사용됩니다. - 비활성화하려면 비워 둡니다.",
|
||||
"Prevent File Creation": "",
|
||||
"Prevent File Creation": "파일 생성 방지",
|
||||
"Preview": "미리보기",
|
||||
"Previous 30 days": "이전 30일",
|
||||
"Previous 7 days": "이전 7일",
|
||||
|
|
@ -1281,7 +1281,7 @@
|
|||
"Pull Model": "모델 pull",
|
||||
"pypdfium2": "",
|
||||
"Query Generation Prompt": "쿼리 생성 프롬프트",
|
||||
"Querying": "",
|
||||
"Querying": "쿼리 진행중",
|
||||
"Quick Actions": "빠른 작업",
|
||||
"RAG Template": "RAG 템플릿",
|
||||
"Rating": "평가",
|
||||
|
|
@ -1367,7 +1367,7 @@
|
|||
"Scroll On Branch Change": "브랜치 변경 시 스크롤",
|
||||
"Search": "검색",
|
||||
"Search a model": "모델 검색",
|
||||
"Search all emojis": "",
|
||||
"Search all emojis": "모든 이모지 검색",
|
||||
"Search Base": "검색 기반",
|
||||
"Search Chats": "채팅 검색",
|
||||
"Search Collection": "컬렉션 검색",
|
||||
|
|
@ -1470,7 +1470,7 @@
|
|||
"Show Formatting Toolbar": "서식 툴바 표시",
|
||||
"Show image preview": "이미지 미리보기",
|
||||
"Show Model": "모델 보기",
|
||||
"Show Shortcuts": "",
|
||||
"Show Shortcuts": "단축키 보기",
|
||||
"Show your support!": "당신의 응원을 보내주세요!",
|
||||
"Showcased creativity": "창의성 발휘",
|
||||
"Sign in": "로그인",
|
||||
|
|
@ -1499,14 +1499,14 @@
|
|||
"Speech-to-Text": "음성-텍스트 변환",
|
||||
"Speech-to-Text Engine": "음성-텍스트 변환 엔진",
|
||||
"standard": "",
|
||||
"Start a new conversation": "",
|
||||
"Start a new conversation": "새 대화 시작",
|
||||
"Start of the channel": "채널 시작",
|
||||
"Start Tag": "시작 태그",
|
||||
"Status Updates": "상태 업데이트",
|
||||
"STDOUT/STDERR": "STDOUT/STDERR",
|
||||
"Steps": "",
|
||||
"Stop": "정지",
|
||||
"Stop Generating": "",
|
||||
"Stop Generating": "생성 중지",
|
||||
"Stop Sequence": "중지 시퀀스",
|
||||
"Stream Chat Response": "스트림 채팅 응답",
|
||||
"Stream Delta Chunk Size": "스트림 델타 청크 크기",
|
||||
|
|
@ -1530,7 +1530,7 @@
|
|||
"System": "시스템",
|
||||
"System Instructions": "시스템 지침",
|
||||
"System Prompt": "시스템 프롬프트",
|
||||
"Table Mode": "",
|
||||
"Table Mode": "테이블 모드",
|
||||
"Tag": "",
|
||||
"Tags": "태그",
|
||||
"Tags Generation": "태그 생성",
|
||||
|
|
@ -1578,7 +1578,7 @@
|
|||
"This chat won't appear in history and your messages will not be saved.": "이 채팅은 기록에 나타나지 않으며 메시지가 저장되지 않습니다.",
|
||||
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "이렇게 하면 소중한 대화 내용이 백엔드 데이터베이스에 안전하게 저장됩니다. 감사합니다!",
|
||||
"This feature is experimental and may be modified or discontinued without notice.": "이 기능은 실험 중이며, 사전 통보 없이 수정되거나 중단될 수 있습니다.",
|
||||
"This is a default user permission and will remain enabled.": "",
|
||||
"This is a default user permission and will remain enabled.": "이것은 기본 사용자 권한이며 계속 활성화됩니다.",
|
||||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "이것은 실험적 기능으로, 예상대로 작동하지 않을 수 있으며 언제든지 변경될 수 있습니다.",
|
||||
"This model is not publicly available. Please select another model.": "이 모델은 공개적으로 사용할 수 없습니다. 다른 모델을 선택해주세요.",
|
||||
"This option controls how long the model will stay loaded into memory following the request (default: 5m)": "이 옵션은 요청 처리 후 모델이 메모리에 유지하는 시간을 제어합니다. (기본값: 5분)",
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@
|
|||
"Allow Continue Response": "允许继续生成回答",
|
||||
"Allow Delete Messages": "允许删除对话消息",
|
||||
"Allow File Upload": "允许上传文件",
|
||||
"Allow Group Sharing": "允许群组内共享",
|
||||
"Allow Multiple Models in Chat": "允许在对话中使用多个模型",
|
||||
"Allow non-local voices": "允许调用非本土音色",
|
||||
"Allow Rate Response": "允许对回答进行评价",
|
||||
|
|
@ -388,6 +389,7 @@
|
|||
"Default description enabled": "默认描述已启用",
|
||||
"Default Features": "默认功能",
|
||||
"Default Filters": "默认过滤器",
|
||||
"Default Group": "默认权限组",
|
||||
"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model's built-in tool-calling capabilities, but requires the model to inherently support this feature.": "“默认”模式会在请求模型前调用工具,因此能兼容更多模型。“原生”模式则依赖模型本身的工具调用能力,但需要模型本身支持该功能。",
|
||||
"Default Model": "默认模型",
|
||||
"Default model updated": "默认模型已更新",
|
||||
|
|
@ -731,6 +733,7 @@
|
|||
"Features": "功能",
|
||||
"Features Permissions": "功能权限",
|
||||
"February": "二月",
|
||||
"Feedback deleted successfully": "反馈删除成功",
|
||||
"Feedback Details": "反馈详情",
|
||||
"Feedback History": "历史反馈",
|
||||
"Feedbacks": "反馈",
|
||||
|
|
@ -745,6 +748,7 @@
|
|||
"File size should not exceed {{maxSize}} MB.": "文件大小不应超过 {{maxSize}} MB",
|
||||
"File Upload": "文件上传",
|
||||
"File uploaded successfully": "文件上传成功",
|
||||
"File uploaded!": "文件上传成功",
|
||||
"Files": "文件",
|
||||
"Filter": "过滤",
|
||||
"Filter is now globally disabled": "过滤器已全局禁用",
|
||||
|
|
@ -858,6 +862,7 @@
|
|||
"Image Compression": "压缩图像",
|
||||
"Image Compression Height": "压缩图像高度",
|
||||
"Image Compression Width": "压缩图像宽度",
|
||||
"Image Edit": "图片编辑",
|
||||
"Image Edit Engine": "图片编辑引擎",
|
||||
"Image Generation": "图像生成",
|
||||
"Image Generation Engine": "图像生成引擎",
|
||||
|
|
@ -942,6 +947,7 @@
|
|||
"Knowledge Name": "知识库名称",
|
||||
"Knowledge Public Sharing": "公开分享知识库",
|
||||
"Knowledge reset successfully.": "知识库重置成功",
|
||||
"Knowledge Sharing": "分享知识库",
|
||||
"Knowledge updated successfully": "知识库更新成功",
|
||||
"Kokoro.js (Browser)": "Kokoro.js(运行于用户浏览器)",
|
||||
"Kokoro.js Dtype": "Kokoro.js Dtype",
|
||||
|
|
@ -1062,7 +1068,8 @@
|
|||
"Models Access": "访问模型列表",
|
||||
"Models configuration saved successfully": "模型配置保存成功",
|
||||
"Models imported successfully": "已成功导入模型配置",
|
||||
"Models Public Sharing": "模型公开共享",
|
||||
"Models Public Sharing": "模型公开分享",
|
||||
"Models Sharing": "分享模型",
|
||||
"Mojeek Search API Key": "Mojeek Search 接口密钥",
|
||||
"More": "更多",
|
||||
"More Concise": "精炼表达",
|
||||
|
|
@ -1129,6 +1136,7 @@
|
|||
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "注意:如果设置了最低分数,搜索结果只会返回分数大于或等于最低分数的文档。",
|
||||
"Notes": "笔记",
|
||||
"Notes Public Sharing": "公开分享笔记",
|
||||
"Notes Sharing": "分享笔记",
|
||||
"Notification Sound": "通知提示音",
|
||||
"Notification Webhook": "通知 Webhook",
|
||||
"Notifications": "桌面通知",
|
||||
|
|
@ -1274,7 +1282,8 @@
|
|||
"Prompt updated successfully": "提示词更新成功",
|
||||
"Prompts": "提示词",
|
||||
"Prompts Access": "访问提示词",
|
||||
"Prompts Public Sharing": "提示词公开共享",
|
||||
"Prompts Public Sharing": "提示词公开分享",
|
||||
"Prompts Sharing": "分享提示词",
|
||||
"Provider Type": "服务提供商类型",
|
||||
"Public": "公共",
|
||||
"Pull \"{{searchValue}}\" from Ollama.com": "从 Ollama.com 下载 “{{searchValue}}”",
|
||||
|
|
@ -1457,6 +1466,7 @@
|
|||
"Sets the random number seed to use for generation. Setting this to a specific number will make the model generate the same text for the same prompt.": "设置用于内容生成的随机数种子。将其设置为特定数字可使模型针对同一提示词生成相同的文本。",
|
||||
"Sets the size of the context window used to generate the next token.": "设置用于生成下一个 Token 的上下文窗口的大小。",
|
||||
"Sets the stop sequences to use. When this pattern is encountered, the LLM will stop generating text and return. Multiple stop patterns may be set by specifying multiple separate stop parameters in a modelfile.": "设置要使用的停止序列。在该模式下,大语言模型将停止生成文本并返回。可以通过在模型文件中指定多个单独的停止参数来设置多个停止模式。",
|
||||
"Setting": "设置",
|
||||
"Settings": "设置",
|
||||
"Settings saved successfully!": "设置已成功保存!",
|
||||
"Share": "分享",
|
||||
|
|
@ -1636,7 +1646,8 @@
|
|||
"Tools are a function calling system with arbitrary code execution": "工具是一个支持任意代码执行的函数调用系统",
|
||||
"Tools Function Calling Prompt": "工具函数调用提示词",
|
||||
"Tools have a function calling system that allows arbitrary code execution.": "注意:工具有权执行任意代码",
|
||||
"Tools Public Sharing": "工具公开共享",
|
||||
"Tools Public Sharing": "工具公开分享",
|
||||
"Tools Sharing": "分享工具",
|
||||
"Top K": "Top K",
|
||||
"Top K Reranker": "Top K Reranker",
|
||||
"Transformers": "Transformers",
|
||||
|
|
@ -1684,6 +1695,7 @@
|
|||
"Upload Pipeline": "上传 Pipeline",
|
||||
"Upload Progress": "上传进度",
|
||||
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "上传进度:{{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)",
|
||||
"Uploading file...": "正在上传文件...",
|
||||
"URL": "URL",
|
||||
"URL is required": "URL 是必填项。",
|
||||
"URL Mode": "URL 模式",
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@
|
|||
"Allow Continue Response": "允許繼續回應",
|
||||
"Allow Delete Messages": "允許刪除訊息",
|
||||
"Allow File Upload": "允許上傳檔案",
|
||||
"Allow Group Sharing": "允許在群組內分享",
|
||||
"Allow Multiple Models in Chat": "允許在對話中使用多個模型",
|
||||
"Allow non-local voices": "允許非本機語音",
|
||||
"Allow Rate Response": "允許為回應評分",
|
||||
|
|
@ -388,6 +389,7 @@
|
|||
"Default description enabled": "預設描述已啟用",
|
||||
"Default Features": "預設功能",
|
||||
"Default Filters": "預設篩選器",
|
||||
"Default Group": "預設群組",
|
||||
"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model's built-in tool-calling capabilities, but requires the model to inherently support this feature.": "預設模式透過在執行前呼叫工具一次,來與更廣泛的模型相容。原生模式則利用模型內建的工具呼叫能力,但需要模型本身就支援此功能。",
|
||||
"Default Model": "預設模型",
|
||||
"Default model updated": "預設模型已更新",
|
||||
|
|
@ -731,6 +733,7 @@
|
|||
"Features": "功能",
|
||||
"Features Permissions": "功能權限",
|
||||
"February": "2 月",
|
||||
"Feedback deleted successfully": "成功刪除回饋",
|
||||
"Feedback Details": "回饋詳情",
|
||||
"Feedback History": "回饋歷史",
|
||||
"Feedbacks": "回饋",
|
||||
|
|
@ -745,6 +748,7 @@
|
|||
"File size should not exceed {{maxSize}} MB.": "檔案大小不應超過 {{maxSize}} MB。",
|
||||
"File Upload": "檔案上傳",
|
||||
"File uploaded successfully": "成功上傳檔案",
|
||||
"File uploaded!": "檔案已上傳",
|
||||
"Files": "檔案",
|
||||
"Filter": "篩選",
|
||||
"Filter is now globally disabled": "篩選器已全域停用",
|
||||
|
|
@ -858,6 +862,7 @@
|
|||
"Image Compression": "圖片壓縮",
|
||||
"Image Compression Height": "圖片壓縮高度",
|
||||
"Image Compression Width": "圖片壓縮寬度",
|
||||
"Image Edit": "圖片編輯",
|
||||
"Image Edit Engine": "圖片編輯引擎",
|
||||
"Image Generation": "圖片生成",
|
||||
"Image Generation Engine": "圖片生成引擎",
|
||||
|
|
@ -933,16 +938,17 @@
|
|||
"Key is required": "金鑰為必填項目",
|
||||
"Keyboard shortcuts": "鍵盤快捷鍵",
|
||||
"Keyboard Shortcuts": "鍵盤快捷鍵",
|
||||
"Knowledge": "知識",
|
||||
"Knowledge Access": "知識存取",
|
||||
"Knowledge": "知識庫",
|
||||
"Knowledge Access": "知識庫存取",
|
||||
"Knowledge Base": "知識庫",
|
||||
"Knowledge created successfully.": "成功建立知識。",
|
||||
"Knowledge deleted successfully.": "成功刪除知識。",
|
||||
"Knowledge created successfully.": "成功建立知識庫。",
|
||||
"Knowledge deleted successfully.": "成功刪除知識庫。",
|
||||
"Knowledge Description": "知識庫描述",
|
||||
"Knowledge Name": "知識庫名稱",
|
||||
"Knowledge Public Sharing": "知識公開分享",
|
||||
"Knowledge reset successfully.": "成功重設知識。",
|
||||
"Knowledge updated successfully": "成功更新知識",
|
||||
"Knowledge Public Sharing": "知識庫公開分享",
|
||||
"Knowledge reset successfully.": "成功重設知識庫。",
|
||||
"Knowledge Sharing": "分享知識庫",
|
||||
"Knowledge updated successfully": "成功更新知識庫",
|
||||
"Kokoro.js (Browser)": "Kokoro.js (瀏覽器)",
|
||||
"Kokoro.js Dtype": "Kokoro.js Dtype",
|
||||
"Label": "標籤",
|
||||
|
|
@ -1063,6 +1069,7 @@
|
|||
"Models configuration saved successfully": "成功儲存模型設定",
|
||||
"Models imported successfully": "已成功匯入模型",
|
||||
"Models Public Sharing": "模型公開分享",
|
||||
"Models Sharing": "分享模型",
|
||||
"Mojeek Search API Key": "Mojeek 搜尋 API 金鑰",
|
||||
"More": "更多",
|
||||
"More Concise": "精煉表達",
|
||||
|
|
@ -1129,6 +1136,7 @@
|
|||
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "注意:如果您設定了最低分數,則搜尋只會回傳分數大於或等於最低分數的檔案。",
|
||||
"Notes": "筆記",
|
||||
"Notes Public Sharing": "公開分享筆記",
|
||||
"Notes Sharing": "分享筆記",
|
||||
"Notification Sound": "通知聲音",
|
||||
"Notification Webhook": "通知 Webhook",
|
||||
"Notifications": "通知",
|
||||
|
|
@ -1275,6 +1283,7 @@
|
|||
"Prompts": "提示詞",
|
||||
"Prompts Access": "提示詞存取",
|
||||
"Prompts Public Sharing": "提示詞公開分享",
|
||||
"Prompts Sharing": "分享提示詞",
|
||||
"Provider Type": "服務提供商類型",
|
||||
"Public": "公開",
|
||||
"Pull \"{{searchValue}}\" from Ollama.com": "從 Ollama.com 下載「{{searchValue}}」",
|
||||
|
|
@ -1457,6 +1466,7 @@
|
|||
"Sets the random number seed to use for generation. Setting this to a specific number will make the model generate the same text for the same prompt.": "設定用於生成的隨機數種子。將其設定為特定數字將使模型針對相同的提示生成相同的文字。",
|
||||
"Sets the size of the context window used to generate the next token.": "設定用於生成下一個 token 的上下文視窗大小。",
|
||||
"Sets the stop sequences to use. When this pattern is encountered, the LLM will stop generating text and return. Multiple stop patterns may be set by specifying multiple separate stop parameters in a modelfile.": "設定要使用的停止序列。當遇到此模式時,大型語言模型將停止生成文字並返回。可以在模型檔案中指定多個單獨的停止參數來設定多個停止模式。",
|
||||
"Setting": "設定",
|
||||
"Settings": "設定",
|
||||
"Settings saved successfully!": "設定已成功儲存!",
|
||||
"Share": "分享",
|
||||
|
|
@ -1637,6 +1647,7 @@
|
|||
"Tools Function Calling Prompt": "工具函式呼叫提示詞",
|
||||
"Tools have a function calling system that allows arbitrary code execution.": "工具具有允許執行任意程式碼的函式呼叫系統。",
|
||||
"Tools Public Sharing": "工具公開分享",
|
||||
"Tools Sharing": "分享工具",
|
||||
"Top K": "Top K",
|
||||
"Top K Reranker": "Top K Reranker",
|
||||
"Transformers": "Transformers",
|
||||
|
|
@ -1684,6 +1695,7 @@
|
|||
"Upload Pipeline": "上傳管線",
|
||||
"Upload Progress": "上傳進度",
|
||||
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "上傳進度:{{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)",
|
||||
"Uploading file...": "正在上傳檔案...",
|
||||
"URL": "URL",
|
||||
"URL is required": "URL 為必填項目",
|
||||
"URL Mode": "URL 模式",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ import emojiShortCodes from '$lib/emoji-shortcodes.json';
|
|||
|
||||
// Backend
|
||||
export const WEBUI_NAME = writable(APP_NAME);
|
||||
|
||||
export const WEBUI_VERSION = writable(null);
|
||||
export const WEBUI_DEPLOYMENT_ID = writable(null);
|
||||
|
||||
export const config: Writable<Config | undefined> = writable(undefined);
|
||||
export const user: Writable<SessionUser | undefined> = writable(undefined);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
import Notes from '$lib/components/notes/Notes.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Sidebar from '$lib/components/icons/Sidebar.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
let loaded = false;
|
||||
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
>
|
||||
<div class=" self-center">
|
||||
<img
|
||||
src={$user?.profile_image_url}
|
||||
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
||||
class="size-6 object-cover rounded-full"
|
||||
alt="User profile"
|
||||
draggable="false"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
theme,
|
||||
WEBUI_NAME,
|
||||
WEBUI_VERSION,
|
||||
WEBUI_DEPLOYMENT_ID,
|
||||
mobile,
|
||||
socket,
|
||||
chatId,
|
||||
|
|
@ -46,7 +47,7 @@
|
|||
import { getAllTags, getChatList } from '$lib/apis/chats';
|
||||
import { chatCompletion } from '$lib/apis/openai';
|
||||
|
||||
import { WEBUI_BASE_URL, WEBUI_HOSTNAME } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL, WEBUI_HOSTNAME } from '$lib/constants';
|
||||
import { bestMatchingLanguage } from '$lib/utils';
|
||||
import { setTextScale } from '$lib/utils/text-scale';
|
||||
|
||||
|
|
@ -54,10 +55,26 @@
|
|||
import AppSidebar from '$lib/components/app/AppSidebar.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import { getUserSettings } from '$lib/apis/users';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const unregisterServiceWorkers = async () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(registrations.map((r) => r.unregister()));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error unregistering service workers:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// handle frontend updates (https://svelte.dev/docs/kit/configuration#version)
|
||||
beforeNavigate(({ willUnload, to }) => {
|
||||
beforeNavigate(async ({ willUnload, to }) => {
|
||||
if (updated.current && !willUnload && to?.url) {
|
||||
await unregisterServiceWorkers();
|
||||
location.href = to.url.href;
|
||||
}
|
||||
});
|
||||
|
|
@ -91,15 +108,30 @@
|
|||
|
||||
_socket.on('connect', async () => {
|
||||
console.log('connected', _socket.id);
|
||||
const version = await getVersion(localStorage.token);
|
||||
if (version !== null) {
|
||||
if ($WEBUI_VERSION !== null && version !== $WEBUI_VERSION) {
|
||||
const res = await getVersion(localStorage.token);
|
||||
|
||||
const deploymentId = res?.deployment_id ?? null;
|
||||
const version = res?.version ?? null;
|
||||
|
||||
if (version !== null || deploymentId !== null) {
|
||||
if (
|
||||
($WEBUI_VERSION !== null && version !== $WEBUI_VERSION) ||
|
||||
($WEBUI_DEPLOYMENT_ID !== null && deploymentId !== $WEBUI_DEPLOYMENT_ID)
|
||||
) {
|
||||
await unregisterServiceWorkers();
|
||||
location.href = location.href;
|
||||
} else {
|
||||
WEBUI_VERSION.set(version);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (deploymentId !== null) {
|
||||
WEBUI_DEPLOYMENT_ID.set(deploymentId);
|
||||
}
|
||||
|
||||
if (version !== null) {
|
||||
WEBUI_VERSION.set(version);
|
||||
}
|
||||
|
||||
console.log('version', version);
|
||||
|
||||
if (localStorage.getItem('token')) {
|
||||
|
|
@ -457,7 +489,7 @@
|
|||
if ($settings?.notificationEnabled ?? false) {
|
||||
new Notification(`${data?.user?.name} (#${event?.channel?.name}) • Open WebUI`, {
|
||||
body: data?.content,
|
||||
icon: data?.user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
icon: `${WEBUI_API_BASE_URL}/users/${data?.user?.id}/profile/image`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -637,6 +669,7 @@
|
|||
? backendConfig.default_locale
|
||||
: bestMatchingLanguage(languages, browserLanguages, 'en-US');
|
||||
changeLanguage(lang);
|
||||
dayjs.locale(lang);
|
||||
}
|
||||
|
||||
if (backendConfig) {
|
||||
|
|
|
|||
BIN
static/doge.png
BIN
static/doge.png
Binary file not shown.
|
Before Width: | Height: | Size: 394 KiB |
Loading…
Reference in a new issue