update with upstream dev 21-11-2025

This commit is contained in:
Diego 2025-11-21 09:27:22 +01:00
commit b6947c9813
75 changed files with 1576 additions and 760 deletions

View file

@ -1203,6 +1203,12 @@ DEFAULT_USER_ROLE = PersistentConfig(
os.getenv("DEFAULT_USER_ROLE", "pending"), 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 = PersistentConfig(
"PENDING_USER_OVERLAY_TITLE", "PENDING_USER_OVERLAY_TITLE",
"ui.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" 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 = ( USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = (
os.environ.get( os.environ.get(
"USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING", "False" "USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING", "False"
@ -1277,8 +1289,10 @@ USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = (
== "true" == "true"
) )
USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = ( USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = (
os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING", "False").lower() os.environ.get(
"USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING", "False"
).lower()
== "true" == "true"
) )
@ -1289,6 +1303,11 @@ USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = (
== "true" == "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 = ( USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = (
os.environ.get( os.environ.get(
"USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING", "False" "USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING", "False"
@ -1296,6 +1315,12 @@ USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = (
== "true" == "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 = ( USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = (
os.environ.get( os.environ.get(
"USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING", "False" "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 = ( USER_PERMISSIONS_CHAT_CONTROLS = (
os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true" os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true"
) )
@ -1425,10 +1461,15 @@ DEFAULT_USER_PERMISSIONS = {
"tools_export": USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT, "tools_export": USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT,
}, },
"sharing": { "sharing": {
"models": USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING,
"public_models": USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_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, "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, "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, "public_tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING,
"notes": USER_PERMISSIONS_NOTES_ALLOW_SHARING,
"public_notes": USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING, "public_notes": USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
}, },
"chat": { "chat": {
@ -2145,6 +2186,11 @@ ENABLE_QDRANT_MULTITENANCY_MODE = (
) )
QDRANT_COLLECTION_PREFIX = os.environ.get("QDRANT_COLLECTION_PREFIX", "open-webui") 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
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200") OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
OPENSEARCH_SSL = os.environ.get("OPENSEARCH_SSL", "true").lower() == "true" 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", ""), 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 = PersistentConfig(
"IMAGE_EDIT_ENGINE", "IMAGE_EDIT_ENGINE",

View file

@ -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_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_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." "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." 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." 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): class TASKS(str, Enum):
def __str__(self) -> str: def __str__(self) -> str:

View file

@ -8,6 +8,8 @@ import shutil
from uuid import uuid4 from uuid import uuid4
from pathlib import Path from pathlib import Path
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
import re
import markdown import markdown
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@ -135,6 +137,9 @@ else:
PACKAGE_DATA = {"version": "0.0.0"} PACKAGE_DATA = {"version": "0.0.0"}
VERSION = PACKAGE_DATA["version"] VERSION = PACKAGE_DATA["version"]
DEPLOYMENT_ID = os.environ.get("DEPLOYMENT_ID", "")
INSTANCE_ID = os.environ.get("INSTANCE_ID", str(uuid4())) 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 = ( BYPASS_MODEL_ACCESS_CONTROL = (
os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true" os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
) )

View file

@ -164,6 +164,7 @@ from open_webui.config import (
IMAGES_GEMINI_API_BASE_URL, IMAGES_GEMINI_API_BASE_URL,
IMAGES_GEMINI_API_KEY, IMAGES_GEMINI_API_KEY,
IMAGES_GEMINI_ENDPOINT_METHOD, IMAGES_GEMINI_ENDPOINT_METHOD,
ENABLE_IMAGE_EDIT,
IMAGE_EDIT_ENGINE, IMAGE_EDIT_ENGINE,
IMAGE_EDIT_MODEL, IMAGE_EDIT_MODEL,
IMAGE_EDIT_SIZE, IMAGE_EDIT_SIZE,
@ -369,6 +370,7 @@ from open_webui.config import (
BYPASS_ADMIN_ACCESS_CONTROL, BYPASS_ADMIN_ACCESS_CONTROL,
USER_PERMISSIONS, USER_PERMISSIONS,
DEFAULT_USER_ROLE, DEFAULT_USER_ROLE,
DEFAULT_GROUP_ID,
PENDING_USER_OVERLAY_CONTENT, PENDING_USER_OVERLAY_CONTENT,
PENDING_USER_OVERLAY_TITLE, PENDING_USER_OVERLAY_TITLE,
DEFAULT_PROMPT_SUGGESTIONS, DEFAULT_PROMPT_SUGGESTIONS,
@ -455,6 +457,7 @@ from open_webui.env import (
SAFE_MODE, SAFE_MODE,
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
VERSION, VERSION,
DEPLOYMENT_ID,
INSTANCE_ID, INSTANCE_ID,
WEBUI_BUILD_HASH, WEBUI_BUILD_HASH,
WEBUI_SECRET_KEY, 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_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.config.DEFAULT_GROUP_ID = DEFAULT_GROUP_ID
app.state.config.PENDING_USER_OVERLAY_CONTENT = PENDING_USER_OVERLAY_CONTENT app.state.config.PENDING_USER_OVERLAY_CONTENT = PENDING_USER_OVERLAY_CONTENT
app.state.config.PENDING_USER_OVERLAY_TITLE = PENDING_USER_OVERLAY_TITLE 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.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_ENGINE = IMAGE_EDIT_ENGINE
app.state.config.IMAGE_EDIT_MODEL = IMAGE_EDIT_MODEL app.state.config.IMAGE_EDIT_MODEL = IMAGE_EDIT_MODEL
app.state.config.IMAGE_EDIT_SIZE = IMAGE_EDIT_SIZE 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": if "pipeline" in model and model["pipeline"].get("type", None) == "filter":
continue 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: try:
model_tags = [ model_tags = [
tag.get("name") 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(): async def get_app_version():
return { return {
"version": VERSION, "version": VERSION,
"deployment_id": DEPLOYMENT_ID,
} }

View file

@ -4,7 +4,7 @@ import uuid
from typing import Optional from typing import Optional
from open_webui.internal.db import Base, get_db 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 open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
@ -92,6 +92,28 @@ class FeedbackForm(BaseModel):
model_config = ConfigDict(extra="allow") 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: class FeedbackTable:
def insert_new_feedback( def insert_new_feedback(
self, user_id: str, form_data: FeedbackForm self, user_id: str, form_data: FeedbackForm
@ -143,6 +165,70 @@ class FeedbackTable:
except Exception: except Exception:
return None 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]: def get_all_feedbacks(self) -> list[FeedbackModel]:
with get_db() as db: with get_db() as db:
return [ return [

View file

@ -101,6 +101,7 @@ class GroupForm(BaseModel):
name: str name: str
description: str description: str
permissions: Optional[dict] = None permissions: Optional[dict] = None
data: Optional[dict] = None
class UserIdsForm(BaseModel): class UserIdsForm(BaseModel):

View file

@ -244,11 +244,9 @@ class ModelsTable:
try: try:
with get_db() as db: with get_db() as db:
# update only the fields that are present in the model # update only the fields that are present in the model
result = ( data = model.model_dump(exclude={"id"})
db.query(Model) result = db.query(Model).filter_by(id=id).update(data)
.filter_by(id=id)
.update(model.model_dump(exclude={"id"}))
)
db.commit() db.commit()
model = db.get(Model, id) model = db.get(Model, id)

View 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

View file

@ -67,6 +67,10 @@ class Vector:
from open_webui.retrieval.vector.dbs.oracle23ai import Oracle23aiClient from open_webui.retrieval.vector.dbs.oracle23ai import Oracle23aiClient
return Oracle23aiClient() return Oracle23aiClient()
case VectorType.WEAVIATE:
from open_webui.retrieval.vector.dbs.weaviate import WeaviateClient
return WeaviateClient()
case _: case _:
raise ValueError(f"Unsupported vector type: {vector_type}") raise ValueError(f"Unsupported vector type: {vector_type}")

View file

@ -11,3 +11,4 @@ class VectorType(StrEnum):
PGVECTOR = "pgvector" PGVECTOR = "pgvector"
ORACLE23AI = "oracle23ai" ORACLE23AI = "oracle23ai"
S3VECTOR = "s3vector" S3VECTOR = "s3vector"
WEAVIATE = "weaviate"

View file

@ -35,6 +35,7 @@ from pydantic import BaseModel
from open_webui.utils.auth import get_admin_user, get_verified_user 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 ( from open_webui.config import (
WHISPER_MODEL_AUTO_UPDATE, WHISPER_MODEL_AUTO_UPDATE,
WHISPER_MODEL_DIR, 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 {}), **(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( r = await session.post(
url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech", url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
json=payload, json=payload,
headers={ headers=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 {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL, ssl=AIOHTTP_CLIENT_SESSION_SSL,
) )
@ -570,7 +565,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path) 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) filename = os.path.basename(file_path)
file_dir = os.path.dirname(file_path) file_dir = os.path.dirname(file_path)
id = filename.split(".")[0] id = filename.split(".")[0]
@ -621,11 +616,15 @@ def transcription_handler(request, file_path, metadata):
if language: if language:
payload["language"] = 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( r = requests.post(
url=f"{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions", url=f"{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
headers={ headers=headers,
"Authorization": f"Bearer {request.app.state.config.STT_OPENAI_API_KEY}"
},
files={"file": (filename, open(file_path, "rb"))}, files={"file": (filename, open(file_path, "rb"))},
data=payload, 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}") log.info(f"transcribe: {file_path} {metadata}")
if is_audio_conversion_required(file_path): 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: with ThreadPoolExecutor() as executor:
# Submit tasks for each chunk_path # Submit tasks for each chunk_path
futures = [ futures = [
executor.submit(transcription_handler, request, chunk_path, metadata) executor.submit(transcription_handler, request, chunk_path, metadata, user)
for chunk_path in chunk_paths for chunk_path in chunk_paths
] ]
# Gather results as they complete # Gather results as they complete
@ -1189,7 +1188,7 @@ def transcription(
if language: if language:
metadata = {"language": language} metadata = {"language": language}
result = transcribe(request, file_path, metadata) result = transcribe(request, file_path, metadata, user)
return { return {
**result, **result,

View file

@ -45,6 +45,7 @@ from pydantic import BaseModel
from open_webui.utils.misc import parse_duration, validate_email_format from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.auth import ( from open_webui.utils.auth import (
validate_password,
verify_password, verify_password,
decode_token, decode_token,
invalidate_token, invalidate_token,
@ -181,10 +182,14 @@ async def update_password(
) )
if user: 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) hashed = get_password_hash(form_data.new_password)
return Auths.update_user_password_by_id(user.id, hashed) return Auths.update_user_password_by_id(user.id, hashed)
else: else:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_PASSWORD) raise HTTPException(400, detail=ERROR_MESSAGES.INCORRECT_PASSWORD)
else: else:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) 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) raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
try: try:
role = "admin" if not has_users else request.app.state.config.DEFAULT_USER_ROLE try:
validate_password(form_data.password)
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing. except Exception as e:
if len(form_data.password.encode("utf-8")) > 72: raise HTTPException(400, detail=str(e))
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.PASSWORD_TOO_LONG,
)
hashed = get_password_hash(form_data.password) 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( user = Auths.insert_new_auth(
form_data.email.lower(), form_data.email.lower(),
hashed, hashed,
@ -692,6 +695,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
# Disable signup after the first user is created # Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False 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 { return {
"token": token, "token": token,
"token_type": "Bearer", "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) raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
try: try:
try:
validate_password(form_data.password)
except Exception as e:
raise HTTPException(400, detail=str(e))
hashed = get_password_hash(form_data.password) hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth( user = Auths.insert_new_auth(
form_data.email.lower(), 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, "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, "API_KEYS_ALLOWED_ENDPOINTS": request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE, "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, "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING, "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING, "ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
@ -900,6 +913,7 @@ class AdminConfig(BaseModel):
ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS: bool ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS: bool
API_KEYS_ALLOWED_ENDPOINTS: str API_KEYS_ALLOWED_ENDPOINTS: str
DEFAULT_USER_ROLE: str DEFAULT_USER_ROLE: str
DEFAULT_GROUP_ID: str
JWT_EXPIRES_IN: str JWT_EXPIRES_IN: str
ENABLE_COMMUNITY_SHARING: bool ENABLE_COMMUNITY_SHARING: bool
ENABLE_MESSAGE_RATING: bool ENABLE_MESSAGE_RATING: bool
@ -933,6 +947,8 @@ async def update_admin_config(
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]: 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_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))$" pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"
# Check if the input string matches the pattern # 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, "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, "API_KEYS_ALLOWED_ENDPOINTS": request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE, "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, "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING, "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING, "ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,

View file

@ -7,6 +7,8 @@ from open_webui.models.feedbacks import (
FeedbackModel, FeedbackModel,
FeedbackResponse, FeedbackResponse,
FeedbackForm, FeedbackForm,
FeedbackUserResponse,
FeedbackListResponse,
Feedbacks, Feedbacks,
) )
@ -56,35 +58,10 @@ async def update_config(
} }
class UserResponse(BaseModel): @router.get("/feedbacks/all", response_model=list[FeedbackResponse])
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])
async def get_all_feedbacks(user=Depends(get_admin_user)): async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks() feedbacks = Feedbacks.get_all_feedbacks()
return 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
@router.delete("/feedbacks/all") @router.delete("/feedbacks/all")
@ -111,6 +88,31 @@ async def delete_feedbacks(user=Depends(get_verified_user)):
return success 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) @router.post("/feedback", response_model=FeedbackModel)
async def create_feedback( async def create_feedback(
request: Request, request: Request,

View file

@ -102,7 +102,7 @@ def process_uploaded_file(request, file, file_path, file_item, file_metadata, us
) )
): ):
file_path = Storage.get_file(file_path) file_path = Storage.get_file(file_path)
result = transcribe(request, file_path, file_metadata) result = transcribe(request, file_path, file_metadata, user)
process_file( process_file(
request, request,

View file

@ -31,20 +31,32 @@ router = APIRouter()
@router.get("/", response_model=list[GroupResponse]) @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": if user.role == "admin":
groups = Groups.get_groups() groups = Groups.get_groups()
else: else:
groups = Groups.get_groups_by_member_id(user.id) groups = Groups.get_groups_by_member_id(user.id)
return [ group_list = []
GroupResponse(
**group.model_dump(), for group in groups:
member_count=Groups.get_group_member_count_by_id(group.id), 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
]
############################ ############################

View file

@ -126,6 +126,7 @@ class ImagesConfig(BaseModel):
IMAGES_GEMINI_API_KEY: str IMAGES_GEMINI_API_KEY: str
IMAGES_GEMINI_ENDPOINT_METHOD: str IMAGES_GEMINI_ENDPOINT_METHOD: str
ENABLE_IMAGE_EDIT: bool
IMAGE_EDIT_ENGINE: str IMAGE_EDIT_ENGINE: str
IMAGE_EDIT_MODEL: str IMAGE_EDIT_MODEL: str
IMAGE_EDIT_SIZE: Optional[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_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_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY,
"IMAGES_GEMINI_ENDPOINT_METHOD": request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD, "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_ENGINE": request.app.state.config.IMAGE_EDIT_ENGINE,
"IMAGE_EDIT_MODEL": request.app.state.config.IMAGE_EDIT_MODEL, "IMAGE_EDIT_MODEL": request.app.state.config.IMAGE_EDIT_MODEL,
"IMAGE_EDIT_SIZE": request.app.state.config.IMAGE_EDIT_SIZE, "IMAGE_EDIT_SIZE": request.app.state.config.IMAGE_EDIT_SIZE,
@ -253,6 +255,7 @@ async def update_config(
) )
# Edit Image # 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_ENGINE = form_data.IMAGE_EDIT_ENGINE
request.app.state.config.IMAGE_EDIT_MODEL = form_data.IMAGE_EDIT_MODEL request.app.state.config.IMAGE_EDIT_MODEL = form_data.IMAGE_EDIT_MODEL
request.app.state.config.IMAGE_EDIT_SIZE = form_data.IMAGE_EDIT_SIZE 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_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_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY,
"IMAGES_GEMINI_ENDPOINT_METHOD": request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD, "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_ENGINE": request.app.state.config.IMAGE_EDIT_ENGINE,
"IMAGE_EDIT_MODEL": request.app.state.config.IMAGE_EDIT_MODEL, "IMAGE_EDIT_MODEL": request.app.state.config.IMAGE_EDIT_MODEL,
"IMAGE_EDIT_SIZE": request.app.state.config.IMAGE_EDIT_SIZE, "IMAGE_EDIT_SIZE": request.app.state.config.IMAGE_EDIT_SIZE,

View file

@ -253,6 +253,7 @@ async def get_model_profile_image(id: str, user=Depends(get_verified_user)):
) )
except Exception as e: except Exception as e:
pass pass
return FileResponse(f"{STATIC_DIR}/favicon.png") return FileResponse(f"{STATIC_DIR}/favicon.png")
else: else:
return FileResponse(f"{STATIC_DIR}/favicon.png") return FileResponse(f"{STATIC_DIR}/favicon.png")
@ -320,7 +321,7 @@ async def update_model_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED, 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 return model

View file

@ -36,7 +36,12 @@ from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS, STATIC_DIR 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 from open_webui.utils.access_control import get_permissions, has_permission
@ -178,10 +183,15 @@ class WorkspacePermissions(BaseModel):
class SharingPermissions(BaseModel): class SharingPermissions(BaseModel):
public_models: bool = True models: bool = False
public_knowledge: bool = True public_models: bool = False
public_prompts: bool = True knowledge: bool = False
public_knowledge: bool = False
prompts: bool = False
public_prompts: bool = False
tools: bool = False
public_tools: bool = True public_tools: bool = True
notes: bool = False
public_notes: bool = True public_notes: bool = True
@ -497,8 +507,12 @@ async def update_user_by_id(
) )
if form_data.password: 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) hashed = get_password_hash(form_data.password)
log.debug(f"hashed: {hashed}")
Auths.update_user_password_by_id(user_id, hashed) Auths.update_user_password_by_id(user_id, hashed)
Auths.update_email_by_id(user_id, form_data.email.lower()) Auths.update_email_by_id(user_id, form_data.email.lower())

View file

@ -28,8 +28,10 @@ from open_webui.models.users import Users
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.env import ( from open_webui.env import (
ENABLE_PASSWORD_VALIDATION,
OFFLINE_MODE, OFFLINE_MODE,
LICENSE_BLOB, LICENSE_BLOB,
PASSWORD_VALIDATION_REGEX_PATTERN,
REDIS_KEY_PREFIX, REDIS_KEY_PREFIX,
pk, pk,
WEBUI_SECRET_KEY, 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") 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: def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash""" """Verify a password against its hash"""
return ( return (

View file

@ -791,42 +791,13 @@ async def chat_image_generation_handler(
input_images = get_last_images(message_list) input_images = get_last_images(message_list)
system_message_content = "" 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: try:
images = await image_generations( images = await image_edits(
request=request, request=request,
form_data=CreateImageForm(**{"prompt": prompt}), form_data=EditImageForm(**{"prompt": prompt, "image": input_images}),
user=user, 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>" 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: 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: try:
images = await image_edits( images = await image_generations(
request=request, request=request,
form_data=EditImageForm(**{"prompt": prompt, "image": input_images}), form_data=CreateImageForm(**{"prompt": prompt}),
user=user, user=user,
) )

View file

@ -49,6 +49,7 @@ langchain-community==0.3.29
fake-useragent==2.2.0 fake-useragent==2.2.0
chromadb==1.1.0 chromadb==1.1.0
weaviate-client==4.17.0
opensearch-py==2.8.0 opensearch-py==2.8.0
transformers transformers

View file

@ -147,6 +147,7 @@ all = [
"elasticsearch==9.1.0", "elasticsearch==9.1.0",
"qdrant-client==1.14.3", "qdrant-client==1.14.3",
"weaviate-client==4.17.0",
"pymilvus==2.6.2", "pymilvus==2.6.2",
"pinecone==6.0.2", "pinecone==6.0.2",
"oracledb==3.2.0", "oracledb==3.2.0",

View file

@ -93,6 +93,45 @@ export const getAllFeedbacks = async (token: string = '') => {
return res; 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 = '') => { export const exportAllFeedbacks = async (token: string = '') => {
let error = null; let error = null;

View file

@ -31,10 +31,15 @@ export const createNewGroup = async (token: string, group: object) => {
return res; return res;
}; };
export const getGroups = async (token: string = '') => { export const getGroups = async (token: string = '', share?: boolean) => {
let error = null; 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', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',

View file

@ -1425,7 +1425,7 @@ export const getVersion = async (token: string) => {
throw error; throw error;
} }
return res?.version ?? null; return res;
}; };
export const getVersionUpdates = async (token: string) => { export const getVersionUpdates = async (token: string) => {

View file

@ -33,7 +33,9 @@
let feedbacks = []; let feedbacks = [];
onMount(async () => { onMount(async () => {
// TODO: feedbacks elo rating calculation should be done in the backend; remove below line later
feedbacks = await getAllFeedbacks(localStorage.token); feedbacks = await getAllFeedbacks(localStorage.token);
loaded = true; loaded = true;
const containerElement = document.getElementById('users-tabs-container'); const containerElement = document.getElementById('users-tabs-container');
@ -117,7 +119,7 @@
{#if selectedTab === 'leaderboard'} {#if selectedTab === 'leaderboard'}
<Leaderboard {feedbacks} /> <Leaderboard {feedbacks} />
{:else if selectedTab === 'feedbacks'} {:else if selectedTab === 'feedbacks'}
<Feedbacks {feedbacks} /> <Feedbacks />
{/if} {/if}
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n'); 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 Tooltip from '$lib/components/common/Tooltip.svelte';
import Download from '$lib/components/icons/Download.svelte'; import Download from '$lib/components/icons/Download.svelte';
@ -23,78 +23,25 @@
import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.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'; import { config } from '$lib/stores';
import Spinner from '$lib/components/common/Spinner.svelte';
export let feedbacks = [];
let page = 1; let page = 1;
$: paginatedFeedbacks = sortedFeedbacks.slice((page - 1) * 10, page * 10); let items = null;
let total = null;
let orderBy: string = 'updated_at'; let orderBy: string = 'updated_at';
let direction: 'asc' | 'desc' = 'desc'; let direction: 'asc' | 'desc' = 'desc';
type Feedback = { const setSortKey = (key) => {
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) {
if (orderBy === key) { if (orderBy === key) {
direction = direction === 'asc' ? 'desc' : 'asc'; direction = direction === 'asc' ? 'desc' : 'asc';
} else { } else {
orderBy = key; orderBy = key;
if (key === 'user' || key === 'model_id') { direction = 'asc';
direction = 'asc';
} else {
direction = 'desc';
}
} }
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 showFeedbackModal = false;
let selectedFeedback = null; 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 deleteFeedbackHandler = async (feedbackId: string) => {
const response = await deleteFeedbackById(localStorage.token, feedbackId).catch((err) => { const response = await deleteFeedbackById(localStorage.token, feedbackId).catch((err) => {
toast.error(err); toast.error(err);
return null; return null;
}); });
if (response) { 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} /> <FeedbackModal bind:show={showFeedbackModal} {selectedFeedback} onClose={closeFeedbackModal} />
<div class="mt-0.5 mb-1 gap-1 flex flex-row justify-between"> {#if items === null || total === null}
<div class="flex md:self-center text-lg font-medium px-0.5"> <div class="my-10">
{$i18n.t('Feedback History')} <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> </div>
{#if feedbacks.length > 0} <div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
<div> {#if (items ?? []).length === 0}
<Tooltip content={$i18n.t('Export')}> <div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
<button {$i18n.t('No feedbacks found')}
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" </div>
on:click={() => { {:else}
exportHandler(); <table
}} class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full"
> >
<Download className="size-3" /> <thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
</button> <tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
</Tooltip> <th
</div> scope="col"
{/if} class="px-2.5 py-2 cursor-pointer select-none w-3"
</div> on:click={() => setSortKey('user')}
>
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full"> <div class="flex gap-1.5 items-center justify-end">
{#if (feedbacks ?? []).length === 0} {$i18n.t('User')}
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1"> {#if orderBy === 'user'}
{$i18n.t('No feedbacks found')} <span class="font-normal">
</div> {#if direction === 'asc'}
{:else} <ChevronUp className="size-2" />
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full"> {:else}
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200"> <ChevronDown className="size-2" />
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850"> {/if}
<th </span>
scope="col" {:else}
class="px-2.5 py-2 cursor-pointer select-none w-3" <span class="invisible">
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" /> <ChevronUp className="size-2" />
{:else} </span>
<ChevronDown className="size-2" /> {/if}
{/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>
</div> </div>
</td> </th>
<td class=" py-1 pl-3 flex flex-col"> <th
<div class="flex flex-col items-start gap-0.5 h-full"> scope="col"
<div class="flex flex-col h-full"> class="px-2.5 py-2 cursor-pointer select-none"
{#if feedback.data?.sibling_model_ids} on:click={() => setSortKey('model_id')}
<div class="font-semibold text-gray-600 dark:text-gray-400 flex-1"> >
{feedback.data?.model_id} <div class="flex gap-1.5 items-center">
</div> {$i18n.t('Models')}
{#if orderBy === 'model_id'}
<Tooltip content={feedback.data.sibling_model_ids.join(', ')}> <span class="font-normal">
<div class=" text-[0.65rem] text-gray-600 dark:text-gray-400 line-clamp-1"> {#if direction === 'asc'}
{#if feedback.data.sibling_model_ids.length > 2} <ChevronUp className="size-2" />
<!-- {$i18n.t('and {{COUNT}} more')} --> {:else}
{feedback.data.sibling_model_ids.slice(0, 2).join(', ')}, {$i18n.t( <ChevronDown className="size-2" />
'and {{COUNT}} more', {/if}
{ COUNT: feedback.data.sibling_model_ids.length - 2 } </span>
)} {:else}
{:else} <span class="invisible">
{feedback.data.sibling_model_ids.join(', ')} <ChevronUp className="size-2" />
{/if} </span>
</div> {/if}
</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> </div>
</td> </th>
{#if feedback?.data?.rating} <th
<td class="px-3 py-1 text-right font-medium text-gray-900 dark:text-white w-max"> scope="col"
<div class=" flex justify-end"> class="px-2.5 py-2 text-right cursor-pointer select-none w-fit"
{#if feedback?.data?.rating.toString() === '1'} on:click={() => setSortKey('rating')}
<Badge type="info" content={$i18n.t('Won')} /> >
{:else if feedback?.data?.rating.toString() === '0'} <div class="flex gap-1.5 items-center justify-end">
<Badge type="muted" content={$i18n.t('Draw')} /> {$i18n.t('Result')}
{:else if feedback?.data?.rating.toString() === '-1'} {#if orderBy === 'rating'}
<Badge type="error" content={$i18n.t('Lost')} /> <span class="font-normal">
{/if} {#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> </div>
</td> </td>
{/if}
<td class=" px-3 py-1 text-right font-medium"> <td class=" py-1 pl-3 flex flex-col">
{dayjs(feedback.updated_at * 1000).fromNow()} <div class="flex flex-col items-start gap-0.5 h-full">
</td> <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()}> <Tooltip content={feedback.data.sibling_model_ids.join(', ')}>
<FeedbackMenu <div class=" text-[0.65rem] text-gray-600 dark:text-gray-400 line-clamp-1">
on:delete={(e) => { {#if feedback.data.sibling_model_ids.length > 2}
deleteFeedbackHandler(feedback.id); <!-- {$i18n.t('and {{COUNT}} more')} -->
}} {feedback.data.sibling_model_ids.slice(0, 2).join(', ')}, {$i18n.t(
> 'and {{COUNT}} more',
<button { COUNT: feedback.data.sibling_model_ids.length - 2 }
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" )}
{: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
</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"
</FeedbackMenu> >
</td> <EllipsisHorizontal />
</tr> </button>
{/each} </FeedbackMenu>
</tbody> </td>
</table> </tr>
{/if} {/each}
</div> </tbody>
</table>
{#if feedbacks.length > 0 && $config?.features?.enable_community_sharing} {/if}
<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> </div>
{/if}
{#if feedbacks.length > 10} {#if total > 0 && $config?.features?.enable_community_sharing}
<Pagination bind:page count={feedbacks.length} perPage={10} /> <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} {/if}

View file

@ -10,7 +10,7 @@
import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.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'); const i18n = getContext('i18n');
@ -339,16 +339,14 @@
<div <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" 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="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div class=" gap-1"> <div>
{$i18n.t('Leaderboard')} {$i18n.t('Leaderboard')}
</div> </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">
{rankedModels.length}
<span class="text-lg font-medium text-gray-500 dark:text-gray-300 mr-1.5" </div>
>{rankedModels.length}</span
>
</div> </div>
<div class=" flex space-x-2"> <div class=" flex space-x-2">
@ -517,7 +515,7 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="shrink-0"> <div class="shrink-0">
<img <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} alt={model.name}
class="size-5 rounded-full object-cover shrink-0" class="size-5 rounded-full object-cover shrink-0"
/> />

View file

@ -757,18 +757,18 @@
{/if} {/if}
{/if} {/if}
<div class="flex justify-between w-full mt-2"> <div class="flex flex-col gap-2 mt-2">
<div class="self-center text-xs font-medium"> <div class=" flex flex-col w-full justify-between">
<Tooltip content={''} placement="top-start"> <div class=" mb-1 text-xs font-medium">
{$i18n.t('Parameters')} {$i18n.t('Parameters')}
</Tooltip> </div>
</div> <div class="flex w-full items-center relative">
<div class=""> <Textarea
<Textarea bind:value={RAGConfig.DOCLING_PARAMS}
bind:value={RAGConfig.DOCLING_PARAMS} placeholder={$i18n.t('Enter additional parameters in JSON format')}
placeholder={$i18n.t('Enter additional parameters in JSON format')} minSize={100}
minSize={100} />
/> </div>
</div> </div>
</div> </div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}

View file

@ -5,6 +5,7 @@
import Cog6 from '$lib/components/icons/Cog6.svelte'; import Cog6 from '$lib/components/icons/Cog6.svelte';
import ArenaModelModal from './ArenaModelModal.svelte'; import ArenaModelModal from './ArenaModelModal.svelte';
import { WEBUI_API_BASE_URL } from '$lib/constants';
export let model; export let model;
let showModel = false; let showModel = false;
@ -27,7 +28,7 @@
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<div class="flex gap-2.5 items-center"> <div class="flex gap-2.5 items-center">
<img <img
src={model.meta.profile_image_url} src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}`}
alt={model.name} alt={model.name}
class="size-8 rounded-full object-cover shrink-0" class="size-8 rounded-full object-cover shrink-0"
/> />

View file

@ -10,6 +10,7 @@
updateLdapConfig, updateLdapConfig,
updateLdapServer updateLdapServer
} from '$lib/apis/auths'; } from '$lib/apis/auths';
import { getGroups } from '$lib/apis/groups';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
@ -32,6 +33,7 @@
let adminConfig = null; let adminConfig = null;
let webhookUrl = ''; let webhookUrl = '';
let groups = [];
// LDAP // LDAP
let ENABLE_LDAP = false; let ENABLE_LDAP = false;
@ -104,6 +106,9 @@
})(), })(),
(async () => { (async () => {
LDAP_SERVER = await getLdapServer(localStorage.token); LDAP_SERVER = await getLdapServer(localStorage.token);
})(),
(async () => {
groups = await getGroups(localStorage.token);
})() })()
]); ]);
@ -299,6 +304,22 @@
</div> </div>
</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=" 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> <div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>

View file

@ -888,23 +888,15 @@
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
<div class="text-xs pr-2"> <div class="text-xs pr-2">
<div class=""> <div class="">
{$i18n.t('Image Edit Engine')} {$i18n.t('Image Edit')}
</div> </div>
</div> </div>
<select <Switch bind:state={config.ENABLE_IMAGE_EDIT} />
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>
</div> </div>
{#if config.ENABLE_IMAGE_GENERATION} {#if config?.ENABLE_IMAGE_GENERATION && config?.ENABLE_IMAGE_EDIT}
<div class="mb-2.5"> <div class="mb-2.5">
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
<div class="text-xs pr-2"> <div class="text-xs pr-2">
@ -949,6 +941,26 @@
</div> </div>
{/if} {/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'} {#if config?.IMAGE_EDIT_ENGINE === 'openai'}
<div class="mb-2.5"> <div class="mb-2.5">
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">

View file

@ -37,7 +37,7 @@
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte'; import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
import EyeSlash from '$lib/components/icons/EyeSlash.svelte'; import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
import Eye from '$lib/components/icons/Eye.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; let shiftKey = false;
@ -334,7 +334,7 @@
: 'opacity-50 dark:opacity-50'} " : 'opacity-50 dark:opacity-50'} "
> >
<img <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" alt="modelfile profile"
class=" rounded-full w-full h-auto object-cover" class=" rounded-full w-full h-auto object-cover"
/> />

View file

@ -105,11 +105,14 @@
/> />
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between"> <div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
<div class="flex md:self-center text-lg font-medium px-0.5"> <div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
{$i18n.t('Groups')} <div>
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" /> {$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>
<div class="flex gap-1"> <div class="flex gap-1">

View file

@ -5,7 +5,7 @@
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import Modal from '$lib/components/common/Modal.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 Permissions from './Permissions.svelte';
import Users from './Users.svelte'; import Users from './Users.svelte';
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte'; import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
@ -34,6 +34,7 @@
export let name = ''; export let name = '';
export let description = ''; export let description = '';
export let data = {};
export let permissions = { export let permissions = {
workspace: { workspace: {
@ -49,10 +50,15 @@
tools_export: false tools_export: false
}, },
sharing: { sharing: {
models: false,
public_models: false, public_models: false,
knowledge: false,
public_knowledge: false, public_knowledge: false,
prompts: false,
public_prompts: false, public_prompts: false,
tools: false,
public_tools: false, public_tools: false,
notes: false,
public_notes: false public_notes: false
}, },
chat: { chat: {
@ -92,6 +98,7 @@
const group = { const group = {
name, name,
description, description,
data,
permissions permissions
}; };
@ -106,6 +113,7 @@
name = group.name; name = group.name;
description = group.description; description = group.description;
permissions = group?.permissions ?? {}; permissions = group?.permissions ?? {};
data = group?.data ?? {};
userCount = group?.member_count ?? 0; 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="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"> <div class="w-full h-full overflow-y-auto scrollbar-hidden">
{#if selectedTab == 'general'} {#if selectedTab == 'general'}
<Display <General
bind:name bind:name
bind:description bind:description
bind:data
{edit} {edit}
onDelete={() => { onDelete={() => {
showDeleteConfirmDialog = true; showDeleteConfirmDialog = true;

View file

@ -2,12 +2,14 @@
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import Textarea from '$lib/components/common/Textarea.svelte'; import Textarea from '$lib/components/common/Textarea.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let name = ''; export let name = '';
export let color = ''; export let color = '';
export let description = ''; export let description = '';
export let data = {};
export let edit = false; export let edit = false;
export let onDelete: Function = () => {}; export let onDelete: Function = () => {};
@ -63,6 +65,34 @@
</div> </div>
</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} {#if edit}
<div class="flex flex-col w-full mt-2"> <div class="flex flex-col w-full mt-2">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Actions')}</div> <div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Actions')}</div>

View file

@ -10,7 +10,7 @@
import Pencil from '$lib/components/icons/Pencil.svelte'; import Pencil from '$lib/components/icons/Pencil.svelte';
import User from '$lib/components/icons/User.svelte'; import User from '$lib/components/icons/User.svelte';
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte'; import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
import GroupModal from './EditGroupModal.svelte'; import EditGroupModal from './EditGroupModal.svelte';
export let group = { export let group = {
name: 'Admins', name: 'Admins',
@ -54,7 +54,7 @@
}); });
</script> </script>
<GroupModal <EditGroupModal
bind:show={showEdit} bind:show={showEdit}
edit edit
{group} {group}

View file

@ -20,10 +20,15 @@
tools_export: false tools_export: false
}, },
sharing: { sharing: {
models: false,
public_models: false, public_models: false,
knowledge: false,
public_knowledge: false, public_knowledge: false,
prompts: false,
public_prompts: false, public_prompts: false,
tools: false,
public_tools: false, public_tools: false,
notes: false,
public_notes: false public_notes: false
}, },
chat: { chat: {
@ -217,11 +222,11 @@
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex w-full justify-between my-1"> <div class="flex w-full justify-between my-1">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Models Public Sharing')} {$i18n.t('Models Sharing')}
</div> </div>
<Switch bind:state={permissions.sharing.public_models} /> <Switch bind:state={permissions.sharing.models} />
</div> </div>
{#if defaultPermissions?.sharing?.public_models && !permissions.sharing.public_models} {#if defaultPermissions?.sharing?.models && !permissions.sharing.models}
<div> <div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
{$i18n.t('This is a default user permission and will remain enabled.')} {$i18n.t('This is a default user permission and will remain enabled.')}
@ -230,14 +235,32 @@
{/if} {/if}
</div> </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 flex-col w-full">
<div class="flex w-full justify-between my-1"> <div class="flex w-full justify-between my-1">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Knowledge Public Sharing')} {$i18n.t('Knowledge Sharing')}
</div> </div>
<Switch bind:state={permissions.sharing.public_knowledge} /> <Switch bind:state={permissions.sharing.knowledge} />
</div> </div>
{#if defaultPermissions?.sharing?.public_knowledge && !permissions.sharing.public_knowledge} {#if defaultPermissions?.sharing?.knowledge && !permissions.sharing.knowledge}
<div> <div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
{$i18n.t('This is a default user permission and will remain enabled.')} {$i18n.t('This is a default user permission and will remain enabled.')}
@ -246,14 +269,32 @@
{/if} {/if}
</div> </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 flex-col w-full">
<div class="flex w-full justify-between my-1"> <div class="flex w-full justify-between my-1">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Prompts Public Sharing')} {$i18n.t('Prompts Sharing')}
</div> </div>
<Switch bind:state={permissions.sharing.public_prompts} /> <Switch bind:state={permissions.sharing.prompts} />
</div> </div>
{#if defaultPermissions?.sharing?.public_prompts && !permissions.sharing.public_prompts} {#if defaultPermissions?.sharing?.prompts && !permissions.sharing.prompts}
<div> <div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
{$i18n.t('This is a default user permission and will remain enabled.')} {$i18n.t('This is a default user permission and will remain enabled.')}
@ -262,14 +303,32 @@
{/if} {/if}
</div> </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 flex-col w-full">
<div class="flex w-full justify-between my-1"> <div class="flex w-full justify-between my-1">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Tools Public Sharing')} {$i18n.t('Tools Sharing')}
</div> </div>
<Switch bind:state={permissions.sharing.public_tools} /> <Switch bind:state={permissions.sharing.tools} />
</div> </div>
{#if defaultPermissions?.sharing?.public_tools && !permissions.sharing.public_tools} {#if defaultPermissions?.sharing?.tools && !permissions.sharing.tools}
<div> <div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
{$i18n.t('This is a default user permission and will remain enabled.')} {$i18n.t('This is a default user permission and will remain enabled.')}
@ -278,14 +337,32 @@
{/if} {/if}
</div> </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 flex-col w-full">
<div class="flex w-full justify-between my-1"> <div class="flex w-full justify-between my-1">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Notes Public Sharing')} {$i18n.t('Notes Sharing')}
</div> </div>
<Switch bind:state={permissions.sharing.public_notes} /> <Switch bind:state={permissions.sharing.notes} />
</div> </div>
{#if defaultPermissions?.sharing?.public_notes && !permissions.sharing.public_notes} {#if defaultPermissions?.sharing?.notes && !permissions.sharing.notes}
<div> <div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
{$i18n.t('This is a default user permission and will remain enabled.')} {$i18n.t('This is a default user permission and will remain enabled.')}
@ -293,6 +370,24 @@
</div> </div>
{/if} {/if}
</div> </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> </div>
<hr class=" border-gray-100 dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />

View file

@ -1,5 +1,5 @@
<script> <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 { WEBUI_NAME, config, user, showSidebar } from '$lib/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
@ -154,27 +154,28 @@
<div <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" 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"> <div class="flex-shrink-0">
{$i18n.t('Users')} {$i18n.t('Users')}
</div> </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} <div>
{#if total > $config?.license_metadata?.seats} {#if ($config?.license_metadata?.seats ?? null) !== null}
<span class="text-lg font-medium text-red-500" {#if total > $config?.license_metadata?.seats}
>{total} of {$config?.license_metadata?.seats} <span class="text-lg font-medium text-red-500"
<span class="text-sm font-normal">{$i18n.t('available users')}</span></span >{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} {:else}
<span class="text-lg font-medium text-gray-500 dark:text-gray-300" <span class="text-lg font-medium text-gray-500 dark:text-gray-300">{total}</span>
>{total} of {$config?.license_metadata?.seats}
<span class="text-sm font-normal">{$i18n.t('available users')}</span></span
>
{/if} {/if}
{:else} </div>
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{total}</span>
{/if}
</div> </div>
<div class="flex gap-1"> <div class="flex gap-1">
@ -361,11 +362,7 @@
<div class="flex items-center"> <div class="flex items-center">
<img <img
class="rounded-full w-6 h-6 object-cover mr-2.5 flex-shrink-0" class="rounded-full w-6 h-6 object-cover mr-2.5 flex-shrink-0"
src={user?.profile_image_url?.startsWith(WEBUI_BASE_URL) || src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
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`}
alt="user" alt="user"
/> />

View file

@ -171,8 +171,7 @@
</div> </div>
{:else if item.type === 'model'} {:else if item.type === 'model'}
<img <img
src={item?.data?.info?.meta?.profile_image_url ?? src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${item.id}&lang=${$i18n.language}`}
`${WEBUI_BASE_URL}/static/favicon.png`}
alt={item?.data?.name ?? item.id} alt={item?.data?.name ?? item.id}
class="rounded-full size-5 items-center mr-2" class="rounded-full size-5 items-center mr-2"
/> />

View file

@ -157,7 +157,7 @@
{#if message?.reply_to_message?.user} {#if message?.reply_to_message?.user}
<div class="relative text-xs mb-1"> <div class="relative text-xs mb-1">
<div <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> ></div>
<button <button
@ -185,8 +185,7 @@
/> />
{:else} {:else}
<img <img
src={message.reply_to_message.user?.profile_image_url ?? src={`${WEBUI_API_BASE_URL}/users/${message.reply_to_message.user?.id}/profile/image`}
`${WEBUI_BASE_URL}/static/favicon.png`}
alt={message.reply_to_message.user?.name ?? $i18n.t('Unknown User')} alt={message.reply_to_message.user?.name ?? $i18n.t('Unknown User')}
class="size-4 ml-0.5 rounded-full object-cover" class="size-4 ml-0.5 rounded-full object-cover"
/> />
@ -220,7 +219,7 @@
{:else} {:else}
<ProfilePreview user={message.user}> <ProfilePreview user={message.user}>
<ProfileImage <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'} className={'size-8 ml-0.5'}
/> />
</ProfilePreview> </ProfilePreview>

View file

@ -2,7 +2,7 @@
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n'); 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; export let user = null;
</script> </script>
@ -11,8 +11,7 @@
<div class=" flex gap-3.5 w-full py-3 px-3 items-center"> <div class=" flex gap-3.5 w-full py-3 px-3 items-center">
<div class=" items-center flex shrink-0"> <div class=" items-center flex shrink-0">
<img <img
crossorigin="anonymous" src={`${WEBUI_API_BASE_URL}/users/${user?.id}/profile/image`}
src={user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
class=" size-12 object-cover rounded-xl" class=" size-12 object-cover rounded-xl"
alt="profile" alt="profile"
/> />

View file

@ -11,6 +11,7 @@
import PencilSquare from '../icons/PencilSquare.svelte'; import PencilSquare from '../icons/PencilSquare.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import Sidebar from '../icons/Sidebar.svelte'; import Sidebar from '../icons/Sidebar.svelte';
import { WEBUI_API_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -80,7 +81,7 @@
> >
<div class=" self-center"> <div class=" self-center">
<img <img
src={$user?.profile_image_url} src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
class="size-6 object-cover rounded-full" class="size-6 object-cover rounded-full"
alt="User profile" alt="User profile"
draggable="false" draggable="false"

View file

@ -1,5 +1,5 @@
<script lang="ts"> <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 { marked } from 'marked';
import { config, user, models as _models, temporaryChatEnabled } from '$lib/stores'; import { config, user, models as _models, temporaryChatEnabled } from '$lib/stores';
@ -53,11 +53,7 @@
placement="right" placement="right"
> >
<img <img
crossorigin="anonymous" src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
src={model?.info?.meta?.profile_image_url ??
($i18n.language === 'dg-DG'
? `${WEBUI_BASE_URL}/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
class=" size-[2.7rem] rounded-full border-[1px] border-gray-100 dark:border-none" class=" size-[2.7rem] rounded-full border-[1px] border-gray-100 dark:border-none"
alt="logo" alt="logo"
draggable="false" draggable="false"

View file

@ -1069,14 +1069,9 @@
<div class="flex items-center justify-between w-full"> <div class="flex items-center justify-between w-full">
<div class="pl-[1px] flex items-center gap-2 text-sm dark:text-gray-500"> <div class="pl-[1px] flex items-center gap-2 text-sm dark:text-gray-500">
<img <img
crossorigin="anonymous"
alt="model profile" alt="model profile"
class="size-3.5 max-w-[28px] object-cover rounded-full" class="size-3.5 max-w-[28px] object-cover rounded-full"
src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${$models.find((model) => model.id === atSelectedModel.id).id}&lang=${$i18n.language}`}
?.profile_image_url ??
($i18n.language === 'dg-DG'
? `${WEBUI_BASE_URL}/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
/> />
<div class="translate-y-[0.5px]"> <div class="translate-y-[0.5px]">
<span class="">{atSelectedModel.name}</span> <span class="">{atSelectedModel.name}</span>

View file

@ -13,6 +13,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte'; import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker'; import { KokoroWorker } from '$lib/workers/KokoroWorker';
import { WEBUI_API_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -759,14 +760,8 @@
? ' size-16' ? ' size-16'
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? 'size-14' ? 'size-14'
: 'size-12'} transition-all rounded-full {(model?.info?.meta : 'size-12'} transition-all rounded-full bg-cover bg-center bg-no-repeat"
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png' style={`background-image: url('${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model?.id}&lang=${$i18n.language}&voice=true');`}
? ' 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}');`
: ''}
/> />
{/if} {/if}
<!-- navbar --> <!-- navbar -->
@ -841,14 +836,8 @@
? 'size-48' ? 'size-48'
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? 'size-44' ? 'size-44'
: 'size-40'} transition-all rounded-full {(model?.info?.meta : 'size-40'} transition-all rounded-full bg-cover bg-center bg-no-repeat"
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png' style={`background-image: url('${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model?.id}&lang=${$i18n.language}&voice=true');`}
? ' 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}');`
: ''}
/> />
{/if} {/if}
</button> </button>

View file

@ -5,7 +5,7 @@
import { tick, getContext } from 'svelte'; import { tick, getContext } from 'svelte';
import { models } from '$lib/stores'; 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'; import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -83,7 +83,7 @@
> >
<div class="flex text-black dark:text-gray-100 line-clamp-1"> <div class="flex text-black dark:text-gray-100 line-clamp-1">
<img <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} alt={model?.name ?? model.id}
class="rounded-full size-5 items-center mr-2" class="rounded-full size-5 items-center mr-2"
/> />

View file

@ -272,14 +272,6 @@
}} }}
> >
<div class="flex items-center gap-1.5"> <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]"> <div class="-translate-y-[1px]">
{model ? `${model.name}` : history.messages[_messageId]?.model} {model ? `${model.name}` : history.messages[_messageId]?.model}
</div> </div>

View file

@ -6,7 +6,6 @@
</script> </script>
<img <img
crossorigin="anonymous"
aria-hidden="true" aria-hidden="true"
src={src === '' src={src === ''
? `${WEBUI_BASE_URL}/static/favicon.png` ? `${WEBUI_BASE_URL}/static/favicon.png`

View file

@ -36,7 +36,7 @@
removeDetails, removeDetails,
removeAllDetails removeAllDetails
} from '$lib/utils'; } 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 Name from './Name.svelte';
import ProfileImage from './ProfileImage.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 `}> <div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex mt-1 `}>
<ProfileImage <ProfileImage
src={model?.info?.meta?.profile_image_url ?? src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
($i18n.language === 'dg-DG'
? `${WEBUI_BASE_URL}/doge.png`
: `${WEBUI_BASE_URL}/favicon.png`)}
className={'size-8 assistant-message-profile-image'} className={'size-8 assistant-message-profile-image'}
/> />
</div> </div>

View file

@ -124,10 +124,7 @@
{#if !($settings?.chatBubble ?? true)} {#if !($settings?.chatBubble ?? true)}
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 mt-1`}> <div class={`shrink-0 ltr:mr-3 rtl:ml-3 mt-1`}>
<ProfileImage <ProfileImage
src={message.user src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
? ($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`)}
className={'size-8 user-message-profile-image'} className={'size-8 user-message-profile-image'}
/> />
</div> </div>

View file

@ -5,7 +5,7 @@
import dayjs from '$lib/dayjs'; import dayjs from '$lib/dayjs';
import { mobile, settings, user } from '$lib/stores'; 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 Tooltip from '$lib/components/common/Tooltip.svelte';
import { copyToClipboard, sanitizeResponseContent } from '$lib/utils'; import { copyToClipboard, sanitizeResponseContent } from '$lib/utils';
@ -77,8 +77,7 @@
<div class="flex items-center min-w-fit"> <div class="flex items-center min-w-fit">
<Tooltip content={$user?.role === 'admin' ? (item?.value ?? '') : ''} placement="top-start"> <Tooltip content={$user?.role === 'admin' ? (item?.value ?? '') : ''} placement="top-start">
<img <img
src={item.model?.info?.meta?.profile_image_url ?? src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${item.model.id}&lang=${$i18n.language}`}
`${WEBUI_BASE_URL}/static/favicon.png`}
alt="Model" alt="Model"
class="rounded-full size-5 flex items-center" class="rounded-full size-5 flex items-center"
/> />

View file

@ -38,6 +38,7 @@
import ChatPlus from '../icons/ChatPlus.svelte'; import ChatPlus from '../icons/ChatPlus.svelte';
import ChatCheck from '../icons/ChatCheck.svelte'; import ChatCheck from '../icons/ChatCheck.svelte';
import Knobs from '../icons/Knobs.svelte'; import Knobs from '../icons/Knobs.svelte';
import { WEBUI_API_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -242,7 +243,7 @@
<div class=" self-center"> <div class=" self-center">
<span class="sr-only">{$i18n.t('User menu')}</span> <span class="sr-only">{$i18n.t('User menu')}</span>
<img <img
src={$user?.profile_image_url} src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
class="size-6 object-cover rounded-full" class="size-6 object-cover rounded-full"
alt="" alt=""
draggable="false" draggable="false"

View file

@ -1,11 +1,14 @@
<script lang="ts"> <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 { Handle, Position, type NodeProps } from '@xyflow/svelte';
import { getContext } from 'svelte';
import ProfileImage from '../Messages/ProfileImage.svelte'; import ProfileImage from '../Messages/ProfileImage.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Heart from '$lib/components/icons/Heart.svelte'; import Heart from '$lib/components/icons/Heart.svelte';
const i18n = getContext('i18n');
type $$Props = NodeProps; type $$Props = NodeProps;
export let data: $$Props['data']; export let data: $$Props['data'];
</script> </script>
@ -21,7 +24,7 @@
{#if data.message.role === 'user'} {#if data.message.role === 'user'}
<div class="flex w-full"> <div class="flex w-full">
<ProfileImage <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]'} className={'size-5 -translate-y-[1px]'}
/> />
<div class="ml-2"> <div class="ml-2">
@ -41,7 +44,7 @@
{:else} {:else}
<div class="flex w-full"> <div class="flex w-full">
<ProfileImage <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]'} className={'size-5 -translate-y-[1px]'}
/> />

View file

@ -20,7 +20,7 @@
currentChatPage currentChatPage
} from '$lib/stores'; } from '$lib/stores';
import { sanitizeResponseContent, extractCurlyBraceWords } from '$lib/utils'; 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 Suggestions from './Suggestions.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
@ -121,11 +121,7 @@
}} }}
> >
<img <img
crossorigin="anonymous" src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model?.id}&lang=${$i18n.language}`}
src={model?.info?.meta?.profile_image_url ??
($i18n.language === 'dg-DG'
? `${WEBUI_BASE_URL}/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none" class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
aria-hidden="true" aria-hidden="true"
draggable="false" draggable="false"

View file

@ -4,8 +4,10 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat'; import localizedFormat from 'dayjs/plugin/localizedFormat';
import calendar from 'dayjs/plugin/calendar'
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
dayjs.extend(calendar);
import { deleteChatById } from '$lib/apis/chats'; import { deleteChatById } from '$lib/apis/chats';
@ -242,7 +244,14 @@
<div class="basis-2/5 flex items-center justify-end"> <div class="basis-2/5 flex items-center justify-end">
<div class="hidden sm:flex text-gray-500 dark:text-gray-400 text-xs"> <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>
<div class="flex justify-end pl-2.5 text-gray-600 dark:text-gray-300"> <div class="flex justify-end pl-2.5 text-gray-600 dark:text-gray-300">

View file

@ -9,6 +9,7 @@
import Spinner from '../common/Spinner.svelte'; import Spinner from '../common/Spinner.svelte';
import dayjs from '$lib/dayjs'; import dayjs from '$lib/dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import calendar from 'dayjs/plugin/calendar'; import calendar from 'dayjs/plugin/calendar';
import Loader from '../common/Loader.svelte'; import Loader from '../common/Loader.svelte';
import { createMessagesList } from '$lib/utils'; import { createMessagesList } from '$lib/utils';
@ -18,6 +19,7 @@
import PencilSquare from '../icons/PencilSquare.svelte'; import PencilSquare from '../icons/PencilSquare.svelte';
import PageEdit from '../icons/PageEdit.svelte'; import PageEdit from '../icons/PageEdit.svelte';
dayjs.extend(calendar); dayjs.extend(calendar);
dayjs.extend(localizedFormat);
export let show = false; export let show = false;
export let onClose = () => {}; export let onClose = () => {};
@ -387,7 +389,14 @@
</div> </div>
<div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs"> <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> </div>
</a> </a>
{/each} {/each}

View file

@ -41,7 +41,7 @@
importChat importChat
} from '$lib/apis/chats'; } from '$lib/apis/chats';
import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders'; 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 ArchivedChatsModal from './ArchivedChatsModal.svelte';
import UserMenu from './Sidebar/UserMenu.svelte'; import UserMenu from './Sidebar/UserMenu.svelte';
@ -537,7 +537,7 @@
{#if !$mobile && !$showSidebar} {#if !$mobile && !$showSidebar}
<div <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" id="sidebar"
> >
<button <button
@ -559,7 +559,6 @@
> >
<div class=" self-center flex items-center justify-center size-9"> <div class=" self-center flex items-center justify-center size-9">
<img <img
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/favicon.png" src="{WEBUI_BASE_URL}/static/favicon.png"
class="sidebar-new-chat-icon size-6 rounded-full group-hover:hidden" class="sidebar-new-chat-icon size-6 rounded-full group-hover:hidden"
alt="" alt=""
@ -571,7 +570,7 @@
</Tooltip> </Tooltip>
</div> </div>
<div> <div class="-mt-[0.5px]">
<div class=""> <div class="">
<Tooltip content={$i18n.t('New Chat')} placement="right"> <Tooltip content={$i18n.t('New Chat')} placement="right">
<a <a
@ -594,7 +593,7 @@
</Tooltip> </Tooltip>
</div> </div>
<div class=""> <div>
<Tooltip content={$i18n.t('Search')} placement="right"> <Tooltip content={$i18n.t('Search')} placement="right">
<button <button
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group" 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"> <div class=" self-center flex items-center justify-center size-9">
<img <img
src={$user?.profile_image_url} src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
class=" size-6 object-cover rounded-full" class=" size-6 object-cover rounded-full"
alt={$i18n.t('Open User Profile Menu')} alt={$i18n.t('Open User Profile Menu')}
aria-label={$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="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 <a
id="sidebar-new-chat-button" 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" 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> </a>
</div> </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 <button
id="sidebar-search-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" 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> </div>
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))} {#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 <a
id="sidebar-notes-button" 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" 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}
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools} {#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 <a
id="sidebar-workspace-button" 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" 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"> <div class=" self-center mr-3">
<img <img
src={$user?.profile_image_url} src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
class=" size-6 object-cover rounded-full" class=" size-6 object-cover rounded-full"
alt={$i18n.t('Open User Profile Menu')} alt={$i18n.t('Open User Profile Menu')}
aria-label={$i18n.t('Open User Profile Menu')} aria-label={$i18n.t('Open User Profile Menu')}

View file

@ -3,7 +3,7 @@
const i18n = getContext('i18n'); 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 Tooltip from '$lib/components/common/Tooltip.svelte';
import PinSlash from '$lib/components/icons/PinSlash.svelte'; import PinSlash from '$lib/components/icons/PinSlash.svelte';
@ -36,8 +36,7 @@
> >
<div class="self-center shrink-0"> <div class="self-center shrink-0">
<img <img
crossorigin="anonymous" src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
src={model?.info?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
class=" size-5 rounded-full -translate-x-[0.5px]" class=" size-5 rounded-full -translate-x-[0.5px]"
alt="logo" alt="logo"
/> />

View file

@ -116,7 +116,8 @@
<AccessControl <AccessControl
bind:accessControl bind:accessControl
accessRoles={['read', 'write']} 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>
</div> </div>

View file

@ -546,14 +546,42 @@
e.preventDefault(); e.preventDefault();
dragged = false; 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?.types?.includes('Files')) {
if (e.dataTransfer?.files) { if (e.dataTransfer?.files) {
const inputFiles = e.dataTransfer?.files; const inputItems = e.dataTransfer?.items;
if (inputFiles && inputFiles.length > 0) { if (inputItems && inputItems.length > 0) {
for (const file of inputFiles) { handleUploadingFileFolder(inputItems);
await uploadFileHandler(file);
}
} else { } else {
toast.error($i18n.t(`File not found.`)); toast.error($i18n.t(`File not found.`));
} }
@ -682,7 +710,8 @@
<AccessControlModal <AccessControlModal
bind:show={showAccessControlModal} bind:show={showAccessControlModal}
bind:accessControl={knowledge.access_control} 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={() => { onChange={() => {
changeDebounceHandler(); changeDebounceHandler();
}} }}

View file

@ -12,7 +12,7 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
import { WEBUI_NAME, config, mobile, models as _models, settings, user } from '$lib/stores'; 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 { import {
createNewModel, createNewModel,
deleteModelById, deleteModelById,
@ -438,13 +438,12 @@
<div class="self-center pl-0.5"> <div class="self-center pl-0.5">
<div class="flex bg-white rounded-2xl"> <div class="flex bg-white rounded-2xl">
<div <div
class="{model.is_active ? '' : 'opacity-50 dark:opacity-50'} {model.meta class="{model.is_active
.profile_image_url !== `${WEBUI_BASE_URL}/static/favicon.png` ? ''
? 'bg-transparent' : 'opacity-50 dark:opacity-50'} bg-transparent rounded-2xl"
: 'bg-white'} rounded-2xl"
> >
<img <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" alt="modelfile profile"
class=" rounded-2xl size-12 object-cover" class=" rounded-2xl size-12 object-cover"
/> />

View file

@ -748,7 +748,8 @@
<AccessControl <AccessControl
bind:accessControl bind:accessControl
accessRoles={['read', 'write']} 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>
</div> </div>

View file

@ -83,7 +83,8 @@
bind:show={showAccessControlModal} bind:show={showAccessControlModal}
bind:accessControl bind:accessControl
accessRoles={['read', 'write']} 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"> <div class="w-full max-h-full flex justify-center">

View file

@ -189,7 +189,8 @@ class Tools:
bind:show={showAccessControlModal} bind:show={showAccessControlModal}
bind:accessControl bind:accessControl
accessRoles={['read', 'write']} 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"> <div class=" flex flex-col justify-between w-full overflow-y-auto h-full">

View file

@ -15,17 +15,18 @@
export let accessRoles = ['read']; export let accessRoles = ['read'];
export let accessControl = {}; export let accessControl = {};
export let allowPublic = true; export let share = true;
export let sharePublic = true;
let selectedGroupId = ''; let selectedGroupId = '';
let groups = []; let groups = [];
$: if (!allowPublic && accessControl === null) { $: if (!sharePublic && accessControl === null) {
initPublicAccess(); initPublicAccess();
} }
const initPublicAccess = () => { const initPublicAccess = () => {
if (!allowPublic && accessControl === null) { if (!sharePublic && accessControl === null) {
accessControl = { accessControl = {
read: { read: {
group_ids: [], group_ids: [],
@ -41,7 +42,7 @@
}; };
onMount(async () => { onMount(async () => {
groups = await getGroups(localStorage.token); groups = await getGroups(localStorage.token, true);
if (accessControl === null) { if (accessControl === null) {
initPublicAccess(); initPublicAccess();
@ -125,7 +126,7 @@
}} }}
> >
<option class=" text-gray-700" value="private" selected>{$i18n.t('Private')}</option> <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> <option class=" text-gray-700" value="public" selected>{$i18n.t('Public')}</option>
{/if} {/if}
</select> </select>
@ -140,48 +141,50 @@
</div> </div>
</div> </div>
</div> </div>
{#if accessControl !== null}
{@const accessGroups = groups.filter((group) =>
(accessControl?.read?.group_ids ?? []).includes(group.id)
)}
<div>
<div class="">
<div class="flex justify-between mb-1.5">
<div class="text-sm font-semibold">
{$i18n.t('Groups')}
</div>
</div>
<div class="mb-1"> {#if share}
<div class="flex w-full"> {#if accessControl !== null}
<div class="flex flex-1 items-center"> {@const accessGroups = groups.filter((group) =>
<div class="w-full px-0.5"> (accessControl?.read?.group_ids ?? []).includes(group.id)
<select )}
class="outline-hidden bg-transparent text-sm rounded-lg block w-full pr-10 max-w-full <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'} {selectedGroupId ? '' : 'text-gray-500'}
dark:placeholder-gray-500" dark:placeholder-gray-500"
bind:value={selectedGroupId} bind:value={selectedGroupId}
on:change={() => { on:change={() => {
if (selectedGroupId !== '') { if (selectedGroupId !== '') {
accessControl.read.group_ids = [ accessControl.read.group_ids = [
...(accessControl?.read?.group_ids ?? []), ...(accessControl?.read?.group_ids ?? []),
selectedGroupId selectedGroupId
]; ];
selectedGroupId = ''; selectedGroupId = '';
onChange(accessControl); onChange(accessControl);
} }
}} }}
>
<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="" disabled selected
<option class=" text-gray-700" value={group.id}>{group.name}</option> >{$i18n.t('Select a group')}</option
{/each} >
</select> {#each groups.filter((group) => !(accessControl?.read?.group_ids ?? []).includes(group.id)) as group}
</div> <option class=" text-gray-700" value={group.id}>{group.name}</option>
<!-- <div> {/each}
</select>
</div>
<!-- <div>
<Tooltip content={$i18n.t('Add Group')}> <Tooltip content={$i18n.t('Add Group')}>
<button <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" 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> </button>
</Tooltip> </Tooltip>
</div> --> </div> -->
</div>
</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"> <div class="flex flex-col gap-2 mb-1 px-0.5">
{#if accessGroups.length > 0} {#if accessGroups.length > 0}
{#each accessGroups as group} {#each accessGroups as group}
<div class="flex items-center gap-3 justify-between text-xs w-full transition"> <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 class="flex items-center gap-1.5 w-full font-medium">
<div> <div>
<UserCircleSolid className="size-4" /> <UserCircleSolid className="size-4" />
</div>
<div>
{group.name}
</div>
</div> </div>
<div> <div class="w-full flex justify-end items-center gap-0.5">
{group.name} <button
</div> class=""
</div> type="button"
on:click={() => {
<div class="w-full flex justify-end items-center gap-0.5"> if (accessRoles.includes('write')) {
<button if ((accessControl?.write?.group_ids ?? []).includes(group.id)) {
class="" accessControl.write.group_ids = (
type="button" accessControl?.write?.group_ids ?? []
on:click={() => { ).filter((group_id) => group_id !== group.id);
if (accessRoles.includes('write')) { } else {
if ((accessControl?.write?.group_ids ?? []).includes(group.id)) { accessControl.write.group_ids = [
accessControl.write.group_ids = ( ...(accessControl?.write?.group_ids ?? []),
accessControl?.write?.group_ids ?? [] group.id
).filter((group_id) => group_id !== group.id); ];
} else { }
accessControl.write.group_ids = [ onChange(accessControl);
...(accessControl?.write?.group_ids ?? []),
group.id
];
} }
onChange(accessControl); }}
} >
}} {#if (accessControl?.write?.group_ids ?? []).includes(group.id)}
> <Badge type={'success'} content={$i18n.t('Write')} />
{#if (accessControl?.write?.group_ids ?? []).includes(group.id)} {:else}
<Badge type={'success'} content={$i18n.t('Write')} /> <Badge type={'info'} content={$i18n.t('Read')} />
{:else} {/if}
<Badge type={'info'} content={$i18n.t('Read')} /> </button>
{/if}
</button>
<button <button
class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition" class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
type="button" type="button"
on:click={() => { on:click={() => {
accessControl.read.group_ids = (accessControl?.read?.group_ids ?? []).filter( accessControl.read.group_ids = (
(id) => id !== group.id accessControl?.read?.group_ids ?? []
); ).filter((id) => id !== group.id);
accessControl.write.group_ids = ( accessControl.write.group_ids = (
accessControl?.write?.group_ids ?? [] accessControl?.write?.group_ids ?? []
).filter((id) => id !== group.id); ).filter((id) => id !== group.id);
onChange(accessControl); onChange(accessControl);
}} }}
> >
<XMark /> <XMark />
</button> </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>
</div> </div>
{/each} {/if}
{:else} </div>
<div class="flex items-center justify-center">
<div class="text-gray-500 text-xs text-center py-2 px-10">
{$i18n.t('No groups with access, add a group to grant access')}
</div>
</div>
{/if}
</div> </div>
</div> </div>
</div> {/if}
{/if} {/if}
</div> </div>

View file

@ -9,7 +9,9 @@
export let show = false; export let show = false;
export let accessControl = {}; export let accessControl = {};
export let accessRoles = ['read']; export let accessRoles = ['read'];
export let allowPublic = true;
export let share = true;
export let sharePublic = true;
export let onChange = () => {}; export let onChange = () => {};
</script> </script>
@ -31,7 +33,7 @@
</div> </div>
<div class="w-full px-5 pb-4 dark:text-white"> <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>
</div> </div>
</Modal> </Modal>

View file

@ -101,5 +101,6 @@ import 'dayjs/locale/yo';
import 'dayjs/locale/zh'; import 'dayjs/locale/zh';
import 'dayjs/locale/zh-tw'; import 'dayjs/locale/zh-tw';
import 'dayjs/locale/et'; import 'dayjs/locale/et';
import 'dayjs/locale/en-gb'
export default dayjs; export default dayjs;

View file

@ -76,7 +76,7 @@
"Advanced Parameters": "고급 매개변수", "Advanced Parameters": "고급 매개변수",
"Advanced parameters for MinerU parsing (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)": "", "Advanced parameters for MinerU parsing (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)": "",
"Advanced Params": "고급 매개변수", "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": "", "AI": "",
"All": "전체", "All": "전체",
"All chats have been unarchived.": "모든 채팅이 보관 해제되었습니다.", "All chats have been unarchived.": "모든 채팅이 보관 해제되었습니다.",
@ -153,7 +153,7 @@
"Ask": "질문", "Ask": "질문",
"Ask a question": "질문하기", "Ask a question": "질문하기",
"Assistant": "어시스턴트", "Assistant": "어시스턴트",
"Attach File From Knowledge": "", "Attach File From Knowledge": "지식 기반에서 파일 첨부",
"Attach Knowledge": "지식 기반 첨부", "Attach Knowledge": "지식 기반 첨부",
"Attach Notes": "노트 첨부", "Attach Notes": "노트 첨부",
"Attach Webpage": "웹페이지 첨부", "Attach Webpage": "웹페이지 첨부",
@ -233,7 +233,7 @@
"Chat Background Image": "채팅 배경 이미지", "Chat Background Image": "채팅 배경 이미지",
"Chat Bubble UI": "버블형 채팅 UI", "Chat Bubble UI": "버블형 채팅 UI",
"Chat Controls": "채팅 제어", "Chat Controls": "채팅 제어",
"Chat Conversation": "", "Chat Conversation": "채팅 대화",
"Chat direction": "채팅 방향", "Chat direction": "채팅 방향",
"Chat ID": "채팅 ID", "Chat ID": "채팅 ID",
"Chat moved successfully": "채팅 이동 성공", "Chat moved successfully": "채팅 이동 성공",
@ -279,7 +279,7 @@
"cloud": "", "cloud": "",
"CMU ARCTIC speaker embedding name": "", "CMU ARCTIC speaker embedding name": "",
"Code Block": "코드 블록", "Code Block": "코드 블록",
"Code Editor": "", "Code Editor": "코드 편집기",
"Code execution": "코드 실행", "Code execution": "코드 실행",
"Code Execution": "코드 실행", "Code Execution": "코드 실행",
"Code Execution Engine": "코드 실행 엔진", "Code Execution Engine": "코드 실행 엔진",
@ -337,8 +337,8 @@
"Copied to clipboard": "클립보드에 복사되었습니다", "Copied to clipboard": "클립보드에 복사되었습니다",
"Copy": "복사", "Copy": "복사",
"Copy Formatted Text": "서식 있는 텍스트 복사", "Copy Formatted Text": "서식 있는 텍스트 복사",
"Copy Last Code Block": "", "Copy Last Code Block": "마지막 코드 블록 복사",
"Copy Last Response": "", "Copy Last Response": "마지막 응답 복사",
"Copy link": "링크 복사", "Copy link": "링크 복사",
"Copy Link": "링크 복사", "Copy Link": "링크 복사",
"Copy to clipboard": "클립보드에 복사", "Copy to clipboard": "클립보드에 복사",
@ -347,13 +347,13 @@
"Create": "생성", "Create": "생성",
"Create a knowledge base": "지식 기반 생성", "Create a knowledge base": "지식 기반 생성",
"Create a model": "모델 생성", "Create a model": "모델 생성",
"Create a new note": "", "Create a new note": "새 노트 생성",
"Create Account": "계정 생성", "Create Account": "계정 생성",
"Create Admin Account": "관리자 계정 생성", "Create Admin Account": "관리자 계정 생성",
"Create Channel": "채널 생성", "Create Channel": "채널 생성",
"Create Folder": "폴더 생성", "Create Folder": "폴더 생성",
"Create Group": "그룹 생성", "Create Group": "그룹 생성",
"Create Image": "", "Create Image": "이미지 생성",
"Create Knowledge": "지식 생성", "Create Knowledge": "지식 생성",
"Create Model": "모델 생성", "Create Model": "모델 생성",
"Create new key": "새로운 키 생성", "Create new key": "새로운 키 생성",
@ -502,7 +502,7 @@
"Edit Connection": "연결 편집", "Edit Connection": "연결 편집",
"Edit Default Permissions": "기본 권한 편집", "Edit Default Permissions": "기본 권한 편집",
"Edit Folder": "폴더 편집", "Edit Folder": "폴더 편집",
"Edit Image": "", "Edit Image": "이미지 편집",
"Edit Last Message": "", "Edit Last Message": "",
"Edit Memory": "메모리 편집", "Edit Memory": "메모리 편집",
"Edit User": "사용자 편집", "Edit User": "사용자 편집",
@ -592,8 +592,8 @@
"Enter Kagi Search API Key": "Kagi Search API 키 입력", "Enter Kagi Search API Key": "Kagi Search API 키 입력",
"Enter Key Behavior": "키 동작 입력", "Enter Key Behavior": "키 동작 입력",
"Enter language codes": "언어 코드 입력", "Enter language codes": "언어 코드 입력",
"Enter MinerU API Key": "", "Enter MinerU API Key": "MinerU API 키 입력",
"Enter Mistral API Base URL": "", "Enter Mistral API Base URL": "Mistral API Base URL 입력",
"Enter Mistral API Key": "Mistral API 키 입력", "Enter Mistral API Key": "Mistral API 키 입력",
"Enter Model ID": "모델 ID 입력", "Enter Model ID": "모델 ID 입력",
"Enter model tag (e.g. {{modelTag}})": "모델 태그 입력(예: {{modelTag}})", "Enter model tag (e.g. {{modelTag}})": "모델 태그 입력(예: {{modelTag}})",
@ -718,8 +718,8 @@
"Failed to load chat preview": "채팅 미리보기 로드 실패", "Failed to load chat preview": "채팅 미리보기 로드 실패",
"Failed to load file content.": "파일 내용 로드 실패.", "Failed to load file content.": "파일 내용 로드 실패.",
"Failed to move chat": "채팅 이동 실패", "Failed to move chat": "채팅 이동 실패",
"Failed to read clipboard contents": "클립보드 내용 가져오기를 실패하였습니다.", "Failed to read clipboard contents": "클립보드 내용 가져오기를 실패하였습니다",
"Failed to render diagram": "", "Failed to render diagram": "다이어그램을 표시할 수 없습니다",
"Failed to render visualization": "", "Failed to render visualization": "",
"Failed to save connections": "연결 저장 실패", "Failed to save connections": "연결 저장 실패",
"Failed to save conversation": "대화 저장 실패", "Failed to save conversation": "대화 저장 실패",
@ -753,7 +753,7 @@
"Firecrawl API Base URL": "Firecrawl API 기본 URL", "Firecrawl API Base URL": "Firecrawl API 기본 URL",
"Firecrawl API Key": "Firecrawl API 키", "Firecrawl API Key": "Firecrawl API 키",
"Floating Quick Actions": "", "Floating Quick Actions": "",
"Focus Chat Input": "", "Focus Chat Input": "채팅 입력창에 포커스",
"Folder": "폴더", "Folder": "폴더",
"Folder Background Image": "폴더 배경 이미지", "Folder Background Image": "폴더 배경 이미지",
"Folder deleted successfully": "성공적으로 폴더가 삭제되었습니다", "Folder deleted successfully": "성공적으로 폴더가 삭제되었습니다",
@ -774,8 +774,8 @@
"Format Lines": "줄 서식", "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 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:": "변수를 다음과 같이 괄호를 사용하여 생성하세요", "Format your variables using brackets like this:": "변수를 다음과 같이 괄호를 사용하여 생성하세요",
"Formatting may be inconsistent from source.": "", "Formatting may be inconsistent from source.": "출처에서의 서식이 일관되지 않을 수 있습니다.",
"Forwards system user OAuth access token to authenticate": "", "Forwards system user OAuth access token to authenticate": "인증을 위해 시스템 사용자 OAuth 액세스 토큰을 전달합니다.",
"Forwards system user session credentials to authenticate": "인증을 위해 시스템 사용자 세션 자격 증명 전달", "Forwards system user session credentials to authenticate": "인증을 위해 시스템 사용자 세션 자격 증명 전달",
"Full Context Mode": "전체 컨텍스트 모드", "Full Context Mode": "전체 컨텍스트 모드",
"Function": "함수", "Function": "함수",
@ -803,7 +803,7 @@
"Generate an image": "이미지 생성", "Generate an image": "이미지 생성",
"Generate Image": "이미지 생성", "Generate Image": "이미지 생성",
"Generate Message Pair": "", "Generate Message Pair": "",
"Generated Image": "", "Generated Image": "생성된 이미지",
"Generating search query": "검색 쿼리 생성", "Generating search query": "검색 쿼리 생성",
"Generating...": "생성 중...", "Generating...": "생성 중...",
"Get information on {{name}} in the UI": "UI에서 {{name}} 정보 확인", "Get information on {{name}} in the UI": "UI에서 {{name}} 정보 확인",
@ -865,7 +865,7 @@
"Image Max Compression Size width": "이미지 최대 압축 크기 너비", "Image Max Compression Size width": "이미지 최대 압축 크기 너비",
"Image Prompt Generation": "이미지 프롬프트 생성", "Image Prompt Generation": "이미지 프롬프트 생성",
"Image Prompt Generation Prompt": "이미지 프롬프트를 생성하기 위한 프롬프트", "Image Prompt Generation Prompt": "이미지 프롬프트를 생성하기 위한 프롬프트",
"Image Size": "", "Image Size": "이미지 크기",
"Images": "이미지", "Images": "이미지",
"Import": "가져오기", "Import": "가져오기",
"Import Chats": "채팅 가져오기", "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}}/api/tags\" endpoint": "\"{{url}}/api/tags\" 엔드포인트의 모든 모델을 포함하려면 비워 두세요",
"Leave empty to include all models from \"{{url}}/models\" endpoint": "\"{{url}}/models\" 엔드포인트의 모든 모델을 포함하려면 비워 두세요", "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 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 empty to use the default prompt, or enter a custom prompt": "기본 프롬프트를 사용하기 위해 빈칸으로 남겨두거나, 커스텀 프롬프트를 입력하세요",
"Leave model field empty to use the default model.": "기본 모델을 사용하려면 모델 필드를 비워 두세요.", "Leave model field empty to use the default model.": "기본 모델을 사용하려면 모델 필드를 비워 두세요.",
"Legacy": "", "Legacy": "",
@ -1017,14 +1017,14 @@
"Memory updated successfully": "성공적으로 메모리가 업데이트되었습니다", "Memory updated successfully": "성공적으로 메모리가 업데이트되었습니다",
"Merge Responses": "응답들 결합하기", "Merge Responses": "응답들 결합하기",
"Merged Response": "결합된 응답", "Merged Response": "결합된 응답",
"Message": "", "Message": "메시지",
"Message rating should be enabled to use this feature": "이 기능을 사용하려면 메시지 평가가 활성화되어야합니다", "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이 있는 사용자는 공유된 채팅을 볼 수 있습니다.", "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": "",
"Microsoft OneDrive (personal)": "Microsoft OneDrive (개인용)", "Microsoft OneDrive (personal)": "Microsoft OneDrive (개인용)",
"Microsoft OneDrive (work/school)": "Microsoft OneDrive (회사/학교용)", "Microsoft OneDrive (work/school)": "Microsoft OneDrive (회사/학교용)",
"MinerU": "", "MinerU": "",
"MinerU API Key required for Cloud API mode.": "", "MinerU API Key required for Cloud API mode.": "클라우드 API 모드를 사용하려면 MinerU API 키가 필요합니다.",
"Mistral OCR": "", "Mistral OCR": "",
"Mistral OCR API Key required.": "Mistral OCR API Key가 필요합니다.", "Mistral OCR API Key required.": "Mistral OCR API Key가 필요합니다.",
"MistralAI": "", "MistralAI": "",
@ -1048,7 +1048,7 @@
"Model ID is required.": "모델 ID가 필요합니다", "Model ID is required.": "모델 ID가 필요합니다",
"Model IDs": "모델 IDs", "Model IDs": "모델 IDs",
"Model Name": "모델 이름", "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 Name is required.": "모델 이름이 필요합니다",
"Model not selected": "모델이 선택되지 않았습니다.", "Model not selected": "모델이 선택되지 않았습니다.",
"Model Params": "모델 매개변수", "Model Params": "모델 매개변수",
@ -1060,7 +1060,7 @@
"Models": "모델", "Models": "모델",
"Models Access": "모델 접근", "Models Access": "모델 접근",
"Models configuration saved successfully": "모델 구성이 성공적으로 저장되었습니다", "Models configuration saved successfully": "모델 구성이 성공적으로 저장되었습니다",
"Models imported successfully": "", "Models imported successfully": "모델을 성공적으로 가져왔습니다.",
"Models Public Sharing": "모델 공개 공유", "Models Public Sharing": "모델 공개 공유",
"Mojeek Search API Key": "Mojeek Search API 키", "Mojeek Search API Key": "Mojeek Search API 키",
"More": "더보기", "More": "더보기",
@ -1246,7 +1246,7 @@
"Please enter a valid URL.": "올바른 URL을 입력하세요.", "Please enter a valid URL.": "올바른 URL을 입력하세요.",
"Please fill in all fields.": "모두 빈칸없이 채워주세요", "Please fill in all fields.": "모두 빈칸없이 채워주세요",
"Please register the OAuth client": "OAuth clith를 등록해주세요", "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 first.": "먼저 모델을 선택하세요.",
"Please select a model.": "모델을 선택하세요.", "Please select a model.": "모델을 선택하세요.",
"Please select a reason": "이유를 선택해주세요", "Please select a reason": "이유를 선택해주세요",
@ -1257,7 +1257,7 @@
"Prefer not to say": "언급하고 싶지 않습니다.", "Prefer not to say": "언급하고 싶지 않습니다.",
"Prefix ID": "Prefix ID", "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에 접두사를 추가하여 다른 연결과의 충돌을 방지하는 데 사용됩니다. - 비활성화하려면 비워 둡니다.", "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": "미리보기", "Preview": "미리보기",
"Previous 30 days": "이전 30일", "Previous 30 days": "이전 30일",
"Previous 7 days": "이전 7일", "Previous 7 days": "이전 7일",
@ -1281,7 +1281,7 @@
"Pull Model": "모델 pull", "Pull Model": "모델 pull",
"pypdfium2": "", "pypdfium2": "",
"Query Generation Prompt": "쿼리 생성 프롬프트", "Query Generation Prompt": "쿼리 생성 프롬프트",
"Querying": "", "Querying": "쿼리 진행중",
"Quick Actions": "빠른 작업", "Quick Actions": "빠른 작업",
"RAG Template": "RAG 템플릿", "RAG Template": "RAG 템플릿",
"Rating": "평가", "Rating": "평가",
@ -1367,7 +1367,7 @@
"Scroll On Branch Change": "브랜치 변경 시 스크롤", "Scroll On Branch Change": "브랜치 변경 시 스크롤",
"Search": "검색", "Search": "검색",
"Search a model": "모델 검색", "Search a model": "모델 검색",
"Search all emojis": "", "Search all emojis": "모든 이모지 검색",
"Search Base": "검색 기반", "Search Base": "검색 기반",
"Search Chats": "채팅 검색", "Search Chats": "채팅 검색",
"Search Collection": "컬렉션 검색", "Search Collection": "컬렉션 검색",
@ -1470,7 +1470,7 @@
"Show Formatting Toolbar": "서식 툴바 표시", "Show Formatting Toolbar": "서식 툴바 표시",
"Show image preview": "이미지 미리보기", "Show image preview": "이미지 미리보기",
"Show Model": "모델 보기", "Show Model": "모델 보기",
"Show Shortcuts": "", "Show Shortcuts": "단축키 보기",
"Show your support!": "당신의 응원을 보내주세요!", "Show your support!": "당신의 응원을 보내주세요!",
"Showcased creativity": "창의성 발휘", "Showcased creativity": "창의성 발휘",
"Sign in": "로그인", "Sign in": "로그인",
@ -1499,14 +1499,14 @@
"Speech-to-Text": "음성-텍스트 변환", "Speech-to-Text": "음성-텍스트 변환",
"Speech-to-Text Engine": "음성-텍스트 변환 엔진", "Speech-to-Text Engine": "음성-텍스트 변환 엔진",
"standard": "", "standard": "",
"Start a new conversation": "", "Start a new conversation": "새 대화 시작",
"Start of the channel": "채널 시작", "Start of the channel": "채널 시작",
"Start Tag": "시작 태그", "Start Tag": "시작 태그",
"Status Updates": "상태 업데이트", "Status Updates": "상태 업데이트",
"STDOUT/STDERR": "STDOUT/STDERR", "STDOUT/STDERR": "STDOUT/STDERR",
"Steps": "", "Steps": "",
"Stop": "정지", "Stop": "정지",
"Stop Generating": "", "Stop Generating": "생성 중지",
"Stop Sequence": "중지 시퀀스", "Stop Sequence": "중지 시퀀스",
"Stream Chat Response": "스트림 채팅 응답", "Stream Chat Response": "스트림 채팅 응답",
"Stream Delta Chunk Size": "스트림 델타 청크 크기", "Stream Delta Chunk Size": "스트림 델타 청크 크기",
@ -1530,7 +1530,7 @@
"System": "시스템", "System": "시스템",
"System Instructions": "시스템 지침", "System Instructions": "시스템 지침",
"System Prompt": "시스템 프롬프트", "System Prompt": "시스템 프롬프트",
"Table Mode": "", "Table Mode": "테이블 모드",
"Tag": "", "Tag": "",
"Tags": "태그", "Tags": "태그",
"Tags Generation": "태그 생성", "Tags Generation": "태그 생성",
@ -1578,7 +1578,7 @@
"This chat won't appear in history and your messages will not be saved.": "이 채팅은 기록에 나타나지 않으며 메시지가 저장되지 않습니다.", "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 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 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 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 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분)", "This option controls how long the model will stay loaded into memory following the request (default: 5m)": "이 옵션은 요청 처리 후 모델이 메모리에 유지하는 시간을 제어합니다. (기본값: 5분)",

View file

@ -95,6 +95,7 @@
"Allow Continue Response": "允许继续生成回答", "Allow Continue Response": "允许继续生成回答",
"Allow Delete Messages": "允许删除对话消息", "Allow Delete Messages": "允许删除对话消息",
"Allow File Upload": "允许上传文件", "Allow File Upload": "允许上传文件",
"Allow Group Sharing": "允许群组内共享",
"Allow Multiple Models in Chat": "允许在对话中使用多个模型", "Allow Multiple Models in Chat": "允许在对话中使用多个模型",
"Allow non-local voices": "允许调用非本土音色", "Allow non-local voices": "允许调用非本土音色",
"Allow Rate Response": "允许对回答进行评价", "Allow Rate Response": "允许对回答进行评价",
@ -388,6 +389,7 @@
"Default description enabled": "默认描述已启用", "Default description enabled": "默认描述已启用",
"Default Features": "默认功能", "Default Features": "默认功能",
"Default Filters": "默认过滤器", "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 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": "默认模型",
"Default model updated": "默认模型已更新", "Default model updated": "默认模型已更新",
@ -731,6 +733,7 @@
"Features": "功能", "Features": "功能",
"Features Permissions": "功能权限", "Features Permissions": "功能权限",
"February": "二月", "February": "二月",
"Feedback deleted successfully": "反馈删除成功",
"Feedback Details": "反馈详情", "Feedback Details": "反馈详情",
"Feedback History": "历史反馈", "Feedback History": "历史反馈",
"Feedbacks": "反馈", "Feedbacks": "反馈",
@ -745,6 +748,7 @@
"File size should not exceed {{maxSize}} MB.": "文件大小不应超过 {{maxSize}} MB", "File size should not exceed {{maxSize}} MB.": "文件大小不应超过 {{maxSize}} MB",
"File Upload": "文件上传", "File Upload": "文件上传",
"File uploaded successfully": "文件上传成功", "File uploaded successfully": "文件上传成功",
"File uploaded!": "文件上传成功",
"Files": "文件", "Files": "文件",
"Filter": "过滤", "Filter": "过滤",
"Filter is now globally disabled": "过滤器已全局禁用", "Filter is now globally disabled": "过滤器已全局禁用",
@ -858,6 +862,7 @@
"Image Compression": "压缩图像", "Image Compression": "压缩图像",
"Image Compression Height": "压缩图像高度", "Image Compression Height": "压缩图像高度",
"Image Compression Width": "压缩图像宽度", "Image Compression Width": "压缩图像宽度",
"Image Edit": "图片编辑",
"Image Edit Engine": "图片编辑引擎", "Image Edit Engine": "图片编辑引擎",
"Image Generation": "图像生成", "Image Generation": "图像生成",
"Image Generation Engine": "图像生成引擎", "Image Generation Engine": "图像生成引擎",
@ -942,6 +947,7 @@
"Knowledge Name": "知识库名称", "Knowledge Name": "知识库名称",
"Knowledge Public Sharing": "公开分享知识库", "Knowledge Public Sharing": "公开分享知识库",
"Knowledge reset successfully.": "知识库重置成功", "Knowledge reset successfully.": "知识库重置成功",
"Knowledge Sharing": "分享知识库",
"Knowledge updated successfully": "知识库更新成功", "Knowledge updated successfully": "知识库更新成功",
"Kokoro.js (Browser)": "Kokoro.js运行于用户浏览器", "Kokoro.js (Browser)": "Kokoro.js运行于用户浏览器",
"Kokoro.js Dtype": "Kokoro.js Dtype", "Kokoro.js Dtype": "Kokoro.js Dtype",
@ -1062,7 +1068,8 @@
"Models Access": "访问模型列表", "Models Access": "访问模型列表",
"Models configuration saved successfully": "模型配置保存成功", "Models configuration saved successfully": "模型配置保存成功",
"Models imported successfully": "已成功导入模型配置", "Models imported successfully": "已成功导入模型配置",
"Models Public Sharing": "模型公开共享", "Models Public Sharing": "模型公开分享",
"Models Sharing": "分享模型",
"Mojeek Search API Key": "Mojeek Search 接口密钥", "Mojeek Search API Key": "Mojeek Search 接口密钥",
"More": "更多", "More": "更多",
"More Concise": "精炼表达", "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.": "注意:如果设置了最低分数,搜索结果只会返回分数大于或等于最低分数的文档。", "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": "笔记",
"Notes Public Sharing": "公开分享笔记", "Notes Public Sharing": "公开分享笔记",
"Notes Sharing": "分享笔记",
"Notification Sound": "通知提示音", "Notification Sound": "通知提示音",
"Notification Webhook": "通知 Webhook", "Notification Webhook": "通知 Webhook",
"Notifications": "桌面通知", "Notifications": "桌面通知",
@ -1274,7 +1282,8 @@
"Prompt updated successfully": "提示词更新成功", "Prompt updated successfully": "提示词更新成功",
"Prompts": "提示词", "Prompts": "提示词",
"Prompts Access": "访问提示词", "Prompts Access": "访问提示词",
"Prompts Public Sharing": "提示词公开共享", "Prompts Public Sharing": "提示词公开分享",
"Prompts Sharing": "分享提示词",
"Provider Type": "服务提供商类型", "Provider Type": "服务提供商类型",
"Public": "公共", "Public": "公共",
"Pull \"{{searchValue}}\" from Ollama.com": "从 Ollama.com 下载 “{{searchValue}}”", "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 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 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.": "设置要使用的停止序列。在该模式下,大语言模型将停止生成文本并返回。可以通过在模型文件中指定多个单独的停止参数来设置多个停止模式。", "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": "设置",
"Settings saved successfully!": "设置已成功保存!", "Settings saved successfully!": "设置已成功保存!",
"Share": "分享", "Share": "分享",
@ -1636,7 +1646,8 @@
"Tools are a function calling system with arbitrary code execution": "工具是一个支持任意代码执行的函数调用系统", "Tools are a function calling system with arbitrary code execution": "工具是一个支持任意代码执行的函数调用系统",
"Tools Function Calling Prompt": "工具函数调用提示词", "Tools Function Calling Prompt": "工具函数调用提示词",
"Tools have a function calling system that allows arbitrary code execution.": "注意:工具有权执行任意代码", "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": "Top K",
"Top K Reranker": "Top K Reranker", "Top K Reranker": "Top K Reranker",
"Transformers": "Transformers", "Transformers": "Transformers",
@ -1684,6 +1695,7 @@
"Upload Pipeline": "上传 Pipeline", "Upload Pipeline": "上传 Pipeline",
"Upload Progress": "上传进度", "Upload Progress": "上传进度",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "上传进度:{{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)", "Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "上传进度:{{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)",
"Uploading file...": "正在上传文件...",
"URL": "URL", "URL": "URL",
"URL is required": "URL 是必填项。", "URL is required": "URL 是必填项。",
"URL Mode": "URL 模式", "URL Mode": "URL 模式",

View file

@ -95,6 +95,7 @@
"Allow Continue Response": "允許繼續回應", "Allow Continue Response": "允許繼續回應",
"Allow Delete Messages": "允許刪除訊息", "Allow Delete Messages": "允許刪除訊息",
"Allow File Upload": "允許上傳檔案", "Allow File Upload": "允許上傳檔案",
"Allow Group Sharing": "允許在群組內分享",
"Allow Multiple Models in Chat": "允許在對話中使用多個模型", "Allow Multiple Models in Chat": "允許在對話中使用多個模型",
"Allow non-local voices": "允許非本機語音", "Allow non-local voices": "允許非本機語音",
"Allow Rate Response": "允許為回應評分", "Allow Rate Response": "允許為回應評分",
@ -388,6 +389,7 @@
"Default description enabled": "預設描述已啟用", "Default description enabled": "預設描述已啟用",
"Default Features": "預設功能", "Default Features": "預設功能",
"Default Filters": "預設篩選器", "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 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": "預設模型",
"Default model updated": "預設模型已更新", "Default model updated": "預設模型已更新",
@ -731,6 +733,7 @@
"Features": "功能", "Features": "功能",
"Features Permissions": "功能權限", "Features Permissions": "功能權限",
"February": "2 月", "February": "2 月",
"Feedback deleted successfully": "成功刪除回饋",
"Feedback Details": "回饋詳情", "Feedback Details": "回饋詳情",
"Feedback History": "回饋歷史", "Feedback History": "回饋歷史",
"Feedbacks": "回饋", "Feedbacks": "回饋",
@ -745,6 +748,7 @@
"File size should not exceed {{maxSize}} MB.": "檔案大小不應超過 {{maxSize}} MB。", "File size should not exceed {{maxSize}} MB.": "檔案大小不應超過 {{maxSize}} MB。",
"File Upload": "檔案上傳", "File Upload": "檔案上傳",
"File uploaded successfully": "成功上傳檔案", "File uploaded successfully": "成功上傳檔案",
"File uploaded!": "檔案已上傳",
"Files": "檔案", "Files": "檔案",
"Filter": "篩選", "Filter": "篩選",
"Filter is now globally disabled": "篩選器已全域停用", "Filter is now globally disabled": "篩選器已全域停用",
@ -858,6 +862,7 @@
"Image Compression": "圖片壓縮", "Image Compression": "圖片壓縮",
"Image Compression Height": "圖片壓縮高度", "Image Compression Height": "圖片壓縮高度",
"Image Compression Width": "圖片壓縮寬度", "Image Compression Width": "圖片壓縮寬度",
"Image Edit": "圖片編輯",
"Image Edit Engine": "圖片編輯引擎", "Image Edit Engine": "圖片編輯引擎",
"Image Generation": "圖片生成", "Image Generation": "圖片生成",
"Image Generation Engine": "圖片生成引擎", "Image Generation Engine": "圖片生成引擎",
@ -933,16 +938,17 @@
"Key is required": "金鑰為必填項目", "Key is required": "金鑰為必填項目",
"Keyboard shortcuts": "鍵盤快捷鍵", "Keyboard shortcuts": "鍵盤快捷鍵",
"Keyboard Shortcuts": "鍵盤快捷鍵", "Keyboard Shortcuts": "鍵盤快捷鍵",
"Knowledge": "知識", "Knowledge": "知識",
"Knowledge Access": "知識存取", "Knowledge Access": "知識存取",
"Knowledge Base": "知識庫", "Knowledge Base": "知識庫",
"Knowledge created successfully.": "成功建立知識。", "Knowledge created successfully.": "成功建立知識。",
"Knowledge deleted successfully.": "成功刪除知識。", "Knowledge deleted successfully.": "成功刪除知識。",
"Knowledge Description": "知識庫描述", "Knowledge Description": "知識庫描述",
"Knowledge Name": "知識庫名稱", "Knowledge Name": "知識庫名稱",
"Knowledge Public Sharing": "知識公開分享", "Knowledge Public Sharing": "知識庫公開分享",
"Knowledge reset successfully.": "成功重設知識。", "Knowledge reset successfully.": "成功重設知識庫。",
"Knowledge updated successfully": "成功更新知識", "Knowledge Sharing": "分享知識庫",
"Knowledge updated successfully": "成功更新知識庫",
"Kokoro.js (Browser)": "Kokoro.js (瀏覽器)", "Kokoro.js (Browser)": "Kokoro.js (瀏覽器)",
"Kokoro.js Dtype": "Kokoro.js Dtype", "Kokoro.js Dtype": "Kokoro.js Dtype",
"Label": "標籤", "Label": "標籤",
@ -1063,6 +1069,7 @@
"Models configuration saved successfully": "成功儲存模型設定", "Models configuration saved successfully": "成功儲存模型設定",
"Models imported successfully": "已成功匯入模型", "Models imported successfully": "已成功匯入模型",
"Models Public Sharing": "模型公開分享", "Models Public Sharing": "模型公開分享",
"Models Sharing": "分享模型",
"Mojeek Search API Key": "Mojeek 搜尋 API 金鑰", "Mojeek Search API Key": "Mojeek 搜尋 API 金鑰",
"More": "更多", "More": "更多",
"More Concise": "精煉表達", "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.": "注意:如果您設定了最低分數,則搜尋只會回傳分數大於或等於最低分數的檔案。", "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": "筆記",
"Notes Public Sharing": "公開分享筆記", "Notes Public Sharing": "公開分享筆記",
"Notes Sharing": "分享筆記",
"Notification Sound": "通知聲音", "Notification Sound": "通知聲音",
"Notification Webhook": "通知 Webhook", "Notification Webhook": "通知 Webhook",
"Notifications": "通知", "Notifications": "通知",
@ -1275,6 +1283,7 @@
"Prompts": "提示詞", "Prompts": "提示詞",
"Prompts Access": "提示詞存取", "Prompts Access": "提示詞存取",
"Prompts Public Sharing": "提示詞公開分享", "Prompts Public Sharing": "提示詞公開分享",
"Prompts Sharing": "分享提示詞",
"Provider Type": "服務提供商類型", "Provider Type": "服務提供商類型",
"Public": "公開", "Public": "公開",
"Pull \"{{searchValue}}\" from Ollama.com": "從 Ollama.com 下載「{{searchValue}}」", "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 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 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.": "設定要使用的停止序列。當遇到此模式時,大型語言模型將停止生成文字並返回。可以在模型檔案中指定多個單獨的停止參數來設定多個停止模式。", "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": "設定",
"Settings saved successfully!": "設定已成功儲存!", "Settings saved successfully!": "設定已成功儲存!",
"Share": "分享", "Share": "分享",
@ -1637,6 +1647,7 @@
"Tools Function Calling Prompt": "工具函式呼叫提示詞", "Tools Function Calling Prompt": "工具函式呼叫提示詞",
"Tools have a function calling system that allows arbitrary code execution.": "工具具有允許執行任意程式碼的函式呼叫系統。", "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": "Top K",
"Top K Reranker": "Top K Reranker", "Top K Reranker": "Top K Reranker",
"Transformers": "Transformers", "Transformers": "Transformers",
@ -1684,6 +1695,7 @@
"Upload Pipeline": "上傳管線", "Upload Pipeline": "上傳管線",
"Upload Progress": "上傳進度", "Upload Progress": "上傳進度",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "上傳進度:{{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)", "Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "上傳進度:{{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)",
"Uploading file...": "正在上傳檔案...",
"URL": "URL", "URL": "URL",
"URL is required": "URL 為必填項目", "URL is required": "URL 為必填項目",
"URL Mode": "URL 模式", "URL Mode": "URL 模式",

View file

@ -8,7 +8,10 @@ import emojiShortCodes from '$lib/emoji-shortcodes.json';
// Backend // Backend
export const WEBUI_NAME = writable(APP_NAME); export const WEBUI_NAME = writable(APP_NAME);
export const WEBUI_VERSION = writable(null); export const WEBUI_VERSION = writable(null);
export const WEBUI_DEPLOYMENT_ID = writable(null);
export const config: Writable<Config | undefined> = writable(undefined); export const config: Writable<Config | undefined> = writable(undefined);
export const user: Writable<SessionUser | undefined> = writable(undefined); export const user: Writable<SessionUser | undefined> = writable(undefined);

View file

@ -14,6 +14,7 @@
import Notes from '$lib/components/notes/Notes.svelte'; import Notes from '$lib/components/notes/Notes.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Sidebar from '$lib/components/icons/Sidebar.svelte'; import Sidebar from '$lib/components/icons/Sidebar.svelte';
import { WEBUI_API_BASE_URL } from '$lib/constants';
let loaded = false; let loaded = false;
@ -95,7 +96,7 @@
> >
<div class=" self-center"> <div class=" self-center">
<img <img
src={$user?.profile_image_url} src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
class="size-6 object-cover rounded-full" class="size-6 object-cover rounded-full"
alt="User profile" alt="User profile"
draggable="false" draggable="false"

View file

@ -17,6 +17,7 @@
theme, theme,
WEBUI_NAME, WEBUI_NAME,
WEBUI_VERSION, WEBUI_VERSION,
WEBUI_DEPLOYMENT_ID,
mobile, mobile,
socket, socket,
chatId, chatId,
@ -46,7 +47,7 @@
import { getAllTags, getChatList } from '$lib/apis/chats'; import { getAllTags, getChatList } from '$lib/apis/chats';
import { chatCompletion } from '$lib/apis/openai'; 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 { bestMatchingLanguage } from '$lib/utils';
import { setTextScale } from '$lib/utils/text-scale'; import { setTextScale } from '$lib/utils/text-scale';
@ -54,10 +55,26 @@
import AppSidebar from '$lib/components/app/AppSidebar.svelte'; import AppSidebar from '$lib/components/app/AppSidebar.svelte';
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import { getUserSettings } from '$lib/apis/users'; 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) // handle frontend updates (https://svelte.dev/docs/kit/configuration#version)
beforeNavigate(({ willUnload, to }) => { beforeNavigate(async ({ willUnload, to }) => {
if (updated.current && !willUnload && to?.url) { if (updated.current && !willUnload && to?.url) {
await unregisterServiceWorkers();
location.href = to.url.href; location.href = to.url.href;
} }
}); });
@ -91,15 +108,30 @@
_socket.on('connect', async () => { _socket.on('connect', async () => {
console.log('connected', _socket.id); console.log('connected', _socket.id);
const version = await getVersion(localStorage.token); const res = await getVersion(localStorage.token);
if (version !== null) {
if ($WEBUI_VERSION !== null && version !== $WEBUI_VERSION) { 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; location.href = location.href;
} else { return;
WEBUI_VERSION.set(version);
} }
} }
if (deploymentId !== null) {
WEBUI_DEPLOYMENT_ID.set(deploymentId);
}
if (version !== null) {
WEBUI_VERSION.set(version);
}
console.log('version', version); console.log('version', version);
if (localStorage.getItem('token')) { if (localStorage.getItem('token')) {
@ -457,7 +489,7 @@
if ($settings?.notificationEnabled ?? false) { if ($settings?.notificationEnabled ?? false) {
new Notification(`${data?.user?.name} (#${event?.channel?.name}) • Open WebUI`, { new Notification(`${data?.user?.name} (#${event?.channel?.name}) • Open WebUI`, {
body: data?.content, 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 ? backendConfig.default_locale
: bestMatchingLanguage(languages, browserLanguages, 'en-US'); : bestMatchingLanguage(languages, browserLanguages, 'en-US');
changeLanguage(lang); changeLanguage(lang);
dayjs.locale(lang);
} }
if (backendConfig) { if (backendConfig) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB