Merge pull request #17827 from open-webui/dev
Release / release (push) Waiting to run
Details
Deploy to HuggingFace Spaces / check-secret (push) Waiting to run
Details
Deploy to HuggingFace Spaces / deploy (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / merge-cuda126-images (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / merge-slim-images (push) Blocked by required conditions
Details
Python CI / Format Backend (3.11.x) (push) Waiting to run
Details
Python CI / Format Backend (3.12.x) (push) Waiting to run
Details
Frontend Build / Format & Build Frontend (push) Waiting to run
Details
Frontend Build / Frontend Unit Tests (push) Waiting to run
Details
Release to PyPI / release (push) Waiting to run
Details
Release / release (push) Waiting to run
Details
Deploy to HuggingFace Spaces / check-secret (push) Waiting to run
Details
Deploy to HuggingFace Spaces / deploy (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Details
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Details
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / merge-cuda126-images (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Details
Create and publish Docker images with specific build args / merge-slim-images (push) Blocked by required conditions
Details
Python CI / Format Backend (3.11.x) (push) Waiting to run
Details
Python CI / Format Backend (3.12.x) (push) Waiting to run
Details
Frontend Build / Format & Build Frontend (push) Waiting to run
Details
Frontend Build / Frontend Unit Tests (push) Waiting to run
Details
Release to PyPI / release (push) Waiting to run
Details
0.6.32
This commit is contained in:
commit
37d1c85c99
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.6.32] - 2025-09-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 🗝️ Permission toggle for public sharing of notes was added, allowing note owners to quickly enable or disable public access from the note settings interface.
|
||||||
|
- ⚠️ A warning is now displayed in the user edit modal if conflicting group permissions are detected, helping administrators resolve access control ambiguities before saving changes.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🧰 Fixed regression where External Tool servers (OpenAPI) were nonfunctional after the 0.6.31 update; external tools integration is now restored and reliable.
|
||||||
|
- 🚑 Resolved a critical bug causing Streamable HTTP OAuth 2.1 (MCP server) integrations to throw a 500 error on first invocation due to missing 'SessionMiddleware'. OAuth 2.1 registration now succeeds and works on subsequent requests as expected.
|
||||||
|
- 🐛 The "Set as default" option is now reliably clickable in model and filter selection menus, fixing cases where the interface appeared unresponsive.
|
||||||
|
- 🛠️ Embed UI now works seamlessly with both default and native function calling flows, ensuring the tool embedding experience is consistent regardless of invocation method.
|
||||||
|
- 🧹 Addressed various minor UI bugs and inconsistencies for a cleaner user experience.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 🧬 MCP tool result handling code was refactored for improved parsing and robustness of tool outputs.
|
||||||
|
- 🧩 The user edit modal was overhauled for clarity and usability, improving the organization of group, permission, and public sharing controls.
|
||||||
|
|
||||||
## [0.6.31] - 2025-09-25
|
## [0.6.31] - 2025-09-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -1217,6 +1217,11 @@ USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = (
|
||||||
== "true"
|
== "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = (
|
||||||
|
os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING", "False").lower()
|
||||||
|
== "true"
|
||||||
|
)
|
||||||
|
|
||||||
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = (
|
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = (
|
||||||
os.environ.get(
|
os.environ.get(
|
||||||
"USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING", "False"
|
"USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING", "False"
|
||||||
|
@ -1354,6 +1359,7 @@ DEFAULT_USER_PERMISSIONS = {
|
||||||
"public_knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING,
|
"public_knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING,
|
||||||
"public_prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING,
|
"public_prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING,
|
||||||
"public_tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING,
|
"public_tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING,
|
||||||
|
"public_notes": USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"controls": USER_PERMISSIONS_CHAT_CONTROLS,
|
"controls": USER_PERMISSIONS_CHAT_CONTROLS,
|
||||||
|
@ -1999,16 +2005,23 @@ if VECTOR_DB == "chroma":
|
||||||
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
||||||
|
|
||||||
# Milvus
|
# Milvus
|
||||||
|
|
||||||
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
|
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
|
||||||
MILVUS_DB = os.environ.get("MILVUS_DB", "default")
|
MILVUS_DB = os.environ.get("MILVUS_DB", "default")
|
||||||
MILVUS_TOKEN = os.environ.get("MILVUS_TOKEN", None)
|
MILVUS_TOKEN = os.environ.get("MILVUS_TOKEN", None)
|
||||||
|
|
||||||
MILVUS_INDEX_TYPE = os.environ.get("MILVUS_INDEX_TYPE", "HNSW")
|
MILVUS_INDEX_TYPE = os.environ.get("MILVUS_INDEX_TYPE", "HNSW")
|
||||||
MILVUS_METRIC_TYPE = os.environ.get("MILVUS_METRIC_TYPE", "COSINE")
|
MILVUS_METRIC_TYPE = os.environ.get("MILVUS_METRIC_TYPE", "COSINE")
|
||||||
MILVUS_HNSW_M = int(os.environ.get("MILVUS_HNSW_M", "16"))
|
MILVUS_HNSW_M = int(os.environ.get("MILVUS_HNSW_M", "16"))
|
||||||
MILVUS_HNSW_EFCONSTRUCTION = int(os.environ.get("MILVUS_HNSW_EFCONSTRUCTION", "100"))
|
MILVUS_HNSW_EFCONSTRUCTION = int(os.environ.get("MILVUS_HNSW_EFCONSTRUCTION", "100"))
|
||||||
MILVUS_IVF_FLAT_NLIST = int(os.environ.get("MILVUS_IVF_FLAT_NLIST", "128"))
|
MILVUS_IVF_FLAT_NLIST = int(os.environ.get("MILVUS_IVF_FLAT_NLIST", "128"))
|
||||||
|
MILVUS_DISKANN_MAX_DEGREE = int(os.environ.get("MILVUS_DISKANN_MAX_DEGREE", "56"))
|
||||||
|
MILVUS_DISKANN_SEARCH_LIST_SIZE = int(
|
||||||
|
os.environ.get("MILVUS_DISKANN_SEARCH_LIST_SIZE", "100")
|
||||||
|
)
|
||||||
|
ENABLE_MILVUS_MULTITENANCY_MODE = (
|
||||||
|
os.environ.get("ENABLE_MILVUS_MULTITENANCY_MODE", "true").lower() == "true"
|
||||||
|
)
|
||||||
|
# Hyphens not allowed, need to use underscores in collection names
|
||||||
|
MILVUS_COLLECTION_PREFIX = os.environ.get("MILVUS_COLLECTION_PREFIX", "open_webui")
|
||||||
|
|
||||||
# Qdrant
|
# Qdrant
|
||||||
QDRANT_URI = os.environ.get("QDRANT_URI", None)
|
QDRANT_URI = os.environ.get("QDRANT_URI", None)
|
||||||
|
|
|
@ -86,6 +86,10 @@ async def get_function_models(request):
|
||||||
try:
|
try:
|
||||||
function_module = get_function_module_by_id(request, pipe.id)
|
function_module = get_function_module_by_id(request, pipe.id)
|
||||||
|
|
||||||
|
has_user_valves = False
|
||||||
|
if hasattr(function_module, "UserValves"):
|
||||||
|
has_user_valves = True
|
||||||
|
|
||||||
# Check if function is a manifold
|
# Check if function is a manifold
|
||||||
if hasattr(function_module, "pipes"):
|
if hasattr(function_module, "pipes"):
|
||||||
sub_pipes = []
|
sub_pipes = []
|
||||||
|
@ -124,6 +128,7 @@ async def get_function_models(request):
|
||||||
"created": pipe.created_at,
|
"created": pipe.created_at,
|
||||||
"owned_by": "openai",
|
"owned_by": "openai",
|
||||||
"pipe": pipe_flag,
|
"pipe": pipe_flag,
|
||||||
|
"has_user_valves": has_user_valves,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -141,6 +146,7 @@ async def get_function_models(request):
|
||||||
"created": pipe.created_at,
|
"created": pipe.created_at,
|
||||||
"owned_by": "openai",
|
"owned_by": "openai",
|
||||||
"pipe": pipe_flag,
|
"pipe": pipe_flag,
|
||||||
|
"has_user_valves": has_user_valves,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -1552,7 +1552,7 @@ async def chat_completion(
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
if mcp_clients := metadata.get("mcp_clients"):
|
if mcp_clients := metadata.get("mcp_clients"):
|
||||||
for client in mcp_clients:
|
for client in mcp_clients.values():
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(f"Error cleaning up: {e}")
|
log.debug(f"Error cleaning up: {e}")
|
||||||
|
@ -1908,38 +1908,33 @@ if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0:
|
||||||
f"mcp:{server_id}", OAuthClientInformationFull(**oauth_client_info)
|
f"mcp:{server_id}", OAuthClientInformationFull(**oauth_client_info)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
# SessionMiddleware is used by authlib for oauth
|
if REDIS_URL:
|
||||||
if len(OAUTH_PROVIDERS) > 0:
|
redis_session_store = RedisStore(
|
||||||
try:
|
url=REDIS_URL,
|
||||||
if REDIS_URL:
|
prefix=(f"{REDIS_KEY_PREFIX}:session:" if REDIS_KEY_PREFIX else "session:"),
|
||||||
redis_session_store = RedisStore(
|
|
||||||
url=REDIS_URL,
|
|
||||||
prefix=(
|
|
||||||
f"{REDIS_KEY_PREFIX}:session:" if REDIS_KEY_PREFIX else "session:"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_middleware(SessionAutoloadMiddleware)
|
|
||||||
app.add_middleware(
|
|
||||||
StarSessionsMiddleware,
|
|
||||||
store=redis_session_store,
|
|
||||||
cookie_name="oui-session",
|
|
||||||
cookie_same_site=WEBUI_SESSION_COOKIE_SAME_SITE,
|
|
||||||
cookie_https_only=WEBUI_SESSION_COOKIE_SECURE,
|
|
||||||
)
|
|
||||||
log.info("Using Redis for session")
|
|
||||||
else:
|
|
||||||
raise ValueError("No Redis URL provided")
|
|
||||||
except Exception as e:
|
|
||||||
app.add_middleware(
|
|
||||||
SessionMiddleware,
|
|
||||||
secret_key=WEBUI_SECRET_KEY,
|
|
||||||
session_cookie="oui-session",
|
|
||||||
same_site=WEBUI_SESSION_COOKIE_SAME_SITE,
|
|
||||||
https_only=WEBUI_SESSION_COOKIE_SECURE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.add_middleware(SessionAutoloadMiddleware)
|
||||||
|
app.add_middleware(
|
||||||
|
StarSessionsMiddleware,
|
||||||
|
store=redis_session_store,
|
||||||
|
cookie_name="owui-session",
|
||||||
|
cookie_same_site=WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||||
|
cookie_https_only=WEBUI_SESSION_COOKIE_SECURE,
|
||||||
|
)
|
||||||
|
log.info("Using Redis for session")
|
||||||
|
else:
|
||||||
|
raise ValueError("No Redis URL provided")
|
||||||
|
except Exception as e:
|
||||||
|
app.add_middleware(
|
||||||
|
SessionMiddleware,
|
||||||
|
secret_key=WEBUI_SECRET_KEY,
|
||||||
|
session_cookie="owui-session",
|
||||||
|
same_site=WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||||
|
https_only=WEBUI_SESSION_COOKIE_SECURE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/oauth/clients/{client_id}/authorize")
|
@app.get("/oauth/clients/{client_id}/authorize")
|
||||||
async def oauth_client_authorize(
|
async def oauth_client_authorize(
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""Add reply_to_id column to message
|
||||||
|
|
||||||
|
Revision ID: a5c220713937
|
||||||
|
Revises: 38d63c18f30f
|
||||||
|
Create Date: 2025-09-27 02:24:18.058455
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "a5c220713937"
|
||||||
|
down_revision: Union[str, None] = "38d63c18f30f"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add 'reply_to_id' column to the 'message' table for replying to messages
|
||||||
|
op.add_column(
|
||||||
|
"message",
|
||||||
|
sa.Column("reply_to_id", sa.Text(), nullable=True),
|
||||||
|
)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Remove 'reply_to_id' column from the 'message' table
|
||||||
|
op.drop_column("message", "reply_to_id")
|
||||||
|
|
||||||
|
pass
|
|
@ -366,6 +366,15 @@ class ChatTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def unarchive_all_chats_by_user_id(self, user_id: str) -> bool:
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
db.query(Chat).filter_by(user_id=user_id).update({"archived": False})
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def update_chat_share_id_by_id(
|
def update_chat_share_id_by_id(
|
||||||
self, id: str, share_id: Optional[str]
|
self, id: str, share_id: Optional[str]
|
||||||
) -> Optional[ChatModel]:
|
) -> Optional[ChatModel]:
|
||||||
|
@ -810,7 +819,7 @@ class ChatTable:
|
||||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
def get_chats_by_folder_id_and_user_id(
|
def get_chats_by_folder_id_and_user_id(
|
||||||
self, folder_id: str, user_id: str
|
self, folder_id: str, user_id: str, skip: int = 0, limit: int = 60
|
||||||
) -> list[ChatModel]:
|
) -> list[ChatModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
query = db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id)
|
query = db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id)
|
||||||
|
@ -819,6 +828,11 @@ class ChatTable:
|
||||||
|
|
||||||
query = query.order_by(Chat.updated_at.desc())
|
query = query.order_by(Chat.updated_at.desc())
|
||||||
|
|
||||||
|
if skip:
|
||||||
|
query = query.offset(skip)
|
||||||
|
if limit:
|
||||||
|
query = query.limit(limit)
|
||||||
|
|
||||||
all_chats = query.all()
|
all_chats = query.all()
|
||||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,20 @@ class FolderModel(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FolderMetadataResponse(BaseModel):
|
||||||
|
icon: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FolderNameIdResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
meta: Optional[FolderMetadataResponse] = None
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
is_expanded: bool = False
|
||||||
|
created_at: int
|
||||||
|
updated_at: int
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -5,6 +5,7 @@ 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.tags import TagModel, Tag, Tags
|
from open_webui.models.tags import TagModel, Tag, Tags
|
||||||
|
from open_webui.models.users import Users, UserNameResponse
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
@ -43,6 +44,7 @@ class Message(Base):
|
||||||
user_id = Column(Text)
|
user_id = Column(Text)
|
||||||
channel_id = Column(Text, nullable=True)
|
channel_id = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
reply_to_id = Column(Text, nullable=True)
|
||||||
parent_id = Column(Text, nullable=True)
|
parent_id = Column(Text, nullable=True)
|
||||||
|
|
||||||
content = Column(Text)
|
content = Column(Text)
|
||||||
|
@ -60,6 +62,7 @@ class MessageModel(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
channel_id: Optional[str] = None
|
channel_id: Optional[str] = None
|
||||||
|
|
||||||
|
reply_to_id: Optional[str] = None
|
||||||
parent_id: Optional[str] = None
|
parent_id: Optional[str] = None
|
||||||
|
|
||||||
content: str
|
content: str
|
||||||
|
@ -77,6 +80,7 @@ class MessageModel(BaseModel):
|
||||||
|
|
||||||
class MessageForm(BaseModel):
|
class MessageForm(BaseModel):
|
||||||
content: str
|
content: str
|
||||||
|
reply_to_id: Optional[str] = None
|
||||||
parent_id: Optional[str] = None
|
parent_id: Optional[str] = None
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
|
@ -88,7 +92,15 @@ class Reactions(BaseModel):
|
||||||
count: int
|
count: int
|
||||||
|
|
||||||
|
|
||||||
class MessageResponse(MessageModel):
|
class MessageUserResponse(MessageModel):
|
||||||
|
user: Optional[UserNameResponse] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MessageReplyToResponse(MessageUserResponse):
|
||||||
|
reply_to_message: Optional[MessageUserResponse] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MessageResponse(MessageReplyToResponse):
|
||||||
latest_reply_at: Optional[int]
|
latest_reply_at: Optional[int]
|
||||||
reply_count: int
|
reply_count: int
|
||||||
reactions: list[Reactions]
|
reactions: list[Reactions]
|
||||||
|
@ -107,6 +119,7 @@ class MessageTable:
|
||||||
"id": id,
|
"id": id,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"channel_id": channel_id,
|
"channel_id": channel_id,
|
||||||
|
"reply_to_id": form_data.reply_to_id,
|
||||||
"parent_id": form_data.parent_id,
|
"parent_id": form_data.parent_id,
|
||||||
"content": form_data.content,
|
"content": form_data.content,
|
||||||
"data": form_data.data,
|
"data": form_data.data,
|
||||||
|
@ -128,19 +141,32 @@ class MessageTable:
|
||||||
if not message:
|
if not message:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
reactions = self.get_reactions_by_message_id(id)
|
reply_to_message = (
|
||||||
replies = self.get_replies_by_message_id(id)
|
self.get_message_by_id(message.reply_to_id)
|
||||||
|
if message.reply_to_id
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
return MessageResponse(
|
reactions = self.get_reactions_by_message_id(id)
|
||||||
**{
|
thread_replies = self.get_thread_replies_by_message_id(id)
|
||||||
|
|
||||||
|
user = Users.get_user_by_id(message.user_id)
|
||||||
|
return MessageResponse.model_validate(
|
||||||
|
{
|
||||||
**MessageModel.model_validate(message).model_dump(),
|
**MessageModel.model_validate(message).model_dump(),
|
||||||
"latest_reply_at": replies[0].created_at if replies else None,
|
"user": user.model_dump() if user else None,
|
||||||
"reply_count": len(replies),
|
"reply_to_message": (
|
||||||
|
reply_to_message.model_dump() if reply_to_message else None
|
||||||
|
),
|
||||||
|
"latest_reply_at": (
|
||||||
|
thread_replies[0].created_at if thread_replies else None
|
||||||
|
),
|
||||||
|
"reply_count": len(thread_replies),
|
||||||
"reactions": reactions,
|
"reactions": reactions,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_replies_by_message_id(self, id: str) -> list[MessageModel]:
|
def get_thread_replies_by_message_id(self, id: str) -> list[MessageReplyToResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
all_messages = (
|
all_messages = (
|
||||||
db.query(Message)
|
db.query(Message)
|
||||||
|
@ -148,7 +174,27 @@ class MessageTable:
|
||||||
.order_by(Message.created_at.desc())
|
.order_by(Message.created_at.desc())
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return [MessageModel.model_validate(message) for message in all_messages]
|
|
||||||
|
messages = []
|
||||||
|
for message in all_messages:
|
||||||
|
reply_to_message = (
|
||||||
|
self.get_message_by_id(message.reply_to_id)
|
||||||
|
if message.reply_to_id
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
MessageReplyToResponse.model_validate(
|
||||||
|
{
|
||||||
|
**MessageModel.model_validate(message).model_dump(),
|
||||||
|
"reply_to_message": (
|
||||||
|
reply_to_message.model_dump()
|
||||||
|
if reply_to_message
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return messages
|
||||||
|
|
||||||
def get_reply_user_ids_by_message_id(self, id: str) -> list[str]:
|
def get_reply_user_ids_by_message_id(self, id: str) -> list[str]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
@ -159,7 +205,7 @@ class MessageTable:
|
||||||
|
|
||||||
def get_messages_by_channel_id(
|
def get_messages_by_channel_id(
|
||||||
self, channel_id: str, skip: int = 0, limit: int = 50
|
self, channel_id: str, skip: int = 0, limit: int = 50
|
||||||
) -> list[MessageModel]:
|
) -> list[MessageReplyToResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
all_messages = (
|
all_messages = (
|
||||||
db.query(Message)
|
db.query(Message)
|
||||||
|
@ -169,11 +215,31 @@ class MessageTable:
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return [MessageModel.model_validate(message) for message in all_messages]
|
|
||||||
|
messages = []
|
||||||
|
for message in all_messages:
|
||||||
|
reply_to_message = (
|
||||||
|
self.get_message_by_id(message.reply_to_id)
|
||||||
|
if message.reply_to_id
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
MessageReplyToResponse.model_validate(
|
||||||
|
{
|
||||||
|
**MessageModel.model_validate(message).model_dump(),
|
||||||
|
"reply_to_message": (
|
||||||
|
reply_to_message.model_dump()
|
||||||
|
if reply_to_message
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return messages
|
||||||
|
|
||||||
def get_messages_by_parent_id(
|
def get_messages_by_parent_id(
|
||||||
self, channel_id: str, parent_id: str, skip: int = 0, limit: int = 50
|
self, channel_id: str, parent_id: str, skip: int = 0, limit: int = 50
|
||||||
) -> list[MessageModel]:
|
) -> list[MessageReplyToResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
message = db.get(Message, parent_id)
|
message = db.get(Message, parent_id)
|
||||||
|
|
||||||
|
@ -193,7 +259,26 @@ class MessageTable:
|
||||||
if len(all_messages) < limit:
|
if len(all_messages) < limit:
|
||||||
all_messages.append(message)
|
all_messages.append(message)
|
||||||
|
|
||||||
return [MessageModel.model_validate(message) for message in all_messages]
|
messages = []
|
||||||
|
for message in all_messages:
|
||||||
|
reply_to_message = (
|
||||||
|
self.get_message_by_id(message.reply_to_id)
|
||||||
|
if message.reply_to_id
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
MessageReplyToResponse.model_validate(
|
||||||
|
{
|
||||||
|
**MessageModel.model_validate(message).model_dump(),
|
||||||
|
"reply_to_message": (
|
||||||
|
reply_to_message.model_dump()
|
||||||
|
if reply_to_message
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return messages
|
||||||
|
|
||||||
def update_message_by_id(
|
def update_message_by_id(
|
||||||
self, id: str, form_data: MessageForm
|
self, id: str, form_data: MessageForm
|
||||||
|
|
|
@ -11,7 +11,7 @@ from open_webui.retrieval.vector.main import (
|
||||||
SearchResult,
|
SearchResult,
|
||||||
GetResult,
|
GetResult,
|
||||||
)
|
)
|
||||||
from open_webui.retrieval.vector.utils import stringify_metadata
|
from open_webui.retrieval.vector.utils import process_metadata
|
||||||
|
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
CHROMA_DATA_PATH,
|
CHROMA_DATA_PATH,
|
||||||
|
@ -146,7 +146,7 @@ class ChromaClient(VectorDBBase):
|
||||||
ids = [item["id"] for item in items]
|
ids = [item["id"] for item in items]
|
||||||
documents = [item["text"] for item in items]
|
documents = [item["text"] for item in items]
|
||||||
embeddings = [item["vector"] for item in items]
|
embeddings = [item["vector"] for item in items]
|
||||||
metadatas = [stringify_metadata(item["metadata"]) for item in items]
|
metadatas = [process_metadata(item["metadata"]) for item in items]
|
||||||
|
|
||||||
for batch in create_batches(
|
for batch in create_batches(
|
||||||
api=self.client,
|
api=self.client,
|
||||||
|
@ -166,7 +166,7 @@ class ChromaClient(VectorDBBase):
|
||||||
ids = [item["id"] for item in items]
|
ids = [item["id"] for item in items]
|
||||||
documents = [item["text"] for item in items]
|
documents = [item["text"] for item in items]
|
||||||
embeddings = [item["vector"] for item in items]
|
embeddings = [item["vector"] for item in items]
|
||||||
metadatas = [stringify_metadata(item["metadata"]) for item in items]
|
metadatas = [process_metadata(item["metadata"]) for item in items]
|
||||||
|
|
||||||
collection.upsert(
|
collection.upsert(
|
||||||
ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
|
ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import Optional
|
||||||
import ssl
|
import ssl
|
||||||
from elasticsearch.helpers import bulk, scan
|
from elasticsearch.helpers import bulk, scan
|
||||||
|
|
||||||
from open_webui.retrieval.vector.utils import stringify_metadata
|
from open_webui.retrieval.vector.utils import process_metadata
|
||||||
from open_webui.retrieval.vector.main import (
|
from open_webui.retrieval.vector.main import (
|
||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
|
@ -245,7 +245,7 @@ class ElasticsearchClient(VectorDBBase):
|
||||||
"collection": collection_name,
|
"collection": collection_name,
|
||||||
"vector": item["vector"],
|
"vector": item["vector"],
|
||||||
"text": item["text"],
|
"text": item["text"],
|
||||||
"metadata": stringify_metadata(item["metadata"]),
|
"metadata": process_metadata(item["metadata"]),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for item in batch
|
for item in batch
|
||||||
|
@ -266,7 +266,7 @@ class ElasticsearchClient(VectorDBBase):
|
||||||
"collection": collection_name,
|
"collection": collection_name,
|
||||||
"vector": item["vector"],
|
"vector": item["vector"],
|
||||||
"text": item["text"],
|
"text": item["text"],
|
||||||
"metadata": stringify_metadata(item["metadata"]),
|
"metadata": process_metadata(item["metadata"]),
|
||||||
},
|
},
|
||||||
"doc_as_upsert": True,
|
"doc_as_upsert": True,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.retrieval.vector.utils import stringify_metadata
|
from open_webui.retrieval.vector.utils import process_metadata
|
||||||
from open_webui.retrieval.vector.main import (
|
from open_webui.retrieval.vector.main import (
|
||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
|
@ -22,6 +22,8 @@ from open_webui.config import (
|
||||||
MILVUS_HNSW_M,
|
MILVUS_HNSW_M,
|
||||||
MILVUS_HNSW_EFCONSTRUCTION,
|
MILVUS_HNSW_EFCONSTRUCTION,
|
||||||
MILVUS_IVF_FLAT_NLIST,
|
MILVUS_IVF_FLAT_NLIST,
|
||||||
|
MILVUS_DISKANN_MAX_DEGREE,
|
||||||
|
MILVUS_DISKANN_SEARCH_LIST_SIZE,
|
||||||
)
|
)
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
@ -131,12 +133,18 @@ class MilvusClient(VectorDBBase):
|
||||||
elif index_type == "IVF_FLAT":
|
elif index_type == "IVF_FLAT":
|
||||||
index_creation_params = {"nlist": MILVUS_IVF_FLAT_NLIST}
|
index_creation_params = {"nlist": MILVUS_IVF_FLAT_NLIST}
|
||||||
log.info(f"IVF_FLAT params: {index_creation_params}")
|
log.info(f"IVF_FLAT params: {index_creation_params}")
|
||||||
|
elif index_type == "DISKANN":
|
||||||
|
index_creation_params = {
|
||||||
|
"max_degree": MILVUS_DISKANN_MAX_DEGREE,
|
||||||
|
"search_list_size": MILVUS_DISKANN_SEARCH_LIST_SIZE,
|
||||||
|
}
|
||||||
|
log.info(f"DISKANN params: {index_creation_params}")
|
||||||
elif index_type in ["FLAT", "AUTOINDEX"]:
|
elif index_type in ["FLAT", "AUTOINDEX"]:
|
||||||
log.info(f"Using {index_type} index with no specific build-time params.")
|
log.info(f"Using {index_type} index with no specific build-time params.")
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(
|
||||||
f"Unsupported MILVUS_INDEX_TYPE: '{index_type}'. "
|
f"Unsupported MILVUS_INDEX_TYPE: '{index_type}'. "
|
||||||
f"Supported types: HNSW, IVF_FLAT, FLAT, AUTOINDEX. "
|
f"Supported types: HNSW, IVF_FLAT, DISKANN, FLAT, AUTOINDEX. "
|
||||||
f"Milvus will use its default for the collection if this type is not directly supported for index creation."
|
f"Milvus will use its default for the collection if this type is not directly supported for index creation."
|
||||||
)
|
)
|
||||||
# For unsupported types, pass the type directly to Milvus; it might handle it or use a default.
|
# For unsupported types, pass the type directly to Milvus; it might handle it or use a default.
|
||||||
|
@ -189,7 +197,7 @@ class MilvusClient(VectorDBBase):
|
||||||
)
|
)
|
||||||
return self._result_to_search_result(result)
|
return self._result_to_search_result(result)
|
||||||
|
|
||||||
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
|
def query(self, collection_name: str, filter: dict, limit: int = -1):
|
||||||
connections.connect(uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB)
|
connections.connect(uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB)
|
||||||
|
|
||||||
# Construct the filter string for querying
|
# Construct the filter string for querying
|
||||||
|
@ -222,7 +230,7 @@ class MilvusClient(VectorDBBase):
|
||||||
"data",
|
"data",
|
||||||
"metadata",
|
"metadata",
|
||||||
],
|
],
|
||||||
limit=limit, # Pass the limit directly; None means no limit.
|
limit=limit, # Pass the limit directly; -1 means no limit.
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
@ -249,7 +257,7 @@ class MilvusClient(VectorDBBase):
|
||||||
)
|
)
|
||||||
# Using query with a trivial filter to get all items.
|
# Using query with a trivial filter to get all items.
|
||||||
# This will use the paginated query logic.
|
# This will use the paginated query logic.
|
||||||
return self.query(collection_name=collection_name, filter={}, limit=None)
|
return self.query(collection_name=collection_name, filter={}, limit=-1)
|
||||||
|
|
||||||
def insert(self, collection_name: str, items: list[VectorItem]):
|
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||||
# Insert the items into the collection, if the collection does not exist, it will be created.
|
# Insert the items into the collection, if the collection does not exist, it will be created.
|
||||||
|
@ -281,7 +289,7 @@ class MilvusClient(VectorDBBase):
|
||||||
"id": item["id"],
|
"id": item["id"],
|
||||||
"vector": item["vector"],
|
"vector": item["vector"],
|
||||||
"data": {"text": item["text"]},
|
"data": {"text": item["text"]},
|
||||||
"metadata": stringify_metadata(item["metadata"]),
|
"metadata": process_metadata(item["metadata"]),
|
||||||
}
|
}
|
||||||
for item in items
|
for item in items
|
||||||
],
|
],
|
||||||
|
@ -317,7 +325,7 @@ class MilvusClient(VectorDBBase):
|
||||||
"id": item["id"],
|
"id": item["id"],
|
||||||
"vector": item["vector"],
|
"vector": item["vector"],
|
||||||
"data": {"text": item["text"]},
|
"data": {"text": item["text"]},
|
||||||
"metadata": stringify_metadata(item["metadata"]),
|
"metadata": process_metadata(item["metadata"]),
|
||||||
}
|
}
|
||||||
for item in items
|
for item in items
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,282 @@
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Tuple, List, Dict, Any
|
||||||
|
|
||||||
|
from open_webui.config import (
|
||||||
|
MILVUS_URI,
|
||||||
|
MILVUS_TOKEN,
|
||||||
|
MILVUS_DB,
|
||||||
|
MILVUS_COLLECTION_PREFIX,
|
||||||
|
MILVUS_INDEX_TYPE,
|
||||||
|
MILVUS_METRIC_TYPE,
|
||||||
|
MILVUS_HNSW_M,
|
||||||
|
MILVUS_HNSW_EFCONSTRUCTION,
|
||||||
|
MILVUS_IVF_FLAT_NLIST,
|
||||||
|
)
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
from open_webui.retrieval.vector.main import (
|
||||||
|
GetResult,
|
||||||
|
SearchResult,
|
||||||
|
VectorDBBase,
|
||||||
|
VectorItem,
|
||||||
|
)
|
||||||
|
from pymilvus import (
|
||||||
|
connections,
|
||||||
|
utility,
|
||||||
|
Collection,
|
||||||
|
CollectionSchema,
|
||||||
|
FieldSchema,
|
||||||
|
DataType,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
RESOURCE_ID_FIELD = "resource_id"
|
||||||
|
|
||||||
|
|
||||||
|
class MilvusClient(VectorDBBase):
|
||||||
|
def __init__(self):
|
||||||
|
# Milvus collection names can only contain numbers, letters, and underscores.
|
||||||
|
self.collection_prefix = MILVUS_COLLECTION_PREFIX.replace("-", "_")
|
||||||
|
connections.connect(
|
||||||
|
alias="default",
|
||||||
|
uri=MILVUS_URI,
|
||||||
|
token=MILVUS_TOKEN,
|
||||||
|
db_name=MILVUS_DB,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Main collection types for multi-tenancy
|
||||||
|
self.MEMORY_COLLECTION = f"{self.collection_prefix}_memories"
|
||||||
|
self.KNOWLEDGE_COLLECTION = f"{self.collection_prefix}_knowledge"
|
||||||
|
self.FILE_COLLECTION = f"{self.collection_prefix}_files"
|
||||||
|
self.WEB_SEARCH_COLLECTION = f"{self.collection_prefix}_web_search"
|
||||||
|
self.HASH_BASED_COLLECTION = f"{self.collection_prefix}_hash_based"
|
||||||
|
self.shared_collections = [
|
||||||
|
self.MEMORY_COLLECTION,
|
||||||
|
self.KNOWLEDGE_COLLECTION,
|
||||||
|
self.FILE_COLLECTION,
|
||||||
|
self.WEB_SEARCH_COLLECTION,
|
||||||
|
self.HASH_BASED_COLLECTION,
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_collection_and_resource_id(self, collection_name: str) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Maps the traditional collection name to multi-tenant collection and resource ID.
|
||||||
|
|
||||||
|
WARNING: This mapping relies on current Open WebUI naming conventions for
|
||||||
|
collection names. If Open WebUI changes how it generates collection names
|
||||||
|
(e.g., "user-memory-" prefix, "file-" prefix, web search patterns, or hash
|
||||||
|
formats), this mapping will break and route data to incorrect collections.
|
||||||
|
POTENTIALLY CAUSING HUGE DATA CORRUPTION, DATA CONSISTENCY ISSUES AND INCORRECT
|
||||||
|
DATA MAPPING INSIDE THE DATABASE.
|
||||||
|
"""
|
||||||
|
resource_id = collection_name
|
||||||
|
|
||||||
|
if collection_name.startswith("user-memory-"):
|
||||||
|
return self.MEMORY_COLLECTION, resource_id
|
||||||
|
elif collection_name.startswith("file-"):
|
||||||
|
return self.FILE_COLLECTION, resource_id
|
||||||
|
elif collection_name.startswith("web-search-"):
|
||||||
|
return self.WEB_SEARCH_COLLECTION, resource_id
|
||||||
|
elif len(collection_name) == 63 and all(
|
||||||
|
c in "0123456789abcdef" for c in collection_name
|
||||||
|
):
|
||||||
|
return self.HASH_BASED_COLLECTION, resource_id
|
||||||
|
else:
|
||||||
|
return self.KNOWLEDGE_COLLECTION, resource_id
|
||||||
|
|
||||||
|
def _create_shared_collection(self, mt_collection_name: str, dimension: int):
|
||||||
|
fields = [
|
||||||
|
FieldSchema(
|
||||||
|
name="id",
|
||||||
|
dtype=DataType.VARCHAR,
|
||||||
|
is_primary=True,
|
||||||
|
auto_id=False,
|
||||||
|
max_length=36,
|
||||||
|
),
|
||||||
|
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=dimension),
|
||||||
|
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535),
|
||||||
|
FieldSchema(name="metadata", dtype=DataType.JSON),
|
||||||
|
FieldSchema(name=RESOURCE_ID_FIELD, dtype=DataType.VARCHAR, max_length=255),
|
||||||
|
]
|
||||||
|
schema = CollectionSchema(fields, "Shared collection for multi-tenancy")
|
||||||
|
collection = Collection(mt_collection_name, schema)
|
||||||
|
|
||||||
|
index_params = {
|
||||||
|
"metric_type": MILVUS_METRIC_TYPE,
|
||||||
|
"index_type": MILVUS_INDEX_TYPE,
|
||||||
|
"params": {},
|
||||||
|
}
|
||||||
|
if MILVUS_INDEX_TYPE == "HNSW":
|
||||||
|
index_params["params"] = {
|
||||||
|
"M": MILVUS_HNSW_M,
|
||||||
|
"efConstruction": MILVUS_HNSW_EFCONSTRUCTION,
|
||||||
|
}
|
||||||
|
elif MILVUS_INDEX_TYPE == "IVF_FLAT":
|
||||||
|
index_params["params"] = {"nlist": MILVUS_IVF_FLAT_NLIST}
|
||||||
|
|
||||||
|
collection.create_index("vector", index_params)
|
||||||
|
collection.create_index(RESOURCE_ID_FIELD)
|
||||||
|
log.info(f"Created shared collection: {mt_collection_name}")
|
||||||
|
return collection
|
||||||
|
|
||||||
|
def _ensure_collection(self, mt_collection_name: str, dimension: int):
|
||||||
|
if not utility.has_collection(mt_collection_name):
|
||||||
|
self._create_shared_collection(mt_collection_name, dimension)
|
||||||
|
|
||||||
|
def has_collection(self, collection_name: str) -> bool:
|
||||||
|
mt_collection, resource_id = self._get_collection_and_resource_id(
|
||||||
|
collection_name
|
||||||
|
)
|
||||||
|
if not utility.has_collection(mt_collection):
|
||||||
|
return False
|
||||||
|
|
||||||
|
collection = Collection(mt_collection)
|
||||||
|
collection.load()
|
||||||
|
res = collection.query(expr=f"{RESOURCE_ID_FIELD} == '{resource_id}'", limit=1)
|
||||||
|
return len(res) > 0
|
||||||
|
|
||||||
|
def upsert(self, collection_name: str, items: List[VectorItem]):
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
mt_collection, resource_id = self._get_collection_and_resource_id(
|
||||||
|
collection_name
|
||||||
|
)
|
||||||
|
dimension = len(items[0]["vector"])
|
||||||
|
self._ensure_collection(mt_collection, dimension)
|
||||||
|
collection = Collection(mt_collection)
|
||||||
|
|
||||||
|
entities = [
|
||||||
|
{
|
||||||
|
"id": item["id"],
|
||||||
|
"vector": item["vector"],
|
||||||
|
"text": item["text"],
|
||||||
|
"metadata": item["metadata"],
|
||||||
|
RESOURCE_ID_FIELD: resource_id,
|
||||||
|
}
|
||||||
|
for item in items
|
||||||
|
]
|
||||||
|
collection.insert(entities)
|
||||||
|
collection.flush()
|
||||||
|
|
||||||
|
def search(
|
||||||
|
self, collection_name: str, vectors: List[List[float]], limit: int
|
||||||
|
) -> Optional[SearchResult]:
|
||||||
|
if not vectors:
|
||||||
|
return None
|
||||||
|
|
||||||
|
mt_collection, resource_id = self._get_collection_and_resource_id(
|
||||||
|
collection_name
|
||||||
|
)
|
||||||
|
if not utility.has_collection(mt_collection):
|
||||||
|
return None
|
||||||
|
|
||||||
|
collection = Collection(mt_collection)
|
||||||
|
collection.load()
|
||||||
|
|
||||||
|
search_params = {"metric_type": MILVUS_METRIC_TYPE, "params": {}}
|
||||||
|
results = collection.search(
|
||||||
|
data=vectors,
|
||||||
|
anns_field="vector",
|
||||||
|
param=search_params,
|
||||||
|
limit=limit,
|
||||||
|
expr=f"{RESOURCE_ID_FIELD} == '{resource_id}'",
|
||||||
|
output_fields=["id", "text", "metadata"],
|
||||||
|
)
|
||||||
|
|
||||||
|
ids, documents, metadatas, distances = [], [], [], []
|
||||||
|
for hits in results:
|
||||||
|
batch_ids, batch_docs, batch_metadatas, batch_dists = [], [], [], []
|
||||||
|
for hit in hits:
|
||||||
|
batch_ids.append(hit.entity.get("id"))
|
||||||
|
batch_docs.append(hit.entity.get("text"))
|
||||||
|
batch_metadatas.append(hit.entity.get("metadata"))
|
||||||
|
batch_dists.append(hit.distance)
|
||||||
|
ids.append(batch_ids)
|
||||||
|
documents.append(batch_docs)
|
||||||
|
metadatas.append(batch_metadatas)
|
||||||
|
distances.append(batch_dists)
|
||||||
|
|
||||||
|
return SearchResult(
|
||||||
|
ids=ids, documents=documents, metadatas=metadatas, distances=distances
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(
|
||||||
|
self,
|
||||||
|
collection_name: str,
|
||||||
|
ids: Optional[List[str]] = None,
|
||||||
|
filter: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
mt_collection, resource_id = self._get_collection_and_resource_id(
|
||||||
|
collection_name
|
||||||
|
)
|
||||||
|
if not utility.has_collection(mt_collection):
|
||||||
|
return
|
||||||
|
|
||||||
|
collection = Collection(mt_collection)
|
||||||
|
|
||||||
|
# Build expression
|
||||||
|
expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'"]
|
||||||
|
if ids:
|
||||||
|
# Milvus expects a string list for 'in' operator
|
||||||
|
id_list_str = ", ".join([f"'{id_val}'" for id_val in ids])
|
||||||
|
expr.append(f"id in [{id_list_str}]")
|
||||||
|
|
||||||
|
if filter:
|
||||||
|
for key, value in filter.items():
|
||||||
|
expr.append(f"metadata['{key}'] == '{value}'")
|
||||||
|
|
||||||
|
collection.delete(" and ".join(expr))
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
for collection_name in self.shared_collections:
|
||||||
|
if utility.has_collection(collection_name):
|
||||||
|
utility.drop_collection(collection_name)
|
||||||
|
|
||||||
|
def delete_collection(self, collection_name: str):
|
||||||
|
mt_collection, resource_id = self._get_collection_and_resource_id(
|
||||||
|
collection_name
|
||||||
|
)
|
||||||
|
if not utility.has_collection(mt_collection):
|
||||||
|
return
|
||||||
|
|
||||||
|
collection = Collection(mt_collection)
|
||||||
|
collection.delete(f"{RESOURCE_ID_FIELD} == '{resource_id}'")
|
||||||
|
|
||||||
|
def query(
|
||||||
|
self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None
|
||||||
|
) -> Optional[GetResult]:
|
||||||
|
mt_collection, resource_id = self._get_collection_and_resource_id(
|
||||||
|
collection_name
|
||||||
|
)
|
||||||
|
if not utility.has_collection(mt_collection):
|
||||||
|
return None
|
||||||
|
|
||||||
|
collection = Collection(mt_collection)
|
||||||
|
collection.load()
|
||||||
|
|
||||||
|
expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'"]
|
||||||
|
if filter:
|
||||||
|
for key, value in filter.items():
|
||||||
|
if isinstance(value, str):
|
||||||
|
expr.append(f"metadata['{key}'] == '{value}'")
|
||||||
|
else:
|
||||||
|
expr.append(f"metadata['{key}'] == {value}")
|
||||||
|
|
||||||
|
results = collection.query(
|
||||||
|
expr=" and ".join(expr),
|
||||||
|
output_fields=["id", "text", "metadata"],
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
ids = [res["id"] for res in results]
|
||||||
|
documents = [res["text"] for res in results]
|
||||||
|
metadatas = [res["metadata"] for res in results]
|
||||||
|
|
||||||
|
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
|
||||||
|
|
||||||
|
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||||
|
return self.query(collection_name, filter={}, limit=None)
|
||||||
|
|
||||||
|
def insert(self, collection_name: str, items: List[VectorItem]):
|
||||||
|
return self.upsert(collection_name, items)
|
|
@ -2,7 +2,7 @@ from opensearchpy import OpenSearch
|
||||||
from opensearchpy.helpers import bulk
|
from opensearchpy.helpers import bulk
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.retrieval.vector.utils import stringify_metadata
|
from open_webui.retrieval.vector.utils import process_metadata
|
||||||
from open_webui.retrieval.vector.main import (
|
from open_webui.retrieval.vector.main import (
|
||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
|
@ -201,7 +201,7 @@ class OpenSearchClient(VectorDBBase):
|
||||||
"_source": {
|
"_source": {
|
||||||
"vector": item["vector"],
|
"vector": item["vector"],
|
||||||
"text": item["text"],
|
"text": item["text"],
|
||||||
"metadata": stringify_metadata(item["metadata"]),
|
"metadata": process_metadata(item["metadata"]),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for item in batch
|
for item in batch
|
||||||
|
@ -223,7 +223,7 @@ class OpenSearchClient(VectorDBBase):
|
||||||
"doc": {
|
"doc": {
|
||||||
"vector": item["vector"],
|
"vector": item["vector"],
|
||||||
"text": item["text"],
|
"text": item["text"],
|
||||||
"metadata": stringify_metadata(item["metadata"]),
|
"metadata": process_metadata(item["metadata"]),
|
||||||
},
|
},
|
||||||
"doc_as_upsert": True,
|
"doc_as_upsert": True,
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ from sqlalchemy.ext.mutable import MutableDict
|
||||||
from sqlalchemy.exc import NoSuchTableError
|
from sqlalchemy.exc import NoSuchTableError
|
||||||
|
|
||||||
|
|
||||||
from open_webui.retrieval.vector.utils import stringify_metadata
|
from open_webui.retrieval.vector.utils import process_metadata
|
||||||
from open_webui.retrieval.vector.main import (
|
from open_webui.retrieval.vector.main import (
|
||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
|
@ -265,7 +265,7 @@ class PgvectorClient(VectorDBBase):
|
||||||
vector=vector,
|
vector=vector,
|
||||||
collection_name=collection_name,
|
collection_name=collection_name,
|
||||||
text=item["text"],
|
text=item["text"],
|
||||||
vmetadata=stringify_metadata(item["metadata"]),
|
vmetadata=process_metadata(item["metadata"]),
|
||||||
)
|
)
|
||||||
new_items.append(new_chunk)
|
new_items.append(new_chunk)
|
||||||
self.session.bulk_save_objects(new_items)
|
self.session.bulk_save_objects(new_items)
|
||||||
|
@ -323,7 +323,7 @@ class PgvectorClient(VectorDBBase):
|
||||||
if existing:
|
if existing:
|
||||||
existing.vector = vector
|
existing.vector = vector
|
||||||
existing.text = item["text"]
|
existing.text = item["text"]
|
||||||
existing.vmetadata = stringify_metadata(item["metadata"])
|
existing.vmetadata = process_metadata(item["metadata"])
|
||||||
existing.collection_name = (
|
existing.collection_name = (
|
||||||
collection_name # Update collection_name if necessary
|
collection_name # Update collection_name if necessary
|
||||||
)
|
)
|
||||||
|
@ -333,7 +333,7 @@ class PgvectorClient(VectorDBBase):
|
||||||
vector=vector,
|
vector=vector,
|
||||||
collection_name=collection_name,
|
collection_name=collection_name,
|
||||||
text=item["text"],
|
text=item["text"],
|
||||||
vmetadata=stringify_metadata(item["metadata"]),
|
vmetadata=process_metadata(item["metadata"]),
|
||||||
)
|
)
|
||||||
self.session.add(new_chunk)
|
self.session.add(new_chunk)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
|
@ -32,7 +32,7 @@ from open_webui.config import (
|
||||||
PINECONE_CLOUD,
|
PINECONE_CLOUD,
|
||||||
)
|
)
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
from open_webui.retrieval.vector.utils import stringify_metadata
|
from open_webui.retrieval.vector.utils import process_metadata
|
||||||
|
|
||||||
|
|
||||||
NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system
|
NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system
|
||||||
|
@ -185,7 +185,7 @@ class PineconeClient(VectorDBBase):
|
||||||
point = {
|
point = {
|
||||||
"id": item["id"],
|
"id": item["id"],
|
||||||
"values": item["vector"],
|
"values": item["vector"],
|
||||||
"metadata": stringify_metadata(metadata),
|
"metadata": process_metadata(metadata),
|
||||||
}
|
}
|
||||||
points.append(point)
|
points.append(point)
|
||||||
return points
|
return points
|
||||||
|
|
|
@ -105,6 +105,13 @@ class QdrantClient(VectorDBBase):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (collection_name, tenant_id)
|
tuple: (collection_name, tenant_id)
|
||||||
|
|
||||||
|
WARNING: This mapping relies on current Open WebUI naming conventions for
|
||||||
|
collection names. If Open WebUI changes how it generates collection names
|
||||||
|
(e.g., "user-memory-" prefix, "file-" prefix, web search patterns, or hash
|
||||||
|
formats), this mapping will break and route data to incorrect collections.
|
||||||
|
POTENTIALLY CAUSING HUGE DATA CORRUPTION, DATA CONSISTENCY ISSUES AND INCORRECT
|
||||||
|
DATA MAPPING INSIDE THE DATABASE.
|
||||||
"""
|
"""
|
||||||
# Check for user memory collections
|
# Check for user memory collections
|
||||||
tenant_id = collection_name
|
tenant_id = collection_name
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from open_webui.retrieval.vector.utils import stringify_metadata
|
from open_webui.retrieval.vector.utils import process_metadata
|
||||||
from open_webui.retrieval.vector.main import (
|
from open_webui.retrieval.vector.main import (
|
||||||
VectorDBBase,
|
VectorDBBase,
|
||||||
VectorItem,
|
VectorItem,
|
||||||
|
@ -185,7 +185,7 @@ class S3VectorClient(VectorDBBase):
|
||||||
metadata["text"] = item["text"]
|
metadata["text"] = item["text"]
|
||||||
|
|
||||||
# Convert metadata to string format for consistency
|
# Convert metadata to string format for consistency
|
||||||
metadata = stringify_metadata(metadata)
|
metadata = process_metadata(metadata)
|
||||||
|
|
||||||
# Filter metadata to comply with S3 Vector API limit of 10 keys
|
# Filter metadata to comply with S3 Vector API limit of 10 keys
|
||||||
metadata = self._filter_metadata(metadata, item["id"])
|
metadata = self._filter_metadata(metadata, item["id"])
|
||||||
|
@ -256,7 +256,7 @@ class S3VectorClient(VectorDBBase):
|
||||||
metadata["text"] = item["text"]
|
metadata["text"] = item["text"]
|
||||||
|
|
||||||
# Convert metadata to string format for consistency
|
# Convert metadata to string format for consistency
|
||||||
metadata = stringify_metadata(metadata)
|
metadata = process_metadata(metadata)
|
||||||
|
|
||||||
# Filter metadata to comply with S3 Vector API limit of 10 keys
|
# Filter metadata to comply with S3 Vector API limit of 10 keys
|
||||||
metadata = self._filter_metadata(metadata, item["id"])
|
metadata = self._filter_metadata(metadata, item["id"])
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
from open_webui.retrieval.vector.main import VectorDBBase
|
from open_webui.retrieval.vector.main import VectorDBBase
|
||||||
from open_webui.retrieval.vector.type import VectorType
|
from open_webui.retrieval.vector.type import VectorType
|
||||||
from open_webui.config import VECTOR_DB, ENABLE_QDRANT_MULTITENANCY_MODE
|
from open_webui.config import (
|
||||||
|
VECTOR_DB,
|
||||||
|
ENABLE_QDRANT_MULTITENANCY_MODE,
|
||||||
|
ENABLE_MILVUS_MULTITENANCY_MODE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Vector:
|
class Vector:
|
||||||
|
@ -12,9 +16,16 @@ class Vector:
|
||||||
"""
|
"""
|
||||||
match vector_type:
|
match vector_type:
|
||||||
case VectorType.MILVUS:
|
case VectorType.MILVUS:
|
||||||
from open_webui.retrieval.vector.dbs.milvus import MilvusClient
|
if ENABLE_MILVUS_MULTITENANCY_MODE:
|
||||||
|
from open_webui.retrieval.vector.dbs.milvus_multitenancy import (
|
||||||
|
MilvusClient,
|
||||||
|
)
|
||||||
|
|
||||||
return MilvusClient()
|
return MilvusClient()
|
||||||
|
else:
|
||||||
|
from open_webui.retrieval.vector.dbs.milvus import MilvusClient
|
||||||
|
|
||||||
|
return MilvusClient()
|
||||||
case VectorType.QDRANT:
|
case VectorType.QDRANT:
|
||||||
if ENABLE_QDRANT_MULTITENANCY_MODE:
|
if ENABLE_QDRANT_MULTITENANCY_MODE:
|
||||||
from open_webui.retrieval.vector.dbs.qdrant_multitenancy import (
|
from open_webui.retrieval.vector.dbs.qdrant_multitenancy import (
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
KEYS_TO_EXCLUDE = ["content", "pages", "tables", "paragraphs", "sections", "figures"]
|
||||||
|
|
||||||
def stringify_metadata(
|
|
||||||
|
def filter_metadata(metadata: dict[str, any]) -> dict[str, any]:
|
||||||
|
metadata = {
|
||||||
|
key: value for key, value in metadata.items() if key not in KEYS_TO_EXCLUDE
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
def process_metadata(
|
||||||
metadata: dict[str, any],
|
metadata: dict[str, any],
|
||||||
) -> dict[str, any]:
|
) -> dict[str, any]:
|
||||||
for key, value in metadata.items():
|
for key, value in metadata.items():
|
||||||
|
# Remove large fields
|
||||||
|
if key in KEYS_TO_EXCLUDE:
|
||||||
|
del metadata[key]
|
||||||
|
|
||||||
|
# Convert non-serializable fields to strings
|
||||||
if (
|
if (
|
||||||
isinstance(value, datetime)
|
isinstance(value, datetime)
|
||||||
or isinstance(value, list)
|
or isinstance(value, list)
|
||||||
|
|
|
@ -167,7 +167,7 @@ async def delete_channel_by_id(id: str, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
|
||||||
class MessageUserResponse(MessageResponse):
|
class MessageUserResponse(MessageResponse):
|
||||||
user: UserNameResponse
|
pass
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}/messages", response_model=list[MessageUserResponse])
|
@router.get("/{id}/messages", response_model=list[MessageUserResponse])
|
||||||
|
@ -196,15 +196,17 @@ async def get_channel_messages(
|
||||||
user = Users.get_user_by_id(message.user_id)
|
user = Users.get_user_by_id(message.user_id)
|
||||||
users[message.user_id] = user
|
users[message.user_id] = user
|
||||||
|
|
||||||
replies = Messages.get_replies_by_message_id(message.id)
|
thread_replies = Messages.get_thread_replies_by_message_id(message.id)
|
||||||
latest_reply_at = replies[0].created_at if replies else None
|
latest_thread_reply_at = (
|
||||||
|
thread_replies[0].created_at if thread_replies else None
|
||||||
|
)
|
||||||
|
|
||||||
messages.append(
|
messages.append(
|
||||||
MessageUserResponse(
|
MessageUserResponse(
|
||||||
**{
|
**{
|
||||||
**message.model_dump(),
|
**message.model_dump(),
|
||||||
"reply_count": len(replies),
|
"reply_count": len(thread_replies),
|
||||||
"latest_reply_at": latest_reply_at,
|
"latest_reply_at": latest_thread_reply_at,
|
||||||
"reactions": Messages.get_reactions_by_message_id(message.id),
|
"reactions": Messages.get_reactions_by_message_id(message.id),
|
||||||
"user": UserNameResponse(**users[message.user_id].model_dump()),
|
"user": UserNameResponse(**users[message.user_id].model_dump()),
|
||||||
}
|
}
|
||||||
|
@ -253,12 +255,26 @@ async def model_response_handler(request, channel, message, user):
|
||||||
mentions = extract_mentions(message.content)
|
mentions = extract_mentions(message.content)
|
||||||
message_content = replace_mentions(message.content)
|
message_content = replace_mentions(message.content)
|
||||||
|
|
||||||
|
model_mentions = {}
|
||||||
|
|
||||||
|
# check if the message is a reply to a message sent by a model
|
||||||
|
if (
|
||||||
|
message.reply_to_message
|
||||||
|
and message.reply_to_message.meta
|
||||||
|
and message.reply_to_message.meta.get("model_id", None)
|
||||||
|
):
|
||||||
|
model_id = message.reply_to_message.meta.get("model_id", None)
|
||||||
|
model_mentions[model_id] = {"id": model_id, "id_type": "M"}
|
||||||
|
|
||||||
# check if any of the mentions are models
|
# check if any of the mentions are models
|
||||||
model_mentions = [mention for mention in mentions if mention["id_type"] == "M"]
|
for mention in mentions:
|
||||||
|
if mention["id_type"] == "M" and mention["id"] not in model_mentions:
|
||||||
|
model_mentions[mention["id"]] = mention
|
||||||
|
|
||||||
if not model_mentions:
|
if not model_mentions:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for mention in model_mentions:
|
for mention in model_mentions.values():
|
||||||
model_id = mention["id"]
|
model_id = mention["id"]
|
||||||
model = MODELS.get(model_id, None)
|
model = MODELS.get(model_id, None)
|
||||||
|
|
||||||
|
@ -326,9 +342,9 @@ async def model_response_handler(request, channel, message, user):
|
||||||
|
|
||||||
system_message = {
|
system_message = {
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": f"You are {model.get('name', model_id)}, an AI assistant participating in a threaded conversation. Be helpful, concise, and conversational."
|
"content": f"You are {model.get('name', model_id)}, participating in a threaded conversation. Be concise and conversational."
|
||||||
+ (
|
+ (
|
||||||
f"Here's the thread history:\n\n{''.join([f'{msg}' for msg in thread_history])}\n\nContinue the conversation naturally, addressing the most recent message while being aware of the full context."
|
f"Here's the thread history:\n\n{''.join([f'{msg}' for msg in thread_history])}\n\nContinue the conversation naturally as {model.get('name', model_id)}, addressing the most recent message while being aware of the full context."
|
||||||
if thread_history
|
if thread_history
|
||||||
else ""
|
else ""
|
||||||
),
|
),
|
||||||
|
@ -406,24 +422,14 @@ async def new_message_handler(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = Messages.insert_new_message(form_data, channel.id, user.id)
|
message = Messages.insert_new_message(form_data, channel.id, user.id)
|
||||||
|
|
||||||
if message:
|
if message:
|
||||||
|
message = Messages.get_message_by_id(message.id)
|
||||||
event_data = {
|
event_data = {
|
||||||
"channel_id": channel.id,
|
"channel_id": channel.id,
|
||||||
"message_id": message.id,
|
"message_id": message.id,
|
||||||
"data": {
|
"data": {
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"data": MessageUserResponse(
|
"data": message.model_dump(),
|
||||||
**{
|
|
||||||
**message.model_dump(),
|
|
||||||
"reply_count": 0,
|
|
||||||
"latest_reply_at": None,
|
|
||||||
"reactions": Messages.get_reactions_by_message_id(
|
|
||||||
message.id
|
|
||||||
),
|
|
||||||
"user": UserNameResponse(**user.model_dump()),
|
|
||||||
}
|
|
||||||
).model_dump(),
|
|
||||||
},
|
},
|
||||||
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
||||||
"channel": channel.model_dump(),
|
"channel": channel.model_dump(),
|
||||||
|
@ -447,23 +453,16 @@ async def new_message_handler(
|
||||||
"message_id": parent_message.id,
|
"message_id": parent_message.id,
|
||||||
"data": {
|
"data": {
|
||||||
"type": "message:reply",
|
"type": "message:reply",
|
||||||
"data": MessageUserResponse(
|
"data": parent_message.model_dump(),
|
||||||
**{
|
|
||||||
**parent_message.model_dump(),
|
|
||||||
"user": UserNameResponse(
|
|
||||||
**Users.get_user_by_id(
|
|
||||||
parent_message.user_id
|
|
||||||
).model_dump()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
).model_dump(),
|
|
||||||
},
|
},
|
||||||
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
||||||
"channel": channel.model_dump(),
|
"channel": channel.model_dump(),
|
||||||
},
|
},
|
||||||
to=f"channel:{channel.id}",
|
to=f"channel:{channel.id}",
|
||||||
)
|
)
|
||||||
return MessageModel(**message.model_dump()), channel
|
return message, channel
|
||||||
|
else:
|
||||||
|
raise Exception("Error creating message")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -651,14 +650,7 @@ async def update_message_by_id(
|
||||||
"message_id": message.id,
|
"message_id": message.id,
|
||||||
"data": {
|
"data": {
|
||||||
"type": "message:update",
|
"type": "message:update",
|
||||||
"data": MessageUserResponse(
|
"data": message.model_dump(),
|
||||||
**{
|
|
||||||
**message.model_dump(),
|
|
||||||
"user": UserNameResponse(
|
|
||||||
**user.model_dump()
|
|
||||||
).model_dump(),
|
|
||||||
}
|
|
||||||
).model_dump(),
|
|
||||||
},
|
},
|
||||||
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
||||||
"channel": channel.model_dump(),
|
"channel": channel.model_dump(),
|
||||||
|
@ -724,9 +716,6 @@ async def add_reaction_to_message(
|
||||||
"type": "message:reaction:add",
|
"type": "message:reaction:add",
|
||||||
"data": {
|
"data": {
|
||||||
**message.model_dump(),
|
**message.model_dump(),
|
||||||
"user": UserNameResponse(
|
|
||||||
**Users.get_user_by_id(message.user_id).model_dump()
|
|
||||||
).model_dump(),
|
|
||||||
"name": form_data.name,
|
"name": form_data.name,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -793,9 +782,6 @@ async def remove_reaction_by_id_and_user_id_and_name(
|
||||||
"type": "message:reaction:remove",
|
"type": "message:reaction:remove",
|
||||||
"data": {
|
"data": {
|
||||||
**message.model_dump(),
|
**message.model_dump(),
|
||||||
"user": UserNameResponse(
|
|
||||||
**Users.get_user_by_id(message.user_id).model_dump()
|
|
||||||
).model_dump(),
|
|
||||||
"name": form_data.name,
|
"name": form_data.name,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -882,16 +868,7 @@ async def delete_message_by_id(
|
||||||
"message_id": parent_message.id,
|
"message_id": parent_message.id,
|
||||||
"data": {
|
"data": {
|
||||||
"type": "message:reply",
|
"type": "message:reply",
|
||||||
"data": MessageUserResponse(
|
"data": parent_message.model_dump(),
|
||||||
**{
|
|
||||||
**parent_message.model_dump(),
|
|
||||||
"user": UserNameResponse(
|
|
||||||
**Users.get_user_by_id(
|
|
||||||
parent_message.user_id
|
|
||||||
).model_dump()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
).model_dump(),
|
|
||||||
},
|
},
|
||||||
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
||||||
"channel": channel.model_dump(),
|
"channel": channel.model_dump(),
|
||||||
|
|
|
@ -218,6 +218,28 @@ async def get_chats_by_folder_id(folder_id: str, user=Depends(get_verified_user)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/folder/{folder_id}/list")
|
||||||
|
async def get_chat_list_by_folder_id(
|
||||||
|
folder_id: str, page: Optional[int] = 1, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
limit = 60
|
||||||
|
skip = (page - 1) * limit
|
||||||
|
|
||||||
|
return [
|
||||||
|
{"title": chat.title, "id": chat.id, "updated_at": chat.updated_at}
|
||||||
|
for chat in Chats.get_chats_by_folder_id_and_user_id(
|
||||||
|
folder_id, user.id, skip=skip, limit=limit
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetPinnedChats
|
# GetPinnedChats
|
||||||
############################
|
############################
|
||||||
|
@ -339,6 +361,16 @@ async def archive_all_chats(user=Depends(get_verified_user)):
|
||||||
return Chats.archive_all_chats_by_user_id(user.id)
|
return Chats.archive_all_chats_by_user_id(user.id)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# UnarchiveAllChats
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/unarchive/all", response_model=bool)
|
||||||
|
async def unarchive_all_chats(user=Depends(get_verified_user)):
|
||||||
|
return Chats.unarchive_all_chats_by_user_id(user.id)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetSharedChatById
|
# GetSharedChatById
|
||||||
############################
|
############################
|
||||||
|
|
|
@ -207,38 +207,39 @@ async def verify_tool_servers_config(
|
||||||
if form_data.type == "mcp":
|
if form_data.type == "mcp":
|
||||||
if form_data.auth_type == "oauth_2.1":
|
if form_data.auth_type == "oauth_2.1":
|
||||||
discovery_urls = get_discovery_urls(form_data.url)
|
discovery_urls = get_discovery_urls(form_data.url)
|
||||||
async with aiohttp.ClientSession() as session:
|
for discovery_url in discovery_urls:
|
||||||
async with session.get(
|
log.debug(
|
||||||
discovery_urls[0]
|
f"Trying to fetch OAuth 2.1 discovery document from {discovery_url}"
|
||||||
) as oauth_server_metadata_response:
|
)
|
||||||
if oauth_server_metadata_response.status != 200:
|
async with aiohttp.ClientSession() as session:
|
||||||
raise HTTPException(
|
async with session.get(
|
||||||
status_code=400,
|
discovery_urls[0]
|
||||||
detail=f"Failed to fetch OAuth 2.1 discovery document from {discovery_urls[0]}",
|
) as oauth_server_metadata_response:
|
||||||
)
|
if oauth_server_metadata_response.status == 200:
|
||||||
|
try:
|
||||||
try:
|
oauth_server_metadata = (
|
||||||
oauth_server_metadata = OAuthMetadata.model_validate(
|
OAuthMetadata.model_validate(
|
||||||
await oauth_server_metadata_response.json()
|
await oauth_server_metadata_response.json()
|
||||||
)
|
)
|
||||||
return {
|
)
|
||||||
"status": True,
|
return {
|
||||||
"oauth_server_metadata": oauth_server_metadata.model_dump(
|
"status": True,
|
||||||
mode="json"
|
"oauth_server_metadata": oauth_server_metadata.model_dump(
|
||||||
),
|
mode="json"
|
||||||
}
|
),
|
||||||
except Exception as e:
|
}
|
||||||
log.info(
|
except Exception as e:
|
||||||
f"Failed to parse OAuth 2.1 discovery document: {e}"
|
log.info(
|
||||||
)
|
f"Failed to parse OAuth 2.1 discovery document: {e}"
|
||||||
raise HTTPException(
|
)
|
||||||
status_code=400,
|
raise HTTPException(
|
||||||
detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_urls[0]}",
|
status_code=400,
|
||||||
)
|
detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_urls[0]}",
|
||||||
|
)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Failed to fetch OAuth 2.1 discovery document from {discovery_urls[0]}",
|
detail=f"Failed to fetch OAuth 2.1 discovery document from {discovery_urls}",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -12,6 +12,7 @@ from open_webui.models.folders import (
|
||||||
FolderForm,
|
FolderForm,
|
||||||
FolderUpdateForm,
|
FolderUpdateForm,
|
||||||
FolderModel,
|
FolderModel,
|
||||||
|
FolderNameIdResponse,
|
||||||
Folders,
|
Folders,
|
||||||
)
|
)
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
|
@ -44,7 +45,7 @@ router = APIRouter()
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[FolderModel])
|
@router.get("/", response_model=list[FolderNameIdResponse])
|
||||||
async def get_folders(user=Depends(get_verified_user)):
|
async def get_folders(user=Depends(get_verified_user)):
|
||||||
folders = Folders.get_folders_by_user_id(user.id)
|
folders = Folders.get_folders_by_user_id(user.id)
|
||||||
|
|
||||||
|
@ -76,14 +77,6 @@ async def get_folders(user=Depends(get_verified_user)):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
**folder.model_dump(),
|
**folder.model_dump(),
|
||||||
"items": {
|
|
||||||
"chats": [
|
|
||||||
{"title": chat.title, "id": chat.id, "updated_at": chat.updated_at}
|
|
||||||
for chat in Chats.get_chats_by_folder_id_and_user_id(
|
|
||||||
folder.id, user.id
|
|
||||||
)
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for folder in folders
|
for folder in folders
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
from open_webui.models.models import (
|
from open_webui.models.models import (
|
||||||
ModelForm,
|
ModelForm,
|
||||||
|
@ -12,7 +15,14 @@ from open_webui.models.models import (
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status, Response
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
Depends,
|
||||||
|
HTTPException,
|
||||||
|
Request,
|
||||||
|
status,
|
||||||
|
Response,
|
||||||
|
)
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +30,8 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
from open_webui.utils.access_control import has_access, has_permission
|
from open_webui.utils.access_control import has_access, has_permission
|
||||||
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, STATIC_DIR
|
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, STATIC_DIR
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,6 +105,50 @@ async def export_models(user=Depends(get_admin_user)):
|
||||||
return Models.get_models()
|
return Models.get_models()
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# ImportModels
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
class ModelsImportForm(BaseModel):
|
||||||
|
models: list[dict]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/import", response_model=bool)
|
||||||
|
async def import_models(
|
||||||
|
user: str = Depends(get_admin_user), form_data: ModelsImportForm = (...)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
data = form_data.models
|
||||||
|
if isinstance(data, list):
|
||||||
|
for model_data in data:
|
||||||
|
# Here, you can add logic to validate model_data if needed
|
||||||
|
model_id = model_data.get("id")
|
||||||
|
if model_id:
|
||||||
|
existing_model = Models.get_model_by_id(model_id)
|
||||||
|
if existing_model:
|
||||||
|
# Update existing model
|
||||||
|
model_data["meta"] = model_data.get("meta", {})
|
||||||
|
model_data["params"] = model_data.get("params", {})
|
||||||
|
|
||||||
|
updated_model = ModelForm(
|
||||||
|
**{**existing_model.model_dump(), **model_data}
|
||||||
|
)
|
||||||
|
Models.update_model_by_id(model_id, updated_model)
|
||||||
|
else:
|
||||||
|
# Insert new model
|
||||||
|
model_data["meta"] = model_data.get("meta", {})
|
||||||
|
model_data["params"] = model_data.get("params", {})
|
||||||
|
new_model = ModelForm(**model_data)
|
||||||
|
Models.insert_new_model(user_id=user.id, form_data=new_model)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid JSON format")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# SyncModels
|
# SyncModels
|
||||||
############################
|
############################
|
||||||
|
|
|
@ -180,6 +180,18 @@ async def update_note_by_id(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if user can share publicly
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and form_data.access_control == None
|
||||||
|
and not has_permission(
|
||||||
|
user.id,
|
||||||
|
"sharing.public_notes",
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
form_data.access_control = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
note = Notes.update_note_by_id(id, form_data)
|
note = Notes.update_note_by_id(id, form_data)
|
||||||
await sio.emit(
|
await sio.emit(
|
||||||
|
|
|
@ -78,6 +78,7 @@ from open_webui.retrieval.utils import (
|
||||||
query_doc,
|
query_doc,
|
||||||
query_doc_with_hybrid_search,
|
query_doc_with_hybrid_search,
|
||||||
)
|
)
|
||||||
|
from open_webui.retrieval.vector.utils import filter_metadata
|
||||||
from open_webui.utils.misc import (
|
from open_webui.utils.misc import (
|
||||||
calculate_sha256_string,
|
calculate_sha256_string,
|
||||||
)
|
)
|
||||||
|
@ -1535,7 +1536,7 @@ def process_file(
|
||||||
Document(
|
Document(
|
||||||
page_content=doc.page_content,
|
page_content=doc.page_content,
|
||||||
metadata={
|
metadata={
|
||||||
**doc.metadata,
|
**filter_metadata(doc.metadata),
|
||||||
"name": file.filename,
|
"name": file.filename,
|
||||||
"created_by": file.user_id,
|
"created_by": file.user_id,
|
||||||
"file_id": file.id,
|
"file_id": file.id,
|
||||||
|
|
|
@ -17,7 +17,11 @@ from open_webui.models.tools import (
|
||||||
ToolUserResponse,
|
ToolUserResponse,
|
||||||
Tools,
|
Tools,
|
||||||
)
|
)
|
||||||
from open_webui.utils.plugin import load_tool_module_by_id, replace_imports
|
from open_webui.utils.plugin import (
|
||||||
|
load_tool_module_by_id,
|
||||||
|
replace_imports,
|
||||||
|
get_tool_module_from_cache,
|
||||||
|
)
|
||||||
from open_webui.utils.tools import get_tool_specs
|
from open_webui.utils.tools import get_tool_specs
|
||||||
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.access_control import has_access, has_permission
|
from open_webui.utils.access_control import has_access, has_permission
|
||||||
|
@ -35,6 +39,14 @@ log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_module(request, tool_id, load_from_db=True):
|
||||||
|
"""
|
||||||
|
Get the tool module by its ID.
|
||||||
|
"""
|
||||||
|
tool_module, _ = get_tool_module_from_cache(request, tool_id, load_from_db)
|
||||||
|
return tool_module
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetTools
|
# GetTools
|
||||||
############################
|
############################
|
||||||
|
@ -42,15 +54,19 @@ router = APIRouter()
|
||||||
|
|
||||||
@router.get("/", response_model=list[ToolUserResponse])
|
@router.get("/", response_model=list[ToolUserResponse])
|
||||||
async def get_tools(request: Request, user=Depends(get_verified_user)):
|
async def get_tools(request: Request, user=Depends(get_verified_user)):
|
||||||
tools = [
|
tools = []
|
||||||
ToolUserResponse(
|
|
||||||
**{
|
# Local Tools
|
||||||
**tool.model_dump(),
|
for tool in Tools.get_tools():
|
||||||
"has_user_valves": "class UserValves(BaseModel):" in tool.content,
|
tool_module = get_tool_module(request, tool.id)
|
||||||
}
|
tools.append(
|
||||||
|
ToolUserResponse(
|
||||||
|
**{
|
||||||
|
**tool.model_dump(),
|
||||||
|
"has_user_valves": hasattr(tool_module, "UserValves"),
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for tool in Tools.get_tools()
|
|
||||||
]
|
|
||||||
|
|
||||||
# OpenAPI Tool Servers
|
# OpenAPI Tool Servers
|
||||||
for server in await get_tool_servers(request):
|
for server in await get_tool_servers(request):
|
||||||
|
|
|
@ -157,6 +157,7 @@ class SharingPermissions(BaseModel):
|
||||||
public_knowledge: bool = True
|
public_knowledge: bool = True
|
||||||
public_prompts: bool = True
|
public_prompts: bool = True
|
||||||
public_tools: bool = True
|
public_tools: bool = True
|
||||||
|
public_notes: bool = True
|
||||||
|
|
||||||
|
|
||||||
class ChatPermissions(BaseModel):
|
class ChatPermissions(BaseModel):
|
||||||
|
|
|
@ -705,6 +705,23 @@ def get_event_emitter(request_info, update_db=True):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "type" in event_data and event_data["type"] == "embeds":
|
||||||
|
message = Chats.get_message_by_id_and_message_id(
|
||||||
|
request_info["chat_id"],
|
||||||
|
request_info["message_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
embeds = event_data.get("data", {}).get("embeds", [])
|
||||||
|
embeds.extend(message.get("embeds", []))
|
||||||
|
|
||||||
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
|
request_info["chat_id"],
|
||||||
|
request_info["message_id"],
|
||||||
|
{
|
||||||
|
"embeds": embeds,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if "type" in event_data and event_data["type"] == "files":
|
if "type" in event_data and event_data["type"] == "files":
|
||||||
message = Chats.get_message_by_id_and_message_id(
|
message = Chats.get_message_by_id_and_message_id(
|
||||||
request_info["chat_id"],
|
request_info["chat_id"],
|
||||||
|
|
|
@ -133,6 +133,149 @@ DEFAULT_SOLUTION_TAGS = [("<|begin_of_solution|>", "<|end_of_solution|>")]
|
||||||
DEFAULT_CODE_INTERPRETER_TAGS = [("<code_interpreter>", "</code_interpreter>")]
|
DEFAULT_CODE_INTERPRETER_TAGS = [("<code_interpreter>", "</code_interpreter>")]
|
||||||
|
|
||||||
|
|
||||||
|
def process_tool_result(
|
||||||
|
request,
|
||||||
|
tool_function_name,
|
||||||
|
tool_result,
|
||||||
|
tool_type,
|
||||||
|
direct_tool=False,
|
||||||
|
metadata=None,
|
||||||
|
user=None,
|
||||||
|
):
|
||||||
|
tool_result_embeds = []
|
||||||
|
|
||||||
|
if isinstance(tool_result, HTMLResponse):
|
||||||
|
content_disposition = tool_result.headers.get("Content-Disposition", "")
|
||||||
|
if "inline" in content_disposition:
|
||||||
|
content = tool_result.body.decode("utf-8")
|
||||||
|
tool_result_embeds.append(content)
|
||||||
|
|
||||||
|
if 200 <= tool_result.status_code < 300:
|
||||||
|
tool_result = {
|
||||||
|
"status": "success",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": f"{tool_function_name}: Embedded UI result is active and visible to the user.",
|
||||||
|
}
|
||||||
|
elif 400 <= tool_result.status_code < 500:
|
||||||
|
tool_result = {
|
||||||
|
"status": "error",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": f"{tool_function_name}: Client error {tool_result.status_code} from embedded UI result.",
|
||||||
|
}
|
||||||
|
elif 500 <= tool_result.status_code < 600:
|
||||||
|
tool_result = {
|
||||||
|
"status": "error",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": f"{tool_function_name}: Server error {tool_result.status_code} from embedded UI result.",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
tool_result = {
|
||||||
|
"status": "error",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": f"{tool_function_name}: Unexpected status code {tool_result.status_code} from embedded UI result.",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
tool_result = tool_result.body.decode("utf-8")
|
||||||
|
|
||||||
|
elif (tool_type == "external" and isinstance(tool_result, tuple)) or (
|
||||||
|
direct_tool and isinstance(tool_result, list) and len(tool_result) == 2
|
||||||
|
):
|
||||||
|
tool_result, tool_response_headers = tool_result
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not isinstance(tool_response_headers, dict):
|
||||||
|
tool_response_headers = dict(tool_response_headers)
|
||||||
|
except Exception as e:
|
||||||
|
tool_response_headers = {}
|
||||||
|
log.debug(e)
|
||||||
|
|
||||||
|
if tool_response_headers and isinstance(tool_response_headers, dict):
|
||||||
|
content_disposition = tool_response_headers.get(
|
||||||
|
"Content-Disposition",
|
||||||
|
tool_response_headers.get("content-disposition", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "inline" in content_disposition:
|
||||||
|
content_type = tool_response_headers.get(
|
||||||
|
"Content-Type",
|
||||||
|
tool_response_headers.get("content-type", ""),
|
||||||
|
)
|
||||||
|
location = tool_response_headers.get(
|
||||||
|
"Location",
|
||||||
|
tool_response_headers.get("location", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "text/html" in content_type:
|
||||||
|
# Display as iframe embed
|
||||||
|
tool_result_embeds.append(tool_result)
|
||||||
|
tool_result = {
|
||||||
|
"status": "success",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": f"{tool_function_name}: Embedded UI result is active and visible to the user.",
|
||||||
|
}
|
||||||
|
elif location:
|
||||||
|
tool_result_embeds.append(location)
|
||||||
|
tool_result = {
|
||||||
|
"status": "success",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": f"{tool_function_name}: Embedded UI result is active and visible to the user.",
|
||||||
|
}
|
||||||
|
|
||||||
|
tool_result_files = []
|
||||||
|
|
||||||
|
if isinstance(tool_result, list):
|
||||||
|
if tool_type == "mcp": # MCP
|
||||||
|
tool_response = []
|
||||||
|
for item in tool_result:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
if item.get("type") == "text":
|
||||||
|
text = item.get("text", "")
|
||||||
|
if isinstance(text, str):
|
||||||
|
try:
|
||||||
|
text = json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
tool_response.append(text)
|
||||||
|
elif item.get("type") in ["image", "audio"]:
|
||||||
|
file_url = get_file_url_from_base64(
|
||||||
|
request,
|
||||||
|
f"data:{item.get('mimeType')};base64,{item.get('data', item.get('blob', ''))}",
|
||||||
|
{
|
||||||
|
"chat_id": metadata.get("chat_id", None),
|
||||||
|
"message_id": metadata.get("message_id", None),
|
||||||
|
"session_id": metadata.get("session_id", None),
|
||||||
|
"result": item,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
|
||||||
|
tool_result_files.append(
|
||||||
|
{
|
||||||
|
"type": item.get("type", "data"),
|
||||||
|
"url": file_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tool_result = tool_response[0] if len(tool_response) == 1 else tool_response
|
||||||
|
else: # OpenAPI
|
||||||
|
for item in tool_result:
|
||||||
|
if isinstance(item, str) and item.startswith("data:"):
|
||||||
|
tool_result_files.append(
|
||||||
|
{
|
||||||
|
"type": "data",
|
||||||
|
"content": item,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tool_result.remove(item)
|
||||||
|
|
||||||
|
if isinstance(tool_result, list):
|
||||||
|
tool_result = {"results": tool_result}
|
||||||
|
|
||||||
|
if isinstance(tool_result, dict) or isinstance(tool_result, list):
|
||||||
|
tool_result = json.dumps(tool_result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
return tool_result, tool_result_files, tool_result_embeds
|
||||||
|
|
||||||
|
|
||||||
async def chat_completion_tools_handler(
|
async def chat_completion_tools_handler(
|
||||||
request: Request, body: dict, extra_params: dict, user: UserModel, models, tools
|
request: Request, body: dict, extra_params: dict, user: UserModel, models, tools
|
||||||
) -> tuple[dict, dict]:
|
) -> tuple[dict, dict]:
|
||||||
|
@ -172,6 +315,7 @@ async def chat_completion_tools_handler(
|
||||||
}
|
}
|
||||||
|
|
||||||
event_caller = extra_params["__event_call__"]
|
event_caller = extra_params["__event_call__"]
|
||||||
|
event_emitter = extra_params["__event_emitter__"]
|
||||||
metadata = extra_params["__metadata__"]
|
metadata = extra_params["__metadata__"]
|
||||||
|
|
||||||
task_model_id = get_task_model_id(
|
task_model_id = get_task_model_id(
|
||||||
|
@ -226,8 +370,14 @@ async def chat_completion_tools_handler(
|
||||||
|
|
||||||
tool_function_params = tool_call.get("parameters", {})
|
tool_function_params = tool_call.get("parameters", {})
|
||||||
|
|
||||||
|
tool = None
|
||||||
|
tool_type = ""
|
||||||
|
direct_tool = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tool = tools[tool_function_name]
|
tool = tools[tool_function_name]
|
||||||
|
tool_type = tool.get("type", "")
|
||||||
|
direct_tool = tool.get("direct", False)
|
||||||
|
|
||||||
spec = tool.get("spec", {})
|
spec = tool.get("spec", {})
|
||||||
allowed_params = (
|
allowed_params = (
|
||||||
|
@ -259,18 +409,46 @@ async def chat_completion_tools_handler(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tool_result = str(e)
|
tool_result = str(e)
|
||||||
|
|
||||||
tool_result_files = []
|
tool_result, tool_result_files, tool_result_embeds = (
|
||||||
if isinstance(tool_result, list):
|
process_tool_result(
|
||||||
for item in tool_result:
|
request,
|
||||||
# check if string
|
tool_function_name,
|
||||||
if isinstance(item, str) and item.startswith("data:"):
|
tool_result,
|
||||||
tool_result_files.append(item)
|
tool_type,
|
||||||
tool_result.remove(item)
|
direct_tool,
|
||||||
|
metadata,
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(tool_result, dict) or isinstance(tool_result, list):
|
if event_emitter:
|
||||||
tool_result = json.dumps(tool_result, indent=2)
|
if tool_result_files:
|
||||||
|
await event_emitter(
|
||||||
|
{
|
||||||
|
"type": "files",
|
||||||
|
"data": {
|
||||||
|
"files": tool_result_files,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(tool_result, str):
|
if tool_result_embeds:
|
||||||
|
await event_emitter(
|
||||||
|
{
|
||||||
|
"type": "embeds",
|
||||||
|
"data": {
|
||||||
|
"embeds": tool_result_embeds,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Tool {tool_function_name} result: {tool_result}",
|
||||||
|
tool_result_files,
|
||||||
|
tool_result_embeds,
|
||||||
|
)
|
||||||
|
|
||||||
|
if tool_result:
|
||||||
tool = tools[tool_function_name]
|
tool = tools[tool_function_name]
|
||||||
tool_id = tool.get("tool_id", "")
|
tool_id = tool.get("tool_id", "")
|
||||||
|
|
||||||
|
@ -284,18 +462,19 @@ async def chat_completion_tools_handler(
|
||||||
sources.append(
|
sources.append(
|
||||||
{
|
{
|
||||||
"source": {
|
"source": {
|
||||||
"name": (f"TOOL:{tool_name}"),
|
"name": (f"{tool_name}"),
|
||||||
},
|
},
|
||||||
"document": [tool_result],
|
"document": [str(tool_result)],
|
||||||
"metadata": [
|
"metadata": [
|
||||||
{
|
{
|
||||||
"source": (f"TOOL:{tool_name}"),
|
"source": (f"{tool_name}"),
|
||||||
"parameters": tool_function_params,
|
"parameters": tool_function_params,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tool_result": True,
|
"tool_result": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Citation is not enabled for this tool
|
# Citation is not enabled for this tool
|
||||||
body["messages"] = add_or_update_user_message(
|
body["messages"] = add_or_update_user_message(
|
||||||
f"\nTool `{tool_name}` Output: {tool_result}",
|
f"\nTool `{tool_name}` Output: {tool_result}",
|
||||||
|
@ -1010,7 +1189,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
|
|
||||||
tools_dict = {}
|
tools_dict = {}
|
||||||
|
|
||||||
mcp_clients = []
|
mcp_clients = {}
|
||||||
mcp_tools_dict = {}
|
mcp_tools_dict = {}
|
||||||
|
|
||||||
if tool_ids:
|
if tool_ids:
|
||||||
|
@ -1071,35 +1250,41 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
log.error(f"Error getting OAuth token: {e}")
|
log.error(f"Error getting OAuth token: {e}")
|
||||||
oauth_token = None
|
oauth_token = None
|
||||||
|
|
||||||
mcp_client = MCPClient()
|
mcp_clients[server_id] = MCPClient()
|
||||||
await mcp_client.connect(
|
await mcp_clients[server_id].connect(
|
||||||
url=mcp_server_connection.get("url", ""),
|
url=mcp_server_connection.get("url", ""),
|
||||||
headers=headers if headers else None,
|
headers=headers if headers else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
tool_specs = await mcp_client.list_tool_specs()
|
tool_specs = await mcp_clients[server_id].list_tool_specs()
|
||||||
for tool_spec in tool_specs:
|
for tool_spec in tool_specs:
|
||||||
|
|
||||||
def make_tool_function(function_name):
|
def make_tool_function(client, function_name):
|
||||||
async def tool_function(**kwargs):
|
async def tool_function(**kwargs):
|
||||||
return await mcp_client.call_tool(
|
print(kwargs)
|
||||||
|
print(client)
|
||||||
|
print(await client.list_tool_specs())
|
||||||
|
return await client.call_tool(
|
||||||
function_name,
|
function_name,
|
||||||
function_args=kwargs,
|
function_args=kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
return tool_function
|
return tool_function
|
||||||
|
|
||||||
tool_function = make_tool_function(tool_spec["name"])
|
tool_function = make_tool_function(
|
||||||
|
mcp_clients[server_id], tool_spec["name"]
|
||||||
|
)
|
||||||
|
|
||||||
mcp_tools_dict[tool_spec["name"]] = {
|
mcp_tools_dict[f"{server_id}_{tool_spec['name']}"] = {
|
||||||
"spec": tool_spec,
|
"spec": {
|
||||||
|
**tool_spec,
|
||||||
|
"name": f"{server_id}_{tool_spec['name']}",
|
||||||
|
},
|
||||||
"callable": tool_function,
|
"callable": tool_function,
|
||||||
"type": "mcp",
|
"type": "mcp",
|
||||||
"client": mcp_client,
|
"client": mcp_clients[server_id],
|
||||||
"direct": False,
|
"direct": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
mcp_clients.append(mcp_client)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
continue
|
continue
|
||||||
|
@ -1140,7 +1325,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
{"type": "function", "function": tool.get("spec", {})}
|
{"type": "function", "function": tool.get("spec", {})}
|
||||||
for tool in tools_dict.values()
|
for tool in tools_dict.values()
|
||||||
]
|
]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# If the function calling is not native, then call the tools function calling handler
|
# If the function calling is not native, then call the tools function calling handler
|
||||||
try:
|
try:
|
||||||
|
@ -1165,9 +1349,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
citation_idx_map = {}
|
citation_idx_map = {}
|
||||||
|
|
||||||
for source in sources:
|
for source in sources:
|
||||||
is_tool_result = source.get("tool_result", False)
|
if "document" in source:
|
||||||
|
|
||||||
if "document" in source and not is_tool_result:
|
|
||||||
for document_text, document_metadata in zip(
|
for document_text, document_metadata in zip(
|
||||||
source["document"], source["metadata"]
|
source["document"], source["metadata"]
|
||||||
):
|
):
|
||||||
|
@ -1228,6 +1410,10 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print("Final form_data:", form_data)
|
||||||
|
print("Final metadata:", metadata)
|
||||||
|
print("Final events:", events)
|
||||||
|
|
||||||
return form_data, metadata, events
|
return form_data, metadata, events
|
||||||
|
|
||||||
|
|
||||||
|
@ -2436,7 +2622,9 @@ async def process_chat_response(
|
||||||
|
|
||||||
print("tool_call", tool_call)
|
print("tool_call", tool_call)
|
||||||
tool_call_id = tool_call.get("id", "")
|
tool_call_id = tool_call.get("id", "")
|
||||||
tool_name = tool_call.get("function", {}).get("name", "")
|
tool_function_name = tool_call.get("function", {}).get(
|
||||||
|
"name", ""
|
||||||
|
)
|
||||||
tool_args = tool_call.get("function", {}).get("arguments", "{}")
|
tool_args = tool_call.get("function", {}).get("arguments", "{}")
|
||||||
|
|
||||||
tool_function_params = {}
|
tool_function_params = {}
|
||||||
|
@ -2466,11 +2654,17 @@ async def process_chat_response(
|
||||||
)
|
)
|
||||||
|
|
||||||
tool_result = None
|
tool_result = None
|
||||||
|
tool = None
|
||||||
|
tool_type = None
|
||||||
|
direct_tool = False
|
||||||
|
|
||||||
if tool_name in tools:
|
if tool_function_name in tools:
|
||||||
tool = tools[tool_name]
|
tool = tools[tool_function_name]
|
||||||
spec = tool.get("spec", {})
|
spec = tool.get("spec", {})
|
||||||
|
|
||||||
|
tool_type = tool.get("type", "")
|
||||||
|
direct_tool = tool.get("direct", False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
allowed_params = (
|
allowed_params = (
|
||||||
spec.get("parameters", {})
|
spec.get("parameters", {})
|
||||||
|
@ -2484,13 +2678,13 @@ async def process_chat_response(
|
||||||
if k in allowed_params
|
if k in allowed_params
|
||||||
}
|
}
|
||||||
|
|
||||||
if tool.get("direct", False):
|
if direct_tool:
|
||||||
tool_result = await event_caller(
|
tool_result = await event_caller(
|
||||||
{
|
{
|
||||||
"type": "execute:tool",
|
"type": "execute:tool",
|
||||||
"data": {
|
"data": {
|
||||||
"id": str(uuid4()),
|
"id": str(uuid4()),
|
||||||
"name": tool_name,
|
"name": tool_function_name,
|
||||||
"params": tool_function_params,
|
"params": tool_function_params,
|
||||||
"server": tool.get("server", {}),
|
"server": tool.get("server", {}),
|
||||||
"session_id": metadata.get(
|
"session_id": metadata.get(
|
||||||
|
@ -2509,151 +2703,17 @@ async def process_chat_response(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tool_result = str(e)
|
tool_result = str(e)
|
||||||
|
|
||||||
tool_result_embeds = []
|
tool_result, tool_result_files, tool_result_embeds = (
|
||||||
if isinstance(tool_result, HTMLResponse):
|
process_tool_result(
|
||||||
content_disposition = tool_result.headers.get(
|
request,
|
||||||
"Content-Disposition", ""
|
tool_function_name,
|
||||||
)
|
tool_result,
|
||||||
if "inline" in content_disposition:
|
tool_type,
|
||||||
content = tool_result.body.decode("utf-8")
|
direct_tool,
|
||||||
tool_result_embeds.append(content)
|
metadata,
|
||||||
|
user,
|
||||||
if 200 <= tool_result.status_code < 300:
|
|
||||||
tool_result = {
|
|
||||||
"status": "success",
|
|
||||||
"code": "ui_component",
|
|
||||||
"message": "Embedded UI result is active and visible to the user.",
|
|
||||||
}
|
|
||||||
elif 400 <= tool_result.status_code < 500:
|
|
||||||
tool_result = {
|
|
||||||
"status": "error",
|
|
||||||
"code": "ui_component",
|
|
||||||
"message": f"Client error {tool_result.status_code} from embedded UI result.",
|
|
||||||
}
|
|
||||||
elif 500 <= tool_result.status_code < 600:
|
|
||||||
tool_result = {
|
|
||||||
"status": "error",
|
|
||||||
"code": "ui_component",
|
|
||||||
"message": f"Server error {tool_result.status_code} from embedded UI result.",
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
tool_result = {
|
|
||||||
"status": "error",
|
|
||||||
"code": "ui_component",
|
|
||||||
"message": f"Unexpected status code {tool_result.status_code} from embedded UI result.",
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
tool_result = tool_result.body.decode("utf-8")
|
|
||||||
|
|
||||||
elif (
|
|
||||||
tool.get("type") == "external"
|
|
||||||
and isinstance(tool_result, tuple)
|
|
||||||
) or (
|
|
||||||
tool.get("direct", True)
|
|
||||||
and isinstance(tool_result, list)
|
|
||||||
and len(tool_result) == 2
|
|
||||||
):
|
|
||||||
tool_result, tool_response_headers = tool_result
|
|
||||||
|
|
||||||
if tool_response_headers:
|
|
||||||
content_disposition = tool_response_headers.get(
|
|
||||||
"Content-Disposition",
|
|
||||||
tool_response_headers.get(
|
|
||||||
"content-disposition", ""
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if "inline" in content_disposition:
|
|
||||||
content_type = tool_response_headers.get(
|
|
||||||
"Content-Type",
|
|
||||||
tool_response_headers.get("content-type", ""),
|
|
||||||
)
|
|
||||||
location = tool_response_headers.get(
|
|
||||||
"Location",
|
|
||||||
tool_response_headers.get("location", ""),
|
|
||||||
)
|
|
||||||
|
|
||||||
if "text/html" in content_type:
|
|
||||||
# Display as iframe embed
|
|
||||||
tool_result_embeds.append(tool_result)
|
|
||||||
tool_result = {
|
|
||||||
"status": "success",
|
|
||||||
"code": "ui_component",
|
|
||||||
"message": "Embedded UI result is active and visible to the user.",
|
|
||||||
}
|
|
||||||
elif location:
|
|
||||||
tool_result_embeds.append(location)
|
|
||||||
tool_result = {
|
|
||||||
"status": "success",
|
|
||||||
"code": "ui_component",
|
|
||||||
"message": "Embedded UI result is active and visible to the user.",
|
|
||||||
}
|
|
||||||
|
|
||||||
tool_result_files = []
|
|
||||||
if isinstance(tool_result, list):
|
|
||||||
for item in tool_result:
|
|
||||||
# check if string
|
|
||||||
if isinstance(item, str) and item.startswith("data:"):
|
|
||||||
tool_result_files.append(
|
|
||||||
{
|
|
||||||
"type": "data",
|
|
||||||
"content": item,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
tool_result.remove(item)
|
|
||||||
|
|
||||||
if tool.get("type") == "mcp":
|
|
||||||
if isinstance(item, dict):
|
|
||||||
if (
|
|
||||||
item.get("type") == "image"
|
|
||||||
or item.get("type") == "audio"
|
|
||||||
):
|
|
||||||
file_url = get_file_url_from_base64(
|
|
||||||
request,
|
|
||||||
f"data:{item.get('mimeType')};base64,{item.get('data', item.get('blob', ''))}",
|
|
||||||
{
|
|
||||||
"chat_id": metadata.get(
|
|
||||||
"chat_id", None
|
|
||||||
),
|
|
||||||
"message_id": metadata.get(
|
|
||||||
"message_id", None
|
|
||||||
),
|
|
||||||
"session_id": metadata.get(
|
|
||||||
"session_id", None
|
|
||||||
),
|
|
||||||
"result": item,
|
|
||||||
},
|
|
||||||
user,
|
|
||||||
)
|
|
||||||
|
|
||||||
tool_result_files.append(
|
|
||||||
{
|
|
||||||
"type": item.get("type", "data"),
|
|
||||||
"url": file_url,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
tool_result.remove(item)
|
|
||||||
|
|
||||||
if tool_result_files:
|
|
||||||
if not isinstance(tool_result, list):
|
|
||||||
tool_result = [
|
|
||||||
tool_result,
|
|
||||||
]
|
|
||||||
|
|
||||||
for file in tool_result_files:
|
|
||||||
tool_result.append(
|
|
||||||
{
|
|
||||||
"type": file.get("type", "data"),
|
|
||||||
"content": "Result is being displayed as a file.",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(tool_result, dict) or isinstance(
|
|
||||||
tool_result, list
|
|
||||||
):
|
|
||||||
tool_result = json.dumps(
|
|
||||||
tool_result, indent=2, ensure_ascii=False
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
|
@ -2673,7 +2733,6 @@ async def process_chat_response(
|
||||||
)
|
)
|
||||||
|
|
||||||
content_blocks[-1]["results"] = results
|
content_blocks[-1]["results"] = results
|
||||||
|
|
||||||
content_blocks.append(
|
content_blocks.append(
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|
|
@ -391,17 +391,10 @@ def parse_ollama_modelfile(model_text):
|
||||||
"top_k": int,
|
"top_k": int,
|
||||||
"top_p": float,
|
"top_p": float,
|
||||||
"num_keep": int,
|
"num_keep": int,
|
||||||
"typical_p": float,
|
|
||||||
"presence_penalty": float,
|
"presence_penalty": float,
|
||||||
"frequency_penalty": float,
|
"frequency_penalty": float,
|
||||||
"penalize_newline": bool,
|
|
||||||
"numa": bool,
|
|
||||||
"num_batch": int,
|
"num_batch": int,
|
||||||
"num_gpu": int,
|
"num_gpu": int,
|
||||||
"main_gpu": int,
|
|
||||||
"low_vram": bool,
|
|
||||||
"f16_kv": bool,
|
|
||||||
"vocab_only": bool,
|
|
||||||
"use_mmap": bool,
|
"use_mmap": bool,
|
||||||
"use_mlock": bool,
|
"use_mlock": bool,
|
||||||
"num_thread": int,
|
"num_thread": int,
|
||||||
|
|
|
@ -263,6 +263,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||||
"icon": function.meta.manifest.get("icon_url", None)
|
"icon": function.meta.manifest.get("icon_url", None)
|
||||||
or getattr(module, "icon_url", None)
|
or getattr(module, "icon_url", None)
|
||||||
or getattr(module, "icon", None),
|
or getattr(module, "icon", None),
|
||||||
|
"has_user_valves": hasattr(module, "UserValves"),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -198,13 +198,25 @@ def get_parsed_and_base_url(server_url) -> tuple[urllib.parse.ParseResult, str]:
|
||||||
|
|
||||||
|
|
||||||
def get_discovery_urls(server_url) -> list[str]:
|
def get_discovery_urls(server_url) -> list[str]:
|
||||||
urls = []
|
|
||||||
parsed, base_url = get_parsed_and_base_url(server_url)
|
parsed, base_url = get_parsed_and_base_url(server_url)
|
||||||
|
|
||||||
urls.append(
|
urls = [
|
||||||
urllib.parse.urljoin(base_url, "/.well-known/oauth-authorization-server")
|
urllib.parse.urljoin(base_url, "/.well-known/oauth-authorization-server"),
|
||||||
)
|
urllib.parse.urljoin(base_url, "/.well-known/openid-configuration"),
|
||||||
urls.append(urllib.parse.urljoin(base_url, "/.well-known/openid-configuration"))
|
]
|
||||||
|
|
||||||
|
if parsed.path and parsed.path != "/":
|
||||||
|
urls.append(
|
||||||
|
urllib.parse.urljoin(
|
||||||
|
base_url,
|
||||||
|
f"/.well-known/oauth-authorization-server{parsed.path.rstrip('/')}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
urls.append(
|
||||||
|
urllib.parse.urljoin(
|
||||||
|
base_url, f"/.well-known/openid-configuration{parsed.path.rstrip('/')}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return urls
|
return urls
|
||||||
|
|
||||||
|
|
|
@ -153,17 +153,11 @@ def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
|
||||||
"repeat_last_n": int,
|
"repeat_last_n": int,
|
||||||
"top_k": int,
|
"top_k": int,
|
||||||
"min_p": float,
|
"min_p": float,
|
||||||
"typical_p": float,
|
|
||||||
"repeat_penalty": float,
|
"repeat_penalty": float,
|
||||||
"presence_penalty": float,
|
"presence_penalty": float,
|
||||||
"frequency_penalty": float,
|
"frequency_penalty": float,
|
||||||
"penalize_newline": bool,
|
|
||||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||||
"numa": bool,
|
|
||||||
"num_gpu": int,
|
"num_gpu": int,
|
||||||
"main_gpu": int,
|
|
||||||
"low_vram": bool,
|
|
||||||
"vocab_only": bool,
|
|
||||||
"use_mmap": bool,
|
"use_mmap": bool,
|
||||||
"use_mlock": bool,
|
"use_mlock": bool,
|
||||||
"num_thread": int,
|
"num_thread": int,
|
||||||
|
|
|
@ -166,6 +166,48 @@ def load_function_module_by_id(function_id: str, content: str | None = None):
|
||||||
os.unlink(temp_file.name)
|
os.unlink(temp_file.name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_module_from_cache(request, tool_id, load_from_db=True):
|
||||||
|
if load_from_db:
|
||||||
|
# Always load from the database by default
|
||||||
|
tool = Tools.get_tool_by_id(tool_id)
|
||||||
|
if not tool:
|
||||||
|
raise Exception(f"Tool not found: {tool_id}")
|
||||||
|
content = tool.content
|
||||||
|
|
||||||
|
new_content = replace_imports(content)
|
||||||
|
if new_content != content:
|
||||||
|
content = new_content
|
||||||
|
# Update the tool content in the database
|
||||||
|
Tools.update_tool_by_id(tool_id, {"content": content})
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasattr(request.app.state, "TOOL_CONTENTS")
|
||||||
|
and tool_id in request.app.state.TOOL_CONTENTS
|
||||||
|
) and (
|
||||||
|
hasattr(request.app.state, "TOOLS") and tool_id in request.app.state.TOOLS
|
||||||
|
):
|
||||||
|
if request.app.state.TOOL_CONTENTS[tool_id] == content:
|
||||||
|
return request.app.state.TOOLS[tool_id], None
|
||||||
|
|
||||||
|
tool_module, frontmatter = load_tool_module_by_id(tool_id, content)
|
||||||
|
else:
|
||||||
|
if hasattr(request.app.state, "TOOLS") and tool_id in request.app.state.TOOLS:
|
||||||
|
return request.app.state.TOOLS[tool_id], None
|
||||||
|
|
||||||
|
tool_module, frontmatter = load_tool_module_by_id(tool_id)
|
||||||
|
|
||||||
|
if not hasattr(request.app.state, "TOOLS"):
|
||||||
|
request.app.state.TOOLS = {}
|
||||||
|
|
||||||
|
if not hasattr(request.app.state, "TOOL_CONTENTS"):
|
||||||
|
request.app.state.TOOL_CONTENTS = {}
|
||||||
|
|
||||||
|
request.app.state.TOOLS[tool_id] = tool_module
|
||||||
|
request.app.state.TOOL_CONTENTS[tool_id] = content
|
||||||
|
|
||||||
|
return tool_module, frontmatter
|
||||||
|
|
||||||
|
|
||||||
def get_function_module_from_cache(request, function_id, load_from_db=True):
|
def get_function_module_from_cache(request, function_id, load_from_db=True):
|
||||||
if load_from_db:
|
if load_from_db:
|
||||||
# Always load from the database by default
|
# Always load from the database by default
|
||||||
|
|
|
@ -588,28 +588,20 @@ async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]:
|
||||||
error = str(err)
|
error = str(err)
|
||||||
raise Exception(error)
|
raise Exception(error)
|
||||||
|
|
||||||
data = {
|
log.debug(f"Fetched data: {res}")
|
||||||
"openapi": res,
|
return res
|
||||||
"info": res.get("info", {}),
|
|
||||||
"specs": convert_openapi_to_tool_payload(res),
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(f"Fetched data: {data}")
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
# Prepare list of enabled servers along with their original index
|
# Prepare list of enabled servers along with their original index
|
||||||
|
|
||||||
|
tasks = []
|
||||||
server_entries = []
|
server_entries = []
|
||||||
for idx, server in enumerate(servers):
|
for idx, server in enumerate(servers):
|
||||||
if (
|
if (
|
||||||
server.get("config", {}).get("enable")
|
server.get("config", {}).get("enable")
|
||||||
and server.get("type", "openapi") == "openapi"
|
and server.get("type", "openapi") == "openapi"
|
||||||
):
|
):
|
||||||
# Path (to OpenAPI spec URL) can be either a full URL or a path to append to the base URL
|
|
||||||
openapi_path = server.get("path", "openapi.json")
|
|
||||||
full_url = get_tool_server_url(server.get("url"), openapi_path)
|
|
||||||
|
|
||||||
info = server.get("info", {})
|
info = server.get("info", {})
|
||||||
|
|
||||||
auth_type = server.get("auth_type", "bearer")
|
auth_type = server.get("auth_type", "bearer")
|
||||||
|
@ -625,12 +617,34 @@ async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str,
|
||||||
if not id:
|
if not id:
|
||||||
id = str(idx)
|
id = str(idx)
|
||||||
|
|
||||||
server_entries.append((id, idx, server, full_url, info, token))
|
server_url = server.get("url")
|
||||||
|
spec_type = server.get("spec_type", "url")
|
||||||
|
|
||||||
# Create async tasks to fetch data
|
# Create async tasks to fetch data
|
||||||
tasks = [
|
task = None
|
||||||
get_tool_server_data(token, url) for (_, _, _, url, _, token) in server_entries
|
if spec_type == "url":
|
||||||
]
|
# Path (to OpenAPI spec URL) can be either a full URL or a path to append to the base URL
|
||||||
|
openapi_path = server.get("path", "openapi.json")
|
||||||
|
spec_url = get_tool_server_url(server_url, openapi_path)
|
||||||
|
# Fetch from URL
|
||||||
|
task = get_tool_server_data(token, spec_url)
|
||||||
|
elif spec_type == "json" and server.get("spec", ""):
|
||||||
|
# Use provided JSON spec
|
||||||
|
spec_json = None
|
||||||
|
try:
|
||||||
|
spec_json = json.loads(server.get("spec", ""))
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error parsing JSON spec for tool server {id}: {e}")
|
||||||
|
|
||||||
|
if spec_json:
|
||||||
|
task = asyncio.sleep(
|
||||||
|
0,
|
||||||
|
result=spec_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
if task:
|
||||||
|
tasks.append(task)
|
||||||
|
server_entries.append((id, idx, server, server_url, info, token))
|
||||||
|
|
||||||
# Execute tasks concurrently
|
# Execute tasks concurrently
|
||||||
responses = await asyncio.gather(*tasks, return_exceptions=True)
|
responses = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
@ -642,8 +656,13 @@ async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str,
|
||||||
log.error(f"Failed to connect to {url} OpenAPI tool server")
|
log.error(f"Failed to connect to {url} OpenAPI tool server")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
openapi_data = response.get("openapi", {})
|
response = {
|
||||||
|
"openapi": response,
|
||||||
|
"info": response.get("info", {}),
|
||||||
|
"specs": convert_openapi_to_tool_payload(response),
|
||||||
|
}
|
||||||
|
|
||||||
|
openapi_data = response.get("openapi", {})
|
||||||
if info and isinstance(openapi_data, dict):
|
if info and isinstance(openapi_data, dict):
|
||||||
openapi_data["info"] = openapi_data.get("info", {})
|
openapi_data["info"] = openapi_data.get("info", {})
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.31",
|
"version": "0.6.32",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.31",
|
"version": "0.6.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^4.5.0",
|
"@azure/msal-browser": "^4.5.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.31",
|
"version": "0.6.32",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
|
|
|
@ -248,6 +248,7 @@ export const getChannelThreadMessages = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
type MessageForm = {
|
type MessageForm = {
|
||||||
|
reply_to_id?: string;
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
content: string;
|
content: string;
|
||||||
data?: object;
|
data?: object;
|
||||||
|
|
|
@ -33,6 +33,38 @@ export const createNewChat = async (token: string, chat: object, folderId: strin
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const unarchiveAllChats = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/unarchive/all`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { 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 importChat = async (
|
export const importChat = async (
|
||||||
token: string,
|
token: string,
|
||||||
chat: object,
|
chat: object,
|
||||||
|
@ -327,6 +359,45 @@ export const getChatsByFolderId = async (token: string, folderId: string) => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getChatListByFolderId = async (token: string, folderId: string, page: number = 1) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (page !== null) {
|
||||||
|
searchParams.append('page', `${page}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${WEBUI_API_BASE_URL}/chats/folder/${folderId}/list?${searchParams.toString()}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAllArchivedChats = async (token: string) => {
|
export const getAllArchivedChats = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const uploadFile = async (token: string, file: File, metadata?: object |
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
error = err.detail;
|
error = err.detail || err.message;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
|
@ -337,14 +337,8 @@ export const getToolServerData = async (token: string, url: string) => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
console.log(res);
|
||||||
openapi: res,
|
return res;
|
||||||
info: res.info,
|
|
||||||
specs: convertOpenApiToToolPayload(res)
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getToolServersData = async (servers: object[]) => {
|
export const getToolServersData = async (servers: object[]) => {
|
||||||
|
@ -356,6 +350,7 @@ export const getToolServersData = async (servers: object[]) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
let toolServerToken = null;
|
let toolServerToken = null;
|
||||||
|
|
||||||
const auth_type = server?.auth_type ?? 'bearer';
|
const auth_type = server?.auth_type ?? 'bearer';
|
||||||
if (auth_type === 'bearer') {
|
if (auth_type === 'bearer') {
|
||||||
toolServerToken = server?.key;
|
toolServerToken = server?.key;
|
||||||
|
@ -365,18 +360,34 @@ export const getToolServersData = async (servers: object[]) => {
|
||||||
toolServerToken = localStorage.token;
|
toolServerToken = localStorage.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await getToolServerData(
|
let res = null;
|
||||||
toolServerToken,
|
const specType = server?.spec_type ?? 'url';
|
||||||
(server?.path ?? '').includes('://')
|
|
||||||
? server?.path
|
if (specType === 'url') {
|
||||||
: `${server?.url}${(server?.path ?? '').startsWith('/') ? '' : '/'}${server?.path}`
|
res = await getToolServerData(
|
||||||
).catch((err) => {
|
toolServerToken,
|
||||||
error = err;
|
(server?.path ?? '').includes('://')
|
||||||
return null;
|
? server?.path
|
||||||
});
|
: `${server?.url}${(server?.path ?? '').startsWith('/') ? '' : '/'}${server?.path}`
|
||||||
|
).catch((err) => {
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
} else if ((specType === 'json' && server?.spec) ?? null) {
|
||||||
|
try {
|
||||||
|
res = JSON.parse(server?.spec);
|
||||||
|
} catch (e) {
|
||||||
|
error = 'Failed to parse JSON spec';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
const { openapi, info, specs } = {
|
||||||
|
openapi: res,
|
||||||
|
info: res.info,
|
||||||
|
specs: convertOpenApiToToolPayload(res)
|
||||||
|
};
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const { openapi, info, specs } = data;
|
|
||||||
return {
|
return {
|
||||||
url: server?.url,
|
url: server?.url,
|
||||||
openapi: openapi,
|
openapi: openapi,
|
||||||
|
|
|
@ -31,6 +31,34 @@ export const getModels = async (token: string = '') => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const importModels = async (token: string, models: object[]) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/models/import`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ models: models })
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getBaseModels = async (token: string = '') => {
|
export const getBaseModels = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import fileSaver from 'file-saver';
|
||||||
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
@ -27,11 +32,16 @@
|
||||||
export let direct = false;
|
export let direct = false;
|
||||||
export let connection = null;
|
export let connection = null;
|
||||||
|
|
||||||
let url = '';
|
let inputElement = null;
|
||||||
let path = 'openapi.json';
|
|
||||||
|
|
||||||
let type = 'openapi'; // 'openapi', 'mcp'
|
let type = 'openapi'; // 'openapi', 'mcp'
|
||||||
|
|
||||||
|
let url = '';
|
||||||
|
|
||||||
|
let spec_type = 'url'; // 'url', 'json'
|
||||||
|
let spec = ''; // used when spec_type is 'json'
|
||||||
|
let path = 'openapi.json';
|
||||||
|
|
||||||
let auth_type = 'bearer';
|
let auth_type = 'bearer';
|
||||||
let key = '';
|
let key = '';
|
||||||
|
|
||||||
|
@ -132,6 +142,84 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const importHandler = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const json = event.target.result;
|
||||||
|
console.log('importHandler', json);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = JSON.parse(json);
|
||||||
|
// validate data
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
if (data.length === 0) {
|
||||||
|
toast.error($i18n.t('Please select a valid JSON file'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data = data[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type) type = data.type;
|
||||||
|
if (data.url) url = data.url;
|
||||||
|
|
||||||
|
if (data.spec_type) spec_type = data.spec_type;
|
||||||
|
if (data.spec) spec = data.spec;
|
||||||
|
if (data.path) path = data.path;
|
||||||
|
|
||||||
|
if (data.auth_type) auth_type = data.auth_type;
|
||||||
|
if (data.key) key = data.key;
|
||||||
|
|
||||||
|
if (data.info) {
|
||||||
|
id = data.info.id ?? '';
|
||||||
|
name = data.info.name ?? '';
|
||||||
|
description = data.info.description ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.config) {
|
||||||
|
enable = data.config.enable ?? true;
|
||||||
|
accessControl = data.config.access_control ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success($i18n.t('Import successful'));
|
||||||
|
} catch (error) {
|
||||||
|
toast.error($i18n.t('Please select a valid JSON file'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportHandler = async () => {
|
||||||
|
// export current connection as json file
|
||||||
|
const json = JSON.stringify([
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
url,
|
||||||
|
|
||||||
|
spec_type,
|
||||||
|
spec,
|
||||||
|
path,
|
||||||
|
|
||||||
|
auth_type,
|
||||||
|
key,
|
||||||
|
|
||||||
|
info: {
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
description: description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const blob = new Blob([json], {
|
||||||
|
type: 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
saveAs(blob, `tool-server-${id || name || 'export'}.json`);
|
||||||
|
};
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
|
@ -149,10 +237,26 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate spec
|
||||||
|
if (spec_type === 'json') {
|
||||||
|
try {
|
||||||
|
const specJSON = JSON.parse(spec);
|
||||||
|
spec = JSON.stringify(specJSON, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error($i18n.t('Please enter a valid JSON spec'));
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const connection = {
|
const connection = {
|
||||||
url,
|
|
||||||
path,
|
|
||||||
type,
|
type,
|
||||||
|
url,
|
||||||
|
|
||||||
|
spec_type,
|
||||||
|
spec,
|
||||||
|
path,
|
||||||
|
|
||||||
auth_type,
|
auth_type,
|
||||||
key,
|
key,
|
||||||
config: {
|
config: {
|
||||||
|
@ -173,9 +277,12 @@
|
||||||
show = false;
|
show = false;
|
||||||
|
|
||||||
// reset form
|
// reset form
|
||||||
url = '';
|
|
||||||
path = 'openapi.json';
|
|
||||||
type = 'openapi';
|
type = 'openapi';
|
||||||
|
url = '';
|
||||||
|
|
||||||
|
spec_type = 'url';
|
||||||
|
spec = '';
|
||||||
|
path = 'openapi.json';
|
||||||
|
|
||||||
key = '';
|
key = '';
|
||||||
auth_type = 'bearer';
|
auth_type = 'bearer';
|
||||||
|
@ -191,10 +298,13 @@
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
|
type = connection?.type ?? 'openapi';
|
||||||
url = connection.url;
|
url = connection.url;
|
||||||
|
|
||||||
|
spec_type = connection?.spec_type ?? 'url';
|
||||||
|
spec = connection?.spec ?? '';
|
||||||
path = connection?.path ?? 'openapi.json';
|
path = connection?.path ?? 'openapi.json';
|
||||||
|
|
||||||
type = connection?.type ?? 'openapi';
|
|
||||||
auth_type = connection?.auth_type ?? 'bearer';
|
auth_type = connection?.auth_type ?? 'bearer';
|
||||||
key = connection?.key ?? '';
|
key = connection?.key ?? '';
|
||||||
|
|
||||||
|
@ -227,19 +337,47 @@
|
||||||
{$i18n.t('Add Connection')}
|
{$i18n.t('Add Connection')}
|
||||||
{/if}
|
{/if}
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
|
||||||
class="self-center"
|
<div class="flex items-center gap-3">
|
||||||
aria-label={$i18n.t('Close Configure Connection Modal')}
|
<div class="flex gap-1.5 text-xs justify-end">
|
||||||
on:click={() => {
|
<button
|
||||||
show = false;
|
class=" hover:underline"
|
||||||
}}
|
type="button"
|
||||||
>
|
on:click={() => {
|
||||||
<XMark className={'size-5'} />
|
inputElement?.click();
|
||||||
</button>
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Import')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class=" hover:underline" type="button" on:click={exportHandler}>
|
||||||
|
{$i18n.t('Export')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
aria-label={$i18n.t('Close Configure Connection Modal')}
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className={'size-5'} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
<input
|
||||||
|
bind:this={inputElement}
|
||||||
|
type="file"
|
||||||
|
hidden
|
||||||
|
accept=".json"
|
||||||
|
on:change={(e) => {
|
||||||
|
importHandler(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="flex flex-col w-full"
|
class="flex flex-col w-full"
|
||||||
on:submit={(e) => {
|
on:submit={(e) => {
|
||||||
|
@ -326,35 +464,81 @@
|
||||||
<Switch bind:state={enable} />
|
<Switch bind:state={enable} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ['', 'openapi'].includes(type)}
|
|
||||||
<div class="flex-1 flex items-center">
|
|
||||||
<label for="url-or-path" class="sr-only"
|
|
||||||
>{$i18n.t('openapi.json URL or Path')}</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
|
||||||
type="text"
|
|
||||||
id="url-or-path"
|
|
||||||
bind:value={path}
|
|
||||||
placeholder={$i18n.t('openapi.json URL or Path')}
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ['', 'openapi'].includes(type)}
|
{#if ['', 'openapi'].includes(type)}
|
||||||
<div
|
<div class="flex gap-2 mt-2">
|
||||||
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
<div class="flex flex-col w-full">
|
||||||
>
|
<div class="flex justify-between items-center mb-0.5">
|
||||||
{$i18n.t(`WebUI will make requests to "{{url}}"`, {
|
<div class="flex gap-2 items-center">
|
||||||
url: path.includes('://')
|
<div
|
||||||
? path
|
for="select-bearer-or-session"
|
||||||
: `${url}${path.startsWith('/') ? '' : '/'}${path}`
|
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
})}
|
>
|
||||||
|
{$i18n.t('OpenAPI Spec')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex-shrink-0 self-start">
|
||||||
|
<select
|
||||||
|
id="select-bearer-or-session"
|
||||||
|
class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||||
|
bind:value={spec_type}
|
||||||
|
>
|
||||||
|
<option value="url">{$i18n.t('URL')}</option>
|
||||||
|
<option value="json">{$i18n.t('JSON')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-1 items-center">
|
||||||
|
{#if spec_type === 'url'}
|
||||||
|
<div class="flex-1 flex items-center">
|
||||||
|
<label for="url-or-path" class="sr-only"
|
||||||
|
>{$i18n.t('openapi.json URL or Path')}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||||
|
type="text"
|
||||||
|
id="url-or-path"
|
||||||
|
bind:value={path}
|
||||||
|
placeholder={$i18n.t('openapi.json URL or Path')}
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else if spec_type === 'json'}
|
||||||
|
<div
|
||||||
|
class={`text-xs w-full self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
<label for="url-or-path" class="sr-only">{$i18n.t('JSON Spec')}</label>
|
||||||
|
<textarea
|
||||||
|
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-black dark:text-white'}`}
|
||||||
|
bind:value={spec}
|
||||||
|
placeholder={$i18n.t('JSON Spec')}
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
rows="5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if ['', 'url'].includes(spec_type)}
|
||||||
|
<div
|
||||||
|
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t(`WebUI will make requests to "{{url}}"`, {
|
||||||
|
url: path.includes('://')
|
||||||
|
? path
|
||||||
|
: `${url}${path.startsWith('/') ? '' : '/'}${path}`
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -566,35 +750,38 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
<div class="flex justify-between pt-3 text-sm font-medium gap-1.5">
|
||||||
{#if edit}
|
<div></div>
|
||||||
<button
|
<div class="flex gap-1.5">
|
||||||
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
|
{#if edit}
|
||||||
type="button"
|
<button
|
||||||
on:click={() => {
|
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
|
||||||
onDelete();
|
type="button"
|
||||||
show = false;
|
on:click={() => {
|
||||||
}}
|
onDelete();
|
||||||
>
|
show = false;
|
||||||
{$i18n.t('Delete')}
|
}}
|
||||||
</button>
|
>
|
||||||
{/if}
|
{$i18n.t('Delete')}
|
||||||
|
</button>
|
||||||
<button
|
|
||||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
|
||||||
? ' cursor-not-allowed'
|
|
||||||
: ''}"
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{$i18n.t('Save')}
|
|
||||||
|
|
||||||
{#if loading}
|
|
||||||
<div class="ml-2 self-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
|
||||||
|
<button
|
||||||
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
||||||
|
? ' cursor-not-allowed'
|
||||||
|
: ''}"
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{$i18n.t('Save')}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="ml-2 self-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -595,7 +595,7 @@
|
||||||
deleteHandler(selectedFunction);
|
deleteHandler(selectedFunction);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" text-sm text-gray-500">
|
<div class=" text-sm text-gray-500 truncate">
|
||||||
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedFunction.name}</span>.
|
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedFunction.name}</span>.
|
||||||
</div>
|
</div>
|
||||||
</DeleteConfirmDialog>
|
</DeleteConfirmDialog>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<div class="shrink-0 line-clamp-1">
|
<div class=" line-clamp-1">
|
||||||
{model.name}
|
{model.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
deleteAllModels,
|
deleteAllModels,
|
||||||
getBaseModels,
|
getBaseModels,
|
||||||
toggleModelById,
|
toggleModelById,
|
||||||
updateModelById
|
updateModelById,
|
||||||
|
importModels
|
||||||
} from '$lib/apis/models';
|
} from '$lib/apis/models';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
|
|
||||||
let shiftKey = false;
|
let shiftKey = false;
|
||||||
|
|
||||||
|
let modelsImportInProgress = false;
|
||||||
let importFiles;
|
let importFiles;
|
||||||
let modelsImportInputElement: HTMLInputElement;
|
let modelsImportInputElement: HTMLInputElement;
|
||||||
|
|
||||||
|
@ -464,47 +466,41 @@
|
||||||
accept=".json"
|
accept=".json"
|
||||||
hidden
|
hidden
|
||||||
on:change={() => {
|
on:change={() => {
|
||||||
console.log(importFiles);
|
if (importFiles.length > 0) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
try {
|
||||||
|
const models = JSON.parse(String(event.target.result));
|
||||||
|
modelsImportInProgress = true;
|
||||||
|
const res = await importModels(localStorage.token, models);
|
||||||
|
modelsImportInProgress = false;
|
||||||
|
|
||||||
let reader = new FileReader();
|
if (res) {
|
||||||
reader.onload = async (event) => {
|
toast.success($i18n.t('Models imported successfully'));
|
||||||
let savedModels = JSON.parse(event.target.result);
|
await init();
|
||||||
console.log(savedModels);
|
} else {
|
||||||
|
toast.error($i18n.t('Failed to import models'));
|
||||||
for (const model of savedModels) {
|
|
||||||
if (Object.keys(model).includes('base_model_id')) {
|
|
||||||
if (model.base_model_id === null) {
|
|
||||||
upsertModelHandler(model);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (model?.info ?? false) {
|
|
||||||
if (model.info.base_model_id === null) {
|
|
||||||
upsertModelHandler(model.info);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.error($i18n.t('Invalid JSON file'));
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
reader.readAsText(importFiles[0]);
|
||||||
await _models.set(
|
}
|
||||||
await getModels(
|
|
||||||
localStorage.token,
|
|
||||||
$config?.features?.enable_direct_connections &&
|
|
||||||
($settings?.directConnections ?? null)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
init();
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsText(importFiles[0]);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||||
|
disabled={modelsImportInProgress}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
modelsImportInputElement.click();
|
modelsImportInputElement.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{#if modelsImportInProgress}
|
||||||
|
<Spinner className="size-3" />
|
||||||
|
{/if}
|
||||||
<div class=" self-center mr-2 font-medium line-clamp-1">
|
<div class=" self-center mr-2 font-medium line-clamp-1">
|
||||||
{$i18n.t('Import Presets')}
|
{$i18n.t('Import Presets')}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,10 +19,9 @@
|
||||||
import Search from '$lib/components/icons/Search.svelte';
|
import Search from '$lib/components/icons/Search.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 './Groups/EditGroupModal.svelte';
|
import EditGroupModal from './Groups/EditGroupModal.svelte';
|
||||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||||
import GroupItem from './Groups/GroupItem.svelte';
|
import GroupItem from './Groups/GroupItem.svelte';
|
||||||
import AddGroupModal from './Groups/AddGroupModal.svelte';
|
|
||||||
import { createNewGroup, getGroups } from '$lib/apis/groups';
|
import { createNewGroup, getGroups } from '$lib/apis/groups';
|
||||||
import {
|
import {
|
||||||
getUserDefaultPermissions,
|
getUserDefaultPermissions,
|
||||||
|
@ -51,54 +50,20 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
let search = '';
|
let search = '';
|
||||||
let defaultPermissions = {
|
let defaultPermissions = {};
|
||||||
workspace: {
|
|
||||||
models: false,
|
|
||||||
knowledge: false,
|
|
||||||
prompts: false,
|
|
||||||
tools: false
|
|
||||||
},
|
|
||||||
sharing: {
|
|
||||||
public_models: false,
|
|
||||||
public_knowledge: false,
|
|
||||||
public_prompts: false,
|
|
||||||
public_tools: false
|
|
||||||
},
|
|
||||||
chat: {
|
|
||||||
controls: true,
|
|
||||||
valves: true,
|
|
||||||
system_prompt: true,
|
|
||||||
params: true,
|
|
||||||
file_upload: true,
|
|
||||||
delete: true,
|
|
||||||
delete_message: true,
|
|
||||||
continue_response: true,
|
|
||||||
regenerate_response: true,
|
|
||||||
rate_response: true,
|
|
||||||
edit: true,
|
|
||||||
share: true,
|
|
||||||
export: true,
|
|
||||||
stt: true,
|
|
||||||
tts: true,
|
|
||||||
call: true,
|
|
||||||
multiple_models: true,
|
|
||||||
temporary: true,
|
|
||||||
temporary_enforced: false
|
|
||||||
},
|
|
||||||
features: {
|
|
||||||
direct_tool_servers: false,
|
|
||||||
web_search: true,
|
|
||||||
image_generation: true,
|
|
||||||
code_interpreter: true,
|
|
||||||
notes: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let showCreateGroupModal = false;
|
let showAddGroupModal = false;
|
||||||
let showDefaultPermissionsModal = false;
|
let showDefaultPermissionsModal = false;
|
||||||
|
|
||||||
const setGroups = async () => {
|
const setGroups = async () => {
|
||||||
groups = await getGroups(localStorage.token);
|
const allGroups = await getGroups(localStorage.token);
|
||||||
|
const userGroup = allGroups.find((g) => g.name.toLowerCase() === 'user');
|
||||||
|
|
||||||
|
if (userGroup) {
|
||||||
|
defaultPermissions = userGroup.permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
groups = allGroups.filter((g) => g.name.toLowerCase() !== 'user');
|
||||||
};
|
};
|
||||||
|
|
||||||
const addGroupHandler = async (group) => {
|
const addGroupHandler = async (group) => {
|
||||||
|
@ -146,14 +111,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
await setGroups();
|
await setGroups();
|
||||||
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
|
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<AddGroupModal bind:show={showCreateGroupModal} onSubmit={addGroupHandler} />
|
<EditGroupModal
|
||||||
|
bind:show={showAddGroupModal}
|
||||||
|
edit={false}
|
||||||
|
permissions={defaultPermissions}
|
||||||
|
onSubmit={addGroupHandler}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||||
{$i18n.t('Groups')}
|
{$i18n.t('Groups')}
|
||||||
|
@ -180,7 +149,7 @@
|
||||||
<button
|
<button
|
||||||
class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
|
class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showCreateGroupModal = !showCreateGroupModal;
|
showAddGroupModal = !showAddGroupModal;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
<Plus className="size-3.5" />
|
||||||
|
@ -207,7 +176,7 @@
|
||||||
class=" px-4 py-1.5 text-sm rounded-full bg-black hover:bg-gray-800 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition font-medium flex items-center space-x-1"
|
class=" px-4 py-1.5 text-sm rounded-full bg-black hover:bg-gray-800 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition font-medium flex items-center space-x-1"
|
||||||
aria-label={$i18n.t('Create Group')}
|
aria-label={$i18n.t('Create Group')}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showCreateGroupModal = true;
|
showAddGroupModal = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Create Group')}
|
{$i18n.t('Create Group')}
|
||||||
|
@ -226,7 +195,7 @@
|
||||||
|
|
||||||
{#each filteredGroups as group}
|
{#each filteredGroups as group}
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<GroupItem {group} {users} {setGroups} />
|
<GroupItem {group} {users} {setGroups} {defaultPermissions} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -234,7 +203,7 @@
|
||||||
|
|
||||||
<hr class="mb-2 border-gray-100 dark:border-gray-850" />
|
<hr class="mb-2 border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<GroupModal
|
<EditGroupModal
|
||||||
bind:show={showDefaultPermissionsModal}
|
bind:show={showDefaultPermissionsModal}
|
||||||
tabs={['permissions']}
|
tabs={['permissions']}
|
||||||
bind:permissions={defaultPermissions}
|
bind:permissions={defaultPermissions}
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
import { getContext, onMount } from 'svelte';
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
|
||||||
export let onSubmit: Function = () => {};
|
|
||||||
export let show = false;
|
|
||||||
|
|
||||||
let name = '';
|
|
||||||
let description = '';
|
|
||||||
let userIds = [];
|
|
||||||
|
|
||||||
let loading = false;
|
|
||||||
|
|
||||||
const submitHandler = async () => {
|
|
||||||
loading = true;
|
|
||||||
|
|
||||||
const group = {
|
|
||||||
name,
|
|
||||||
description
|
|
||||||
};
|
|
||||||
|
|
||||||
await onSubmit(group);
|
|
||||||
|
|
||||||
loading = false;
|
|
||||||
show = false;
|
|
||||||
|
|
||||||
name = '';
|
|
||||||
description = '';
|
|
||||||
userIds = [];
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal size="sm" bind:show>
|
|
||||||
<div>
|
|
||||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
|
||||||
<div class=" text-lg font-medium self-center font-primary">
|
|
||||||
{$i18n.t('Add User Group')}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="self-center"
|
|
||||||
on:click={() => {
|
|
||||||
show = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XMark className={'size-5'} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
|
||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
|
||||||
<form
|
|
||||||
class="flex flex-col w-full"
|
|
||||||
on:submit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
submitHandler();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="px-1 flex flex-col w-full">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<input
|
|
||||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
|
||||||
type="text"
|
|
||||||
bind:value={name}
|
|
||||||
placeholder={$i18n.t('Group Name')}
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full mt-2">
|
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Description')}</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<Textarea
|
|
||||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden resize-none"
|
|
||||||
rows={2}
|
|
||||||
bind:value={description}
|
|
||||||
placeholder={$i18n.t('Group Description')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
|
||||||
<button
|
|
||||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
|
||||||
? ' cursor-not-allowed'
|
|
||||||
: ''}"
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{$i18n.t('Create')}
|
|
||||||
|
|
||||||
{#if loading}
|
|
||||||
<div class="ml-2 self-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
export let users = [];
|
export let users = [];
|
||||||
export let group = null;
|
export let group = null;
|
||||||
|
export let defaultPermissions = {};
|
||||||
|
|
||||||
export let custom = true;
|
export let custom = true;
|
||||||
|
|
||||||
|
@ -230,7 +231,7 @@
|
||||||
{#if selectedTab == 'general'}
|
{#if selectedTab == 'general'}
|
||||||
<Display bind:name bind:description />
|
<Display bind:name bind:description />
|
||||||
{:else if selectedTab == 'permissions'}
|
{:else if selectedTab == 'permissions'}
|
||||||
<Permissions bind:permissions />
|
<Permissions bind:permissions {defaultPermissions} />
|
||||||
{:else if selectedTab == 'users'}
|
{:else if selectedTab == 'users'}
|
||||||
<Users bind:userIds {users} />
|
<Users bind:userIds {users} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
name: 'Admins',
|
name: 'Admins',
|
||||||
user_ids: [1, 2, 3]
|
user_ids: [1, 2, 3]
|
||||||
};
|
};
|
||||||
|
export let defaultPermissions = {};
|
||||||
|
|
||||||
export let setGroups = () => {};
|
export let setGroups = () => {};
|
||||||
|
|
||||||
|
@ -59,6 +60,7 @@
|
||||||
edit
|
edit
|
||||||
{users}
|
{users}
|
||||||
{group}
|
{group}
|
||||||
|
{defaultPermissions}
|
||||||
onSubmit={updateHandler}
|
onSubmit={updateHandler}
|
||||||
onDelete={deleteHandler}
|
onDelete={deleteHandler}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
// Default values for permissions
|
// Default values for permissions
|
||||||
const defaultPermissions = {
|
const DEFAULT_PERMISSIONS = {
|
||||||
workspace: {
|
workspace: {
|
||||||
models: false,
|
models: false,
|
||||||
knowledge: false,
|
knowledge: false,
|
||||||
|
@ -17,7 +17,8 @@
|
||||||
public_models: false,
|
public_models: false,
|
||||||
public_knowledge: false,
|
public_knowledge: false,
|
||||||
public_prompts: false,
|
public_prompts: false,
|
||||||
public_tools: false
|
public_tools: false,
|
||||||
|
public_notes: false
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
controls: true,
|
controls: true,
|
||||||
|
@ -50,10 +51,11 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
export let permissions = {};
|
export let permissions = {};
|
||||||
|
export let defaultPermissions = {};
|
||||||
|
|
||||||
// Reactive statement to ensure all fields are present in `permissions`
|
// Reactive statement to ensure all fields are present in `permissions`
|
||||||
$: {
|
$: {
|
||||||
permissions = fillMissingProperties(permissions, defaultPermissions);
|
permissions = fillMissingProperties(permissions, DEFAULT_PERMISSIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillMissingProperties(obj: any, defaults: any) {
|
function fillMissingProperties(obj: any, defaults: any) {
|
||||||
|
@ -68,140 +70,70 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
permissions = fillMissingProperties(permissions, defaultPermissions);
|
permissions = fillMissingProperties(permissions, DEFAULT_PERMISSIONS);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div class="space-y-2">
|
||||||
<!-- <div>
|
<!-- {$i18n.t('Default Model')}
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Model Permissions')}</div>
|
{$i18n.t('Model Filtering')}
|
||||||
|
{$i18n.t('Model Permissions')}
|
||||||
<div class="mb-2">
|
{$i18n.t('No model IDs')} -->
|
||||||
<div class="flex justify-between items-center text-xs pr-2">
|
|
||||||
<div class=" text-xs font-medium">{$i18n.t('Model Filtering')}</div>
|
|
||||||
|
|
||||||
<Switch bind:state={permissions.model.filter} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if permissions.model.filter}
|
|
||||||
<div class="mb-2">
|
|
||||||
<div class=" space-y-1.5">
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<div class="mb-1 flex justify-between">
|
|
||||||
<div class="text-xs text-gray-500">{$i18n.t('Model IDs')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if model_ids.length > 0}
|
|
||||||
<div class="flex flex-col">
|
|
||||||
{#each model_ids as modelId, modelIdx}
|
|
||||||
<div class=" flex gap-2 w-full justify-between items-center">
|
|
||||||
<div class=" text-sm flex-1 rounded-lg">
|
|
||||||
{modelId}
|
|
||||||
</div>
|
|
||||||
<div class="shrink-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
model_ids = model_ids.filter((_, idx) => idx !== modelIdx);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Minus strokeWidth="2" className="size-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="text-gray-500 text-xs text-center py-2 px-10">
|
|
||||||
{$i18n.t('No model IDs')}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-700/10 mt-2.5 mb-1 w-full" />
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<select
|
|
||||||
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
|
|
||||||
? ''
|
|
||||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
|
||||||
bind:value={selectedModelId}
|
|
||||||
>
|
|
||||||
<option value="">{$i18n.t('Select a model')}</option>
|
|
||||||
{#each $models.filter((m) => m?.owned_by !== 'arena') as model}
|
|
||||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
if (selectedModelId && !permissions.model.model_ids.includes(selectedModelId)) {
|
|
||||||
permissions.model.model_ids = [...permissions.model.model_ids, selectedModelId];
|
|
||||||
selectedModelId = '';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="size-3.5" strokeWidth="2" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class=" space-y-1 mb-3">
|
|
||||||
<div class="">
|
|
||||||
<div class="flex justify-between items-center text-xs">
|
|
||||||
<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 mr-2">
|
|
||||||
<select
|
|
||||||
class="w-full bg-transparent outline-hidden py-0.5 text-sm"
|
|
||||||
bind:value={permissions.model.default_id}
|
|
||||||
placeholder={$i18n.t('Select a model')}
|
|
||||||
>
|
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
|
||||||
{#each permissions.model.filter ? $models.filter( (model) => filterModelIds.includes(model.id) ) : $models.filter((model) => model.id) as model}
|
|
||||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" /> -->
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Models Access')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Models Access')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.workspace.models} />
|
||||||
</div>
|
</div>
|
||||||
<Switch bind:state={permissions.workspace.models} />
|
{#if defaultPermissions?.workspace?.models && !permissions.workspace.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>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Knowledge Access')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Knowledge Access')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.workspace.knowledge} />
|
||||||
</div>
|
</div>
|
||||||
<Switch bind:state={permissions.workspace.knowledge} />
|
{#if defaultPermissions?.workspace?.knowledge && !permissions.workspace.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>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Prompts Access')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Prompts Access')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.workspace.prompts} />
|
||||||
</div>
|
</div>
|
||||||
<Switch bind:state={permissions.workspace.prompts} />
|
{#if defaultPermissions?.workspace?.prompts && !permissions.workspace.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>
|
</div>
|
||||||
|
|
||||||
<div class=" ">
|
<div class="flex flex-col w-full">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className=" flex w-full justify-between my-2 pr-2"
|
className="flex w-full justify-between my-1"
|
||||||
content={$i18n.t(
|
content={$i18n.t(
|
||||||
'Warning: Enabling this will allow users to upload arbitrary code on the server.'
|
'Warning: Enabling this will allow users to upload arbitrary code on the server.'
|
||||||
)}
|
)}
|
||||||
|
@ -212,247 +144,499 @@
|
||||||
</div>
|
</div>
|
||||||
<Switch bind:state={permissions.workspace.tools} />
|
<Switch bind:state={permissions.workspace.tools} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{#if defaultPermissions?.workspace?.tools && !permissions.workspace.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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Sharing Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Sharing Permissions')}</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Models Public Sharing')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Models Public Sharing')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.sharing.public_models} />
|
||||||
</div>
|
</div>
|
||||||
<Switch bind:state={permissions.sharing.public_models} />
|
{#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>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Knowledge Public Sharing')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Knowledge Public Sharing')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.sharing.public_knowledge} />
|
||||||
</div>
|
</div>
|
||||||
<Switch bind:state={permissions.sharing.public_knowledge} />
|
{#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>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Prompts Public Sharing')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Prompts Public Sharing')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.sharing.public_prompts} />
|
||||||
</div>
|
</div>
|
||||||
<Switch bind:state={permissions.sharing.public_prompts} />
|
{#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>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Tools Public Sharing')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Tools Public Sharing')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.sharing.public_tools} />
|
||||||
</div>
|
</div>
|
||||||
<Switch bind:state={permissions.sharing.public_tools} />
|
{#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>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow File Upload')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow File Upload')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.file_upload} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.file_upload && !permissions.chat.file_upload}
|
||||||
<Switch bind:state={permissions.chat.file_upload} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Chat Controls')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat Controls')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.controls} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.controls && !permissions.chat.controls}
|
||||||
<Switch bind:state={permissions.chat.controls} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if permissions.chat.controls}
|
{#if permissions.chat.controls}
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Chat Valves')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat Valves')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.valves} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.valves && !permissions.chat.valves}
|
||||||
<Switch bind:state={permissions.chat.valves} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Chat System Prompt')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat System Prompt')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.system_prompt} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.system_prompt && !permissions.chat.system_prompt}
|
||||||
<Switch bind:state={permissions.chat.system_prompt} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Chat Params')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat Params')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.params} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.params && !permissions.chat.params}
|
||||||
<Switch bind:state={permissions.chat.params} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Chat Edit')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat Edit')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.edit} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.edit && !permissions.chat.edit}
|
||||||
<Switch bind:state={permissions.chat.edit} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Chat Delete')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat Delete')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.delete} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.delete && !permissions.chat.delete}
|
||||||
<Switch bind:state={permissions.chat.delete} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Delete Messages')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Delete Messages')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.delete_message} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.delete_message && !permissions.chat.delete_message}
|
||||||
<Switch bind:state={permissions.chat.delete_message} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Continue Response')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Continue Response')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.continue_response} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.continue_response && !permissions.chat.continue_response}
|
||||||
<Switch bind:state={permissions.chat.continue_response} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Regenerate Response')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Regenerate Response')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.regenerate_response} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.regenerate_response && !permissions.chat.regenerate_response}
|
||||||
<Switch bind:state={permissions.chat.regenerate_response} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Rate Response')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Rate Response')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.rate_response} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.rate_response && !permissions.chat.rate_response}
|
||||||
<Switch bind:state={permissions.chat.rate_response} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Chat Share')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat Share')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.share} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.share && !permissions.chat.share}
|
||||||
<Switch bind:state={permissions.chat.share} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Chat Export')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat Export')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.export} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.export && !permissions.chat.export}
|
||||||
<Switch bind:state={permissions.chat.export} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Speech to Text')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Speech to Text')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.stt} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.stt && !permissions.chat.stt}
|
||||||
<Switch bind:state={permissions.chat.stt} />
|
<div>
|
||||||
</div>
|
<div class="text-xs text-gray-500">
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
<div class=" self-center text-xs font-medium">
|
</div>
|
||||||
{$i18n.t('Allow Text to Speech')}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<Switch bind:state={permissions.chat.tts} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Call')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Text to Speech')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.tts} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.tts && !permissions.chat.tts}
|
||||||
<Switch bind:state={permissions.chat.call} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Multiple Models in Chat')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Call')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.call} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.call && !permissions.chat.call}
|
||||||
<Switch bind:state={permissions.chat.multiple_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>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Allow Temporary Chat')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Multiple Models in Chat')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.multiple_models} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.multiple_models && !permissions.chat.multiple_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>
|
||||||
|
|
||||||
<Switch bind:state={permissions.chat.temporary} />
|
<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('Allow Temporary Chat')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.temporary} />
|
||||||
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.temporary && !permissions.chat.temporary}
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if permissions.chat.temporary}
|
{#if permissions.chat.temporary}
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Enforce Temporary Chat')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Enforce Temporary Chat')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.chat.temporary_enforced} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.chat?.temporary_enforced && !permissions.chat.temporary_enforced}
|
||||||
<Switch bind:state={permissions.chat.temporary_enforced} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Direct Tool Servers')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Direct Tool Servers')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.features.direct_tool_servers} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.features?.direct_tool_servers && !permissions.features.direct_tool_servers}
|
||||||
<Switch bind:state={permissions.features.direct_tool_servers} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Web Search')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Web Search')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.features.web_search} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.features?.web_search && !permissions.features.web_search}
|
||||||
<Switch bind:state={permissions.features.web_search} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Image Generation')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Image Generation')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.features.image_generation} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.features?.image_generation && !permissions.features.image_generation}
|
||||||
<Switch bind:state={permissions.features.image_generation} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Code Interpreter')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Code Interpreter')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.features.code_interpreter} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.features?.code_interpreter && !permissions.features.code_interpreter}
|
||||||
<Switch bind:state={permissions.features.code_interpreter} />
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class="flex w-full justify-between my-1">
|
||||||
{$i18n.t('Notes')}
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Notes')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.features.notes} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if defaultPermissions?.features?.notes && !permissions.features.notes}
|
||||||
<Switch bind:state={permissions.features.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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -75,10 +75,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full items-center justify-between">
|
<div class="flex w-full items-center justify-between overflow-hidden">
|
||||||
<Tooltip content={user.email} placement="top-start">
|
<Tooltip content={user.email} placement="top-start">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class=" font-medium self-center">{user.name}</div>
|
<div class=" font-medium self-center truncate">{user.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
|
@ -339,30 +339,6 @@
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
|
||||||
on:click={() => setSortKey('oauth_sub')}
|
|
||||||
>
|
|
||||||
<div class="flex gap-1.5 items-center">
|
|
||||||
{$i18n.t('OAuth ID')}
|
|
||||||
|
|
||||||
{#if orderBy === 'oauth_sub'}
|
|
||||||
<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" />
|
<th scope="col" class="px-2.5 py-2 text-right" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -383,10 +359,10 @@
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1 font-medium text-gray-900 dark:text-white w-max">
|
<td class="px-3 py-1 font-medium text-gray-900 dark:text-white max-w-48">
|
||||||
<div class="flex flex-row w-max">
|
<div class="flex items-center">
|
||||||
<img
|
<img
|
||||||
class=" rounded-full w-6 h-6 object-cover mr-2.5"
|
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={user?.profile_image_url?.startsWith(WEBUI_BASE_URL) ||
|
||||||
user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
|
user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
|
||||||
user.profile_image_url.startsWith('data:')
|
user.profile_image_url.startsWith('data:')
|
||||||
|
@ -395,7 +371,7 @@
|
||||||
alt="user"
|
alt="user"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class=" font-medium self-center">{user.name}</div>
|
<div class="font-medium truncate">{user.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class=" px-3 py-1"> {user.email} </td>
|
<td class=" px-3 py-1"> {user.email} </td>
|
||||||
|
@ -408,8 +384,6 @@
|
||||||
{dayjs(user.created_at * 1000).format('LL')}
|
{dayjs(user.created_at * 1000).format('LL')}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class=" px-3 py-1"> {user.oauth_sub ?? ''} </td>
|
|
||||||
|
|
||||||
<td class="px-3 py-1 text-right">
|
<td class="px-3 py-1 text-right">
|
||||||
<div class="flex justify-end w-full">
|
<div class="flex justify-end w-full">
|
||||||
{#if $config.features.enable_admin_chat_access && user.role !== 'admin'}
|
{#if $config.features.enable_admin_chat_access && user.role !== 'admin'}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
|
import UserProfileImage from '$lib/components/chat/Settings/Account/UserProfileImage.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
@ -83,110 +84,118 @@
|
||||||
submitHandler();
|
submitHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" flex items-center rounded-md px-5 py-2 w-full">
|
<div class=" px-5 pt-3 pb-5 w-full">
|
||||||
<div class=" self-center mr-5">
|
<div class="flex self-center w-full">
|
||||||
<img
|
<div class=" self-start h-full mr-6">
|
||||||
src={selectedUser.profile_image_url}
|
<UserProfileImage bind:profileImageUrl={_user.profile_image_url} user={_user} />
|
||||||
class=" max-w-[55px] object-cover rounded-full"
|
|
||||||
alt="User profile"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
|
|
||||||
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
{$i18n.t('Created at')}
|
|
||||||
{dayjs(selectedUser.created_at * 1000).format('LL')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" px-5 pt-3 pb-5">
|
<div class=" flex-1">
|
||||||
<div class=" flex flex-col space-y-1.5">
|
<div class="overflow-hidden w-ful mb-2">
|
||||||
<div class="flex flex-col w-full">
|
<div class=" self-center capitalize font-medium truncate">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
|
{selectedUser.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="text-xs text-gray-500">
|
||||||
<select
|
{$i18n.t('Created at')}
|
||||||
class="w-full dark:bg-gray-900 text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
{dayjs(selectedUser.created_at * 1000).format('LL')}
|
||||||
bind:value={_user.role}
|
</div>
|
||||||
disabled={_user.id == sessionUser.id}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="admin">{$i18n.t('Admin')}</option>
|
|
||||||
<option value="user">{$i18n.t('User')}</option>
|
|
||||||
<option value="pending">{$i18n.t('Pending')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if userGroups}
|
<div class=" flex flex-col space-y-1.5">
|
||||||
<div class="flex flex-col w-full text-sm">
|
{#if (userGroups ?? []).length > 0}
|
||||||
<div class="mb-1 text-xs text-gray-500">{$i18n.t('User Groups')}</div>
|
<div class="flex flex-col w-full text-sm">
|
||||||
|
<div class="mb-1 text-xs text-gray-500">{$i18n.t('User Groups')}</div>
|
||||||
|
|
||||||
{#if userGroups.length}
|
<div class="flex flex-wrap gap-1 my-0.5 -mx-1">
|
||||||
<div class="flex flex-wrap gap-1 my-0.5 -mx-1">
|
{#each userGroups as userGroup}
|
||||||
{#each userGroups as userGroup}
|
<span
|
||||||
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-xs">
|
class="px-1.5 py-0.5 rounded-xl bg-gray-100 dark:bg-gray-850 text-xs"
|
||||||
<a
|
|
||||||
href={'/admin/users/groups?id=' + userGroup.id}
|
|
||||||
on:click|preventDefault={() =>
|
|
||||||
goto('/admin/users/groups?id=' + userGroup.id)}
|
|
||||||
>
|
>
|
||||||
{userGroup.name}
|
<a
|
||||||
</a>
|
href={'/admin/users/groups?id=' + userGroup.id}
|
||||||
</span>
|
on:click|preventDefault={() =>
|
||||||
{/each}
|
goto('/admin/users/groups?id=' + userGroup.id)}
|
||||||
|
>
|
||||||
|
{userGroup.name}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
<span>-</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<select
|
||||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
class="w-full dark:bg-gray-900 text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||||
type="email"
|
bind:value={_user.role}
|
||||||
bind:value={_user.email}
|
disabled={_user.id == sessionUser.id}
|
||||||
placeholder={$i18n.t('Enter Your Email')}
|
required
|
||||||
autocomplete="off"
|
>
|
||||||
required
|
<option value="admin">{$i18n.t('Admin')}</option>
|
||||||
/>
|
<option value="user">{$i18n.t('User')}</option>
|
||||||
</div>
|
<option value="pending">{$i18n.t('Pending')}</option>
|
||||||
</div>
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent outline-hidden"
|
class="w-full text-sm bg-transparent outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={_user.name}
|
bind:value={_user.name}
|
||||||
placeholder={$i18n.t('Enter Your Name')}
|
placeholder={$i18n.t('Enter Your Name')}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<SensitiveInput
|
<input
|
||||||
class="w-full text-sm bg-transparent outline-hidden"
|
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||||
type="password"
|
type="email"
|
||||||
placeholder={$i18n.t('Enter New Password')}
|
bind:value={_user.email}
|
||||||
bind:value={_user.password}
|
placeholder={$i18n.t('Enter Your Email')}
|
||||||
autocomplete="new-password"
|
autocomplete="off"
|
||||||
required={false}
|
required
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if _user?.oauth_sub}
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('OAuth ID')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1 text-sm break-all mb-1">
|
||||||
|
{_user.oauth_sub ?? ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<SensitiveInput
|
||||||
|
class="w-full text-sm bg-transparent outline-hidden"
|
||||||
|
type="password"
|
||||||
|
placeholder={$i18n.t('Enter New Password')}
|
||||||
|
bind:value={_user.password}
|
||||||
|
autocomplete="new-password"
|
||||||
|
required={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -107,7 +107,9 @@
|
||||||
bind:query
|
bind:query
|
||||||
bind:orderBy
|
bind:orderBy
|
||||||
bind:direction
|
bind:direction
|
||||||
title={$i18n.t("{{user}}'s Chats", { user: user.name })}
|
title={$i18n.t("{{user}}'s Chats", {
|
||||||
|
user: user.name.length > 32 ? `${user.name.slice(0, 32)}...` : user.name
|
||||||
|
})}
|
||||||
emptyPlaceholder={$i18n.t('No chats found for this user.')}
|
emptyPlaceholder={$i18n.t('No chats found for this user.')}
|
||||||
shareUrl={true}
|
shareUrl={true}
|
||||||
{chatList}
|
{chatList}
|
||||||
|
|
|
@ -20,12 +20,14 @@
|
||||||
|
|
||||||
let scrollEnd = true;
|
let scrollEnd = true;
|
||||||
let messagesContainerElement = null;
|
let messagesContainerElement = null;
|
||||||
|
let chatInputElement = null;
|
||||||
|
|
||||||
let top = false;
|
let top = false;
|
||||||
|
|
||||||
let channel = null;
|
let channel = null;
|
||||||
let messages = null;
|
let messages = null;
|
||||||
|
|
||||||
|
let replyToMessage = null;
|
||||||
let threadId = null;
|
let threadId = null;
|
||||||
|
|
||||||
let typingUsers = [];
|
let typingUsers = [];
|
||||||
|
@ -141,16 +143,20 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await sendMessage(localStorage.token, id, { content: content, data: data }).catch(
|
const res = await sendMessage(localStorage.token, id, {
|
||||||
(error) => {
|
content: content,
|
||||||
toast.error(`${error}`);
|
data: data,
|
||||||
return null;
|
reply_to_id: replyToMessage?.id ?? null
|
||||||
}
|
}).catch((error) => {
|
||||||
);
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replyToMessage = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChange = async () => {
|
const onChange = async () => {
|
||||||
|
@ -222,8 +228,14 @@
|
||||||
{#key id}
|
{#key id}
|
||||||
<Messages
|
<Messages
|
||||||
{channel}
|
{channel}
|
||||||
{messages}
|
|
||||||
{top}
|
{top}
|
||||||
|
{messages}
|
||||||
|
{replyToMessage}
|
||||||
|
onReply={async (message) => {
|
||||||
|
replyToMessage = message;
|
||||||
|
await tick();
|
||||||
|
chatInputElement?.focus();
|
||||||
|
}}
|
||||||
onThread={(id) => {
|
onThread={(id) => {
|
||||||
threadId = id;
|
threadId = id;
|
||||||
}}
|
}}
|
||||||
|
@ -250,6 +262,8 @@
|
||||||
<div class=" pb-[1rem] px-2.5">
|
<div class=" pb-[1rem] px-2.5">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
id="root"
|
id="root"
|
||||||
|
bind:chatInputElement
|
||||||
|
bind:replyToMessage
|
||||||
{typingUsers}
|
{typingUsers}
|
||||||
userSuggestions={true}
|
userSuggestions={true}
|
||||||
channelSuggestions={true}
|
channelSuggestions={true}
|
||||||
|
|
|
@ -23,20 +23,23 @@
|
||||||
|
|
||||||
import { getSessionUser } from '$lib/apis/auths';
|
import { getSessionUser } from '$lib/apis/auths';
|
||||||
|
|
||||||
|
import { uploadFile } from '$lib/apis/files';
|
||||||
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
||||||
|
import CommandSuggestionList from '../chat/MessageInput/CommandSuggestionList.svelte';
|
||||||
|
|
||||||
|
import InputMenu from './MessageInput/InputMenu.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import RichTextInput from '../common/RichTextInput.svelte';
|
import RichTextInput from '../common/RichTextInput.svelte';
|
||||||
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
|
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
|
||||||
import InputMenu from './MessageInput/InputMenu.svelte';
|
|
||||||
import { uploadFile } from '$lib/apis/files';
|
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
|
||||||
import FileItem from '../common/FileItem.svelte';
|
import FileItem from '../common/FileItem.svelte';
|
||||||
import Image from '../common/Image.svelte';
|
import Image from '../common/Image.svelte';
|
||||||
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
||||||
import InputVariablesModal from '../chat/MessageInput/InputVariablesModal.svelte';
|
import InputVariablesModal from '../chat/MessageInput/InputVariablesModal.svelte';
|
||||||
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
|
||||||
import CommandSuggestionList from '../chat/MessageInput/CommandSuggestionList.svelte';
|
|
||||||
import MentionList from './MessageInput/MentionList.svelte';
|
import MentionList from './MessageInput/MentionList.svelte';
|
||||||
import Skeleton from '../chat/Messages/Skeleton.svelte';
|
import Skeleton from '../chat/Messages/Skeleton.svelte';
|
||||||
|
import XMark from '../icons/XMark.svelte';
|
||||||
|
|
||||||
export let placeholder = $i18n.t('Type here...');
|
export let placeholder = $i18n.t('Type here...');
|
||||||
|
|
||||||
|
@ -60,6 +63,8 @@
|
||||||
export let userSuggestions = false;
|
export let userSuggestions = false;
|
||||||
export let channelSuggestions = false;
|
export let channelSuggestions = false;
|
||||||
|
|
||||||
|
export let replyToMessage = null;
|
||||||
|
|
||||||
export let typingUsersClassName = 'from-white dark:from-gray-900';
|
export let typingUsersClassName = 'from-white dark:from-gray-900';
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
@ -773,6 +778,32 @@
|
||||||
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
|
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
|
||||||
dir={$settings?.chatDirection ?? 'auto'}
|
dir={$settings?.chatDirection ?? 'auto'}
|
||||||
>
|
>
|
||||||
|
{#if replyToMessage !== null}
|
||||||
|
<div class="px-3 pt-3 text-left w-full flex flex-col z-10">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<div class="pl-[1px] flex items-center gap-2 text-sm">
|
||||||
|
<div class="translate-y-[0.5px]">
|
||||||
|
<span class=""
|
||||||
|
>{$i18n.t('Replying to {{NAME}}', {
|
||||||
|
NAME: replyToMessage?.meta?.model_name ?? replyToMessage.user.name
|
||||||
|
})}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="flex items-center dark:text-gray-500"
|
||||||
|
on:click={() => {
|
||||||
|
replyToMessage = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if files.length > 0}
|
{#if files.length > 0}
|
||||||
<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
|
<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
|
||||||
{#each files as file, fileIdx}
|
{#each files as file, fileIdx}
|
||||||
|
@ -890,6 +921,7 @@
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
console.info('Escape');
|
console.info('Escape');
|
||||||
|
replyToMessage = null;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:paste={async (e) => {
|
on:paste={async (e) => {
|
||||||
|
|
|
@ -23,10 +23,12 @@
|
||||||
export let id = null;
|
export let id = null;
|
||||||
export let channel = null;
|
export let channel = null;
|
||||||
export let messages = [];
|
export let messages = [];
|
||||||
|
export let replyToMessage = null;
|
||||||
export let top = false;
|
export let top = false;
|
||||||
export let thread = false;
|
export let thread = false;
|
||||||
|
|
||||||
export let onLoad: Function = () => {};
|
export let onLoad: Function = () => {};
|
||||||
|
export let onReply: Function = () => {};
|
||||||
export let onThread: Function = () => {};
|
export let onThread: Function = () => {};
|
||||||
|
|
||||||
let messagesLoading = false;
|
let messagesLoading = false;
|
||||||
|
@ -94,10 +96,12 @@
|
||||||
<Message
|
<Message
|
||||||
{message}
|
{message}
|
||||||
{thread}
|
{thread}
|
||||||
|
replyToMessage={replyToMessage?.id === message.id}
|
||||||
disabled={!channel?.write_access}
|
disabled={!channel?.write_access}
|
||||||
showUserProfile={messageIdx === 0 ||
|
showUserProfile={messageIdx === 0 ||
|
||||||
messageList.at(messageIdx - 1)?.user_id !== message.user_id ||
|
messageList.at(messageIdx - 1)?.user_id !== message.user_id ||
|
||||||
messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id}
|
messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id ||
|
||||||
|
message?.reply_to_message}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
messages = messages.filter((m) => m.id !== message.id);
|
messages = messages.filter((m) => m.id !== message.id);
|
||||||
|
|
||||||
|
@ -123,6 +127,9 @@
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onReply={(message) => {
|
||||||
|
onReply(message);
|
||||||
|
}}
|
||||||
onThread={(id) => {
|
onThread={(id) => {
|
||||||
onThread(id);
|
onThread(id);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -13,8 +13,9 @@
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||||
|
|
||||||
import { settings, user, shortCodesToEmojis } from '$lib/stores';
|
import { formatDate } from '$lib/utils';
|
||||||
|
|
||||||
|
import { settings, user, shortCodesToEmojis } from '$lib/stores';
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
||||||
|
@ -32,18 +33,20 @@
|
||||||
import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
|
import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
|
||||||
import EmojiPicker from '$lib/components/common/EmojiPicker.svelte';
|
import EmojiPicker from '$lib/components/common/EmojiPicker.svelte';
|
||||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||||
import { formatDate } from '$lib/utils';
|
|
||||||
import Emoji from '$lib/components/common/Emoji.svelte';
|
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||||
import { t } from 'i18next';
|
|
||||||
import Skeleton from '$lib/components/chat/Messages/Skeleton.svelte';
|
import Skeleton from '$lib/components/chat/Messages/Skeleton.svelte';
|
||||||
|
import ArrowUpLeftAlt from '$lib/components/icons/ArrowUpLeftAlt.svelte';
|
||||||
|
|
||||||
export let message;
|
export let message;
|
||||||
export let showUserProfile = true;
|
export let showUserProfile = true;
|
||||||
export let thread = false;
|
export let thread = false;
|
||||||
|
|
||||||
|
export let replyToMessage = false;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
export let onDelete: Function = () => {};
|
export let onDelete: Function = () => {};
|
||||||
export let onEdit: Function = () => {};
|
export let onEdit: Function = () => {};
|
||||||
|
export let onReply: Function = () => {};
|
||||||
export let onThread: Function = () => {};
|
export let onThread: Function = () => {};
|
||||||
export let onReaction: Function = () => {};
|
export let onReaction: Function = () => {};
|
||||||
|
|
||||||
|
@ -65,9 +68,15 @@
|
||||||
|
|
||||||
{#if message}
|
{#if message}
|
||||||
<div
|
<div
|
||||||
|
id="message-{message.id}"
|
||||||
class="flex flex-col justify-between px-5 {showUserProfile
|
class="flex flex-col justify-between px-5 {showUserProfile
|
||||||
? 'pt-1.5 pb-0.5'
|
? 'pt-1.5 pb-0.5'
|
||||||
: ''} w-full max-w-full mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative"
|
: ''} w-full max-w-full mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative {replyToMessage
|
||||||
|
? 'border-l-4 border-blue-500 bg-blue-100/10 dark:bg-blue-100/5 pl-4'
|
||||||
|
: ''} {(message?.reply_to_message?.meta?.model_id ?? message?.reply_to_message?.user_id) ===
|
||||||
|
$user?.id
|
||||||
|
? 'border-l-4 border-orange-500 bg-orange-100/10 dark:bg-orange-100/5 pl-4'
|
||||||
|
: ''}"
|
||||||
>
|
>
|
||||||
{#if !edit && !disabled}
|
{#if !edit && !disabled}
|
||||||
<div
|
<div
|
||||||
|
@ -95,6 +104,17 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</EmojiPicker>
|
</EmojiPicker>
|
||||||
|
|
||||||
|
<Tooltip content={$i18n.t('Reply')}>
|
||||||
|
<button
|
||||||
|
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-0.5"
|
||||||
|
on:click={() => {
|
||||||
|
onReply(message);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowUpLeftAlt className="size-5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{#if !thread}
|
{#if !thread}
|
||||||
<Tooltip content={$i18n.t('Reply in Thread')}>
|
<Tooltip content={$i18n.t('Reply in Thread')}>
|
||||||
<button
|
<button
|
||||||
|
@ -134,6 +154,56 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if message?.reply_to_message?.user}
|
||||||
|
<div class="relative text-xs mb-1">
|
||||||
|
<div
|
||||||
|
class="absolute h-3 w-7 left-[18px] top-2 rounded-tl-lg border-t-2 border-l-2 border-gray-300 dark:border-gray-500 z-0"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="ml-12 flex items-center space-x-2 relative z-0"
|
||||||
|
on:click={() => {
|
||||||
|
const messageElement = document.getElementById(
|
||||||
|
`message-${message.reply_to_message.id}`
|
||||||
|
);
|
||||||
|
if (messageElement) {
|
||||||
|
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
messageElement.classList.add('highlight');
|
||||||
|
setTimeout(() => {
|
||||||
|
messageElement.classList.remove('highlight');
|
||||||
|
}, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if message?.reply_to_message?.meta?.model_id}
|
||||||
|
<img
|
||||||
|
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${message.reply_to_message.meta.model_id}`}
|
||||||
|
alt={message.reply_to_message.meta.model_name ??
|
||||||
|
message.reply_to_message.meta.model_id}
|
||||||
|
class="size-4 ml-0.5 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<img
|
||||||
|
src={message.reply_to_message.user?.profile_image_url ??
|
||||||
|
`${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
|
alt={message.reply_to_message.user?.name ?? $i18n.t('Unknown User')}
|
||||||
|
class="size-4 ml-0.5 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="shrink-0">
|
||||||
|
{message?.reply_to_message.meta?.model_name ??
|
||||||
|
message?.reply_to_message.user?.name ??
|
||||||
|
$i18n.t('Unknown User')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="italic text-sm text-gray-500 dark:text-gray-400 line-clamp-1 w-full flex-1">
|
||||||
|
<Markdown id={`${message.id}-reply-to`} content={message?.reply_to_message?.content} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div
|
<div
|
||||||
class=" flex w-full message-{message.id}"
|
class=" flex w-full message-{message.id}"
|
||||||
id="message-{message.id}"
|
id="message-{message.id}"
|
||||||
|
@ -151,7 +221,7 @@
|
||||||
<ProfilePreview user={message.user}>
|
<ProfilePreview user={message.user}>
|
||||||
<ProfileImage
|
<ProfileImage
|
||||||
src={message.user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
src={message.user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
className={'size-8 translate-y-1 ml-0.5'}
|
className={'size-8 ml-0.5'}
|
||||||
/>
|
/>
|
||||||
</ProfilePreview>
|
</ProfilePreview>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -348,3 +418,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.highlight {
|
||||||
|
animation: highlightAnimation 2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes highlightAnimation {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(0, 60, 255, 0.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -22,11 +22,14 @@
|
||||||
let messages = null;
|
let messages = null;
|
||||||
let top = false;
|
let top = false;
|
||||||
|
|
||||||
|
let messagesContainerElement = null;
|
||||||
|
let chatInputElement = null;
|
||||||
|
|
||||||
|
let replyToMessage = null;
|
||||||
|
|
||||||
let typingUsers = [];
|
let typingUsers = [];
|
||||||
let typingUsersTimeout = {};
|
let typingUsersTimeout = {};
|
||||||
|
|
||||||
let messagesContainerElement = null;
|
|
||||||
|
|
||||||
$: if (threadId) {
|
$: if (threadId) {
|
||||||
initHandler();
|
initHandler();
|
||||||
}
|
}
|
||||||
|
@ -128,12 +131,15 @@
|
||||||
|
|
||||||
const res = await sendMessage(localStorage.token, channel.id, {
|
const res = await sendMessage(localStorage.token, channel.id, {
|
||||||
parent_id: threadId,
|
parent_id: threadId,
|
||||||
|
reply_to_id: replyToMessage?.id ?? null,
|
||||||
content: content,
|
content: content,
|
||||||
data: data
|
data: data
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
replyToMessage = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChange = async () => {
|
const onChange = async () => {
|
||||||
|
@ -180,9 +186,16 @@
|
||||||
<Messages
|
<Messages
|
||||||
id={threadId}
|
id={threadId}
|
||||||
{channel}
|
{channel}
|
||||||
{messages}
|
|
||||||
{top}
|
{top}
|
||||||
|
{messages}
|
||||||
|
{replyToMessage}
|
||||||
thread={true}
|
thread={true}
|
||||||
|
onReply={async (message) => {
|
||||||
|
replyToMessage = message;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
chatInputElement?.focus();
|
||||||
|
}}
|
||||||
onLoad={async () => {
|
onLoad={async () => {
|
||||||
const newMessages = await getChannelThreadMessages(
|
const newMessages = await getChannelThreadMessages(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
|
@ -207,6 +220,8 @@
|
||||||
|
|
||||||
<div class=" pb-[1rem] px-2.5 w-full">
|
<div class=" pb-[1rem] px-2.5 w-full">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
|
bind:replyToMessage
|
||||||
|
bind:chatInputElement
|
||||||
id={threadId}
|
id={threadId}
|
||||||
disabled={!channel?.write_access}
|
disabled={!channel?.write_access}
|
||||||
placeholder={!channel?.write_access
|
placeholder={!channel?.write_access
|
||||||
|
|
|
@ -38,7 +38,8 @@
|
||||||
toolServers,
|
toolServers,
|
||||||
functions,
|
functions,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
pinnedChats
|
pinnedChats,
|
||||||
|
showEmbeds
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
convertMessagesToHistory,
|
convertMessagesToHistory,
|
||||||
|
@ -362,6 +363,8 @@
|
||||||
message.content = data.content;
|
message.content = data.content;
|
||||||
} else if (type === 'chat:message:files' || type === 'files') {
|
} else if (type === 'chat:message:files' || type === 'files') {
|
||||||
message.files = data.files;
|
message.files = data.files;
|
||||||
|
} else if (type === 'chat:message:embeds' || type === 'embeds') {
|
||||||
|
message.embeds = data.embeds;
|
||||||
} else if (type === 'chat:message:error') {
|
} else if (type === 'chat:message:error') {
|
||||||
message.error = data.error;
|
message.error = data.error;
|
||||||
} else if (type === 'chat:message:follow_ups') {
|
} else if (type === 'chat:message:follow_ups') {
|
||||||
|
@ -562,6 +565,7 @@
|
||||||
showCallOverlay.set(false);
|
showCallOverlay.set(false);
|
||||||
showOverview.set(false);
|
showOverview.set(false);
|
||||||
showArtifacts.set(false);
|
showArtifacts.set(false);
|
||||||
|
showEmbeds.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2230,7 +2234,7 @@
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>
|
<title>
|
||||||
{$chatTitle
|
{$settings.showChatTitleInTab !== false && $chatTitle
|
||||||
? `${$chatTitle.length > 30 ? `${$chatTitle.slice(0, 30)}...` : $chatTitle} • ${$WEBUI_NAME}`
|
? `${$chatTitle.length > 30 ? `${$chatTitle.slice(0, 30)}...` : $chatTitle} • ${$WEBUI_NAME}`
|
||||||
: `${$WEBUI_NAME}`}
|
: `${$WEBUI_NAME}`}
|
||||||
</title>
|
</title>
|
||||||
|
|
|
@ -4,7 +4,14 @@
|
||||||
import { Pane, PaneResizer } from 'paneforge';
|
import { Pane, PaneResizer } from 'paneforge';
|
||||||
|
|
||||||
import { onDestroy, onMount, tick } from 'svelte';
|
import { onDestroy, onMount, tick } from 'svelte';
|
||||||
import { mobile, showControls, showCallOverlay, showOverview, showArtifacts } from '$lib/stores';
|
import {
|
||||||
|
mobile,
|
||||||
|
showControls,
|
||||||
|
showCallOverlay,
|
||||||
|
showOverview,
|
||||||
|
showArtifacts,
|
||||||
|
showEmbeds
|
||||||
|
} from '$lib/stores';
|
||||||
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Controls from './Controls/Controls.svelte';
|
import Controls from './Controls/Controls.svelte';
|
||||||
|
@ -13,6 +20,7 @@
|
||||||
import Overview from './Overview.svelte';
|
import Overview from './Overview.svelte';
|
||||||
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
||||||
import Artifacts from './Artifacts.svelte';
|
import Artifacts from './Artifacts.svelte';
|
||||||
|
import Embeds from './ChatControls/Embeds.svelte';
|
||||||
|
|
||||||
export let history;
|
export let history;
|
||||||
export let models = [];
|
export let models = [];
|
||||||
|
@ -134,6 +142,7 @@
|
||||||
showControls.set(false);
|
showControls.set(false);
|
||||||
showOverview.set(false);
|
showOverview.set(false);
|
||||||
showArtifacts.set(false);
|
showArtifacts.set(false);
|
||||||
|
showEmbeds.set(false);
|
||||||
|
|
||||||
if ($showCallOverlay) {
|
if ($showCallOverlay) {
|
||||||
showCallOverlay.set(false);
|
showCallOverlay.set(false);
|
||||||
|
@ -155,9 +164,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class=" {$showCallOverlay || $showOverview || $showArtifacts
|
class=" {$showCallOverlay || $showOverview || $showArtifacts || $showEmbeds
|
||||||
? ' h-screen w-full'
|
? ' h-screen w-full'
|
||||||
: 'px-6 py-4'} h-full"
|
: 'px-4 py-3'} h-full"
|
||||||
>
|
>
|
||||||
{#if $showCallOverlay}
|
{#if $showCallOverlay}
|
||||||
<div
|
<div
|
||||||
|
@ -175,6 +184,8 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if $showEmbeds}
|
||||||
|
<Embeds />
|
||||||
{:else if $showArtifacts}
|
{:else if $showArtifacts}
|
||||||
<Artifacts {history} />
|
<Artifacts {history} />
|
||||||
{:else if $showOverview}
|
{:else if $showOverview}
|
||||||
|
@ -241,9 +252,9 @@
|
||||||
{#if $showControls}
|
{#if $showControls}
|
||||||
<div class="flex max-h-full min-h-full">
|
<div class="flex max-h-full min-h-full">
|
||||||
<div
|
<div
|
||||||
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
|
class="w-full {($showOverview || $showArtifacts || $showEmbeds) && !$showCallOverlay
|
||||||
? ' '
|
? ' '
|
||||||
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 '} z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
: 'px-4 py-3 bg-white dark:shadow-lg dark:bg-gray-850 '} z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||||
id="controls-container"
|
id="controls-container"
|
||||||
>
|
>
|
||||||
{#if $showCallOverlay}
|
{#if $showCallOverlay}
|
||||||
|
@ -260,6 +271,8 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if $showEmbeds}
|
||||||
|
<Embeds overlay={dragged} />
|
||||||
{:else if $showArtifacts}
|
{:else if $showArtifacts}
|
||||||
<Artifacts {history} overlay={dragged} />
|
<Artifacts {history} overlay={dragged} />
|
||||||
{:else if $showOverview}
|
{:else if $showOverview}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script>
|
||||||
|
import { embed, showControls, showEmbeds } from '$lib/stores';
|
||||||
|
|
||||||
|
import FullHeightIframe from '$lib/components/common/FullHeightIframe.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
|
export let overlay = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $embed}
|
||||||
|
<div class="h-full w-full">
|
||||||
|
<div
|
||||||
|
class="pointer-events-auto z-20 flex justify-between items-center py-3 px-2 font-primar text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
<div class="flex-1 flex items-center justify-between pl-2">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
{$embed?.title ?? 'Embedded Content'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
|
||||||
|
on:click={() => {
|
||||||
|
showControls.set(false);
|
||||||
|
showEmbeds.set(false);
|
||||||
|
embed.set(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className="size-3.5 text-gray-900 dark:text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" w-full h-full relative">
|
||||||
|
{#if overlay}
|
||||||
|
<div class=" absolute top-0 left-0 right-0 bottom-0 z-10"></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<FullHeightIframe src={$embed?.url} iframeClassName="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -78,6 +78,8 @@
|
||||||
|
|
||||||
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
||||||
import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte';
|
import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte';
|
||||||
|
import Knobs from '../icons/Knobs.svelte';
|
||||||
|
import ValvesModal from '../workspace/common/ValvesModal.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
@ -112,6 +114,10 @@
|
||||||
let inputVariables = {};
|
let inputVariables = {};
|
||||||
let inputVariableValues = {};
|
let inputVariableValues = {};
|
||||||
|
|
||||||
|
let showValvesModal = false;
|
||||||
|
let selectedValvesType = 'tool'; // 'tool' or 'function'
|
||||||
|
let selectedValvesItemId = null;
|
||||||
|
|
||||||
$: onChange({
|
$: onChange({
|
||||||
prompt,
|
prompt,
|
||||||
files: files
|
files: files
|
||||||
|
@ -932,6 +938,16 @@
|
||||||
onSave={inputVariablesModalCallback}
|
onSave={inputVariablesModalCallback}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ValvesModal
|
||||||
|
bind:show={showValvesModal}
|
||||||
|
userValves={true}
|
||||||
|
type={selectedValvesType}
|
||||||
|
id={selectedValvesItemId ?? null}
|
||||||
|
on:save={async () => {
|
||||||
|
await tick();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="w-full font-primary">
|
<div class="w-full font-primary">
|
||||||
<div class=" mx-auto inset-x-0 bg-transparent flex justify-center">
|
<div class=" mx-auto inset-x-0 bg-transparent flex justify-center">
|
||||||
|
@ -1449,6 +1465,12 @@
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
bind:imageGenerationEnabled
|
bind:imageGenerationEnabled
|
||||||
bind:codeInterpreterEnabled
|
bind:codeInterpreterEnabled
|
||||||
|
onShowValves={(e) => {
|
||||||
|
const { type, id } = e;
|
||||||
|
selectedValvesType = type;
|
||||||
|
selectedValvesItemId = id;
|
||||||
|
showValvesModal = true;
|
||||||
|
}}
|
||||||
onClose={async () => {
|
onClose={async () => {
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
|
@ -1465,6 +1487,24 @@
|
||||||
</IntegrationsMenu>
|
</IntegrationsMenu>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedModelIds.length === 1 && $models.find((m) => m.id === selectedModelIds[0])?.has_user_valves}
|
||||||
|
<div class="ml-1 flex gap-1.5">
|
||||||
|
<Tooltip content={$i18n.t('Valves')} placement="top">
|
||||||
|
<button
|
||||||
|
id="model-valves-button"
|
||||||
|
class="bg-transparent hover:bg-gray-100 text-gray-700 dark:text-white dark:hover:bg-gray-800 rounded-full size-8 flex justify-center items-center outline-hidden focus:outline-hidden"
|
||||||
|
on:click={() => {
|
||||||
|
selectedValvesType = 'function';
|
||||||
|
selectedValvesItemId = selectedModelIds[0]?.split('.')[0];
|
||||||
|
showValvesModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Knobs className="size-4" strokeWidth="1.5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="ml-1 flex gap-1.5">
|
<div class="ml-1 flex gap-1.5">
|
||||||
{#if (selectedToolIds ?? []).length > 0}
|
{#if (selectedToolIds ?? []).length > 0}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -1500,11 +1540,11 @@
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {selectedFilterIds.includes(
|
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {selectedFilterIds.includes(
|
||||||
filterId
|
filterId
|
||||||
)
|
)
|
||||||
? 'text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-400/10 border border-sky-200/40 dark:border-sky-500/20'
|
? 'text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-600/10 border border-sky-200/40 dark:border-sky-500/20'
|
||||||
: 'bg-transparent text-gray-600 dark:text-gray-300 '} capitalize"
|
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} capitalize"
|
||||||
>
|
>
|
||||||
{#if filter?.icon}
|
{#if filter?.icon}
|
||||||
<div class="size-4 items-center flex justify-center">
|
<div class="size-4 items-center flex justify-center">
|
||||||
|
@ -1533,10 +1573,10 @@
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
|
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
|
||||||
type="button"
|
type="button"
|
||||||
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {webSearchEnabled ||
|
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {webSearchEnabled ||
|
||||||
($settings?.webSearch ?? false) === 'always'
|
($settings?.webSearch ?? false) === 'always'
|
||||||
? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-400/10 border border-sky-200/40 dark:border-sky-500/20'
|
? ' text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-600/10 border border-sky-200/40 dark:border-sky-500/20'
|
||||||
: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
|
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
|
||||||
>
|
>
|
||||||
<GlobeAlt className="size-4" strokeWidth="1.75" />
|
<GlobeAlt className="size-4" strokeWidth="1.75" />
|
||||||
<div class="hidden group-hover:block">
|
<div class="hidden group-hover:block">
|
||||||
|
@ -1552,9 +1592,9 @@
|
||||||
on:click|preventDefault={() =>
|
on:click|preventDefault={() =>
|
||||||
(imageGenerationEnabled = !imageGenerationEnabled)}
|
(imageGenerationEnabled = !imageGenerationEnabled)}
|
||||||
type="button"
|
type="button"
|
||||||
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {imageGenerationEnabled
|
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {imageGenerationEnabled
|
||||||
? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-400/10 border border-sky-200/40 dark:border-sky-500/20'
|
? ' text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-700/10 border border-sky-200/40 dark:border-sky-500/20'
|
||||||
: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
|
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
|
||||||
>
|
>
|
||||||
<Photo className="size-4" strokeWidth="1.75" />
|
<Photo className="size-4" strokeWidth="1.75" />
|
||||||
<div class="hidden group-hover:block">
|
<div class="hidden group-hover:block">
|
||||||
|
@ -1574,9 +1614,9 @@
|
||||||
on:click|preventDefault={() =>
|
on:click|preventDefault={() =>
|
||||||
(codeInterpreterEnabled = !codeInterpreterEnabled)}
|
(codeInterpreterEnabled = !codeInterpreterEnabled)}
|
||||||
type="button"
|
type="button"
|
||||||
class=" group p-[7px] flex gap-1.5 items-center text-sm transition-colors duration-300 max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {codeInterpreterEnabled
|
class=" group p-[7px] flex gap-1.5 items-center text-sm transition-colors duration-300 max-w-full overflow-hidden {codeInterpreterEnabled
|
||||||
? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-400/10 border border-sky-200/40 dark:border-sky-500/20'
|
? ' text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-700/10 border border-sky-200/40 dark:border-sky-500/20'
|
||||||
: 'bg-transparent text-gray-600 dark:text-gray-300 '} {($settings?.highContrastMode ??
|
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} {($settings?.highContrastMode ??
|
||||||
false)
|
false)
|
||||||
? 'm-1'
|
? 'm-1'
|
||||||
: 'focus:outline-hidden rounded-full'}"
|
: 'focus:outline-hidden rounded-full'}"
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
|
|
||||||
import { config, user, tools as _tools, mobile, settings, toolServers } from '$lib/stores';
|
import { config, user, tools as _tools, mobile, settings, toolServers } from '$lib/stores';
|
||||||
|
|
||||||
|
import { getOAuthClientAuthorizationUrl } from '$lib/apis/configs';
|
||||||
import { getTools } from '$lib/apis/tools';
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
|
||||||
|
import Knobs from '$lib/components/icons/Knobs.svelte';
|
||||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
|
@ -19,9 +21,6 @@
|
||||||
import Terminal from '$lib/components/icons/Terminal.svelte';
|
import Terminal from '$lib/components/icons/Terminal.svelte';
|
||||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||||
import ValvesModal from '$lib/components/workspace/common/ValvesModal.svelte';
|
|
||||||
import { getOAuthClientAuthorizationUrl } from '$lib/apis/configs';
|
|
||||||
import { partition } from 'd3-hierarchy';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
@ -41,16 +40,12 @@
|
||||||
export let showCodeInterpreterButton = false;
|
export let showCodeInterpreterButton = false;
|
||||||
export let codeInterpreterEnabled = false;
|
export let codeInterpreterEnabled = false;
|
||||||
|
|
||||||
|
export let onShowValves: Function;
|
||||||
export let onClose: Function;
|
export let onClose: Function;
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
let tab = '';
|
let tab = '';
|
||||||
|
|
||||||
let showValvesModal = false;
|
|
||||||
|
|
||||||
let selectedValvesType = 'tool';
|
|
||||||
let selectedValvesItemId = null;
|
|
||||||
|
|
||||||
let tools = null;
|
let tools = null;
|
||||||
|
|
||||||
$: if (show) {
|
$: if (show) {
|
||||||
|
@ -96,16 +91,6 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ValvesModal
|
|
||||||
bind:show={showValvesModal}
|
|
||||||
userValves={true}
|
|
||||||
type={selectedValvesType}
|
|
||||||
id={selectedValvesItemId ?? null}
|
|
||||||
on:save={async () => {
|
|
||||||
await tick();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
bind:show
|
bind:show
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
|
@ -192,6 +177,27 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if filter?.has_user_valves}
|
||||||
|
<div class=" shrink-0">
|
||||||
|
<Tooltip content={$i18n.t('Valves')}>
|
||||||
|
<button
|
||||||
|
class="self-center w-fit text-sm text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition rounded-full"
|
||||||
|
type="button"
|
||||||
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onShowValves({
|
||||||
|
type: 'function',
|
||||||
|
id: filter.id
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Knobs />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class=" shrink-0">
|
<div class=" shrink-0">
|
||||||
<Switch
|
<Switch
|
||||||
state={selectedFilterIds.includes(filter.id)}
|
state={selectedFilterIds.includes(filter.id)}
|
||||||
|
@ -340,7 +346,7 @@
|
||||||
>
|
>
|
||||||
{#if !(tools[toolId]?.authenticated ?? true)}
|
{#if !(tools[toolId]?.authenticated ?? true)}
|
||||||
<!-- make it slighly darker and not clickable -->
|
<!-- make it slighly darker and not clickable -->
|
||||||
<div class="absolute inset-0 opacity-50 rounded-xl cursor-not-allowed z-10" />
|
<div class="absolute inset-0 opacity-50 rounded-xl cursor-pointer z-10" />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex-1 truncate">
|
<div class="flex-1 truncate">
|
||||||
<div class="flex flex-1 gap-2 items-center">
|
<div class="flex flex-1 gap-2 items-center">
|
||||||
|
@ -364,30 +370,13 @@
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
selectedValvesType = 'tool';
|
onShowValves({
|
||||||
selectedValvesItemId = toolId;
|
type: 'tool',
|
||||||
showValvesModal = true;
|
id: toolId
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<Knobs />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import CitationModal from './Citations/CitationModal.svelte';
|
import CitationModal from './Citations/CitationModal.svelte';
|
||||||
|
import { embed, showControls, showEmbeds } from '$lib/stores';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
@ -21,8 +22,24 @@
|
||||||
export const showSourceModal = (sourceIdx) => {
|
export const showSourceModal = (sourceIdx) => {
|
||||||
if (citations[sourceIdx]) {
|
if (citations[sourceIdx]) {
|
||||||
console.log('Showing citation modal for:', citations[sourceIdx]);
|
console.log('Showing citation modal for:', citations[sourceIdx]);
|
||||||
selectedCitation = citations[sourceIdx];
|
|
||||||
showCitationModal = true;
|
if (citations[sourceIdx]?.source?.embed_url) {
|
||||||
|
const embedUrl = citations[sourceIdx].source.embed_url;
|
||||||
|
if (embedUrl) {
|
||||||
|
showControls.set(true);
|
||||||
|
showEmbeds.set(true);
|
||||||
|
embed.set({
|
||||||
|
title: citations[sourceIdx]?.source?.name || 'Embedded Content',
|
||||||
|
url: embedUrl
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
selectedCitation = citations[sourceIdx];
|
||||||
|
showCitationModal = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedCitation = citations[sourceIdx];
|
||||||
|
showCitationModal = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
settings,
|
settings,
|
||||||
showArtifacts,
|
showArtifacts,
|
||||||
showControls,
|
showControls,
|
||||||
|
showEmbeds,
|
||||||
showOverview
|
showOverview
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte';
|
import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte';
|
||||||
|
@ -194,6 +195,7 @@
|
||||||
await showControls.set(true);
|
await showControls.set(true);
|
||||||
await showArtifacts.set(true);
|
await showArtifacts.set(true);
|
||||||
await showOverview.set(false);
|
await showOverview.set(false);
|
||||||
|
await showEmbeds.set(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -209,7 +211,7 @@
|
||||||
: (selectedModels ?? []).length > 0
|
: (selectedModels ?? []).length > 0
|
||||||
? selectedModels.at(0)
|
? selectedModels.at(0)
|
||||||
: model?.id}
|
: model?.id}
|
||||||
messages={createMessagesList(history, id)}
|
messages={createMessagesList(history, messageId)}
|
||||||
onAdd={({ modelId, parentId, messages }) => {
|
onAdd={({ modelId, parentId, messages }) => {
|
||||||
console.log(modelId, parentId, messages);
|
console.log(modelId, parentId, messages);
|
||||||
onAddMessages({ modelId, parentId, messages });
|
onAddMessages({ modelId, parentId, messages });
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import RegenerateMenu from './ResponseMessage/RegenerateMenu.svelte';
|
import RegenerateMenu from './ResponseMessage/RegenerateMenu.svelte';
|
||||||
import StatusHistory from './ResponseMessage/StatusHistory.svelte';
|
import StatusHistory from './ResponseMessage/StatusHistory.svelte';
|
||||||
|
import FullHeightIframe from '$lib/components/common/FullHeightIframe.svelte';
|
||||||
|
|
||||||
interface MessageType {
|
interface MessageType {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -676,6 +677,22 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if message?.embeds && message.embeds.length > 0}
|
||||||
|
<div class="my-1 w-full flex overflow-x-auto gap-2 flex-wrap">
|
||||||
|
{#each message.embeds as embed, idx}
|
||||||
|
<div class="my-2 w-full" id={`${message.id}-embeds-${idx}`}>
|
||||||
|
<FullHeightIframe
|
||||||
|
src={embed}
|
||||||
|
allowScripts={true}
|
||||||
|
allowForms={true}
|
||||||
|
allowSameOrigin={true}
|
||||||
|
allowPopups={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if edit === true}
|
{#if edit === true}
|
||||||
<div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
|
<div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
|
||||||
<textarea
|
<textarea
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const editMessageConfirmHandler = async (submit = true) => {
|
const editMessageConfirmHandler = async (submit = true) => {
|
||||||
if (!editedContent && editedFiles.length === 0) {
|
if (!editedContent && (editedFiles ?? []).length === 0) {
|
||||||
toast.error($i18n.t('Please enter a message or attach a file.'));
|
toast.error($i18n.t('Please enter a message or attach a file.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,7 +125,7 @@
|
||||||
|
|
||||||
{#if showSetDefault}
|
{#if showSetDefault}
|
||||||
<div
|
<div
|
||||||
class="absolute text-left mt-[1px] ml-1 text-[0.7rem] text-gray-600 dark:text-gray-400 font-primary"
|
class="relative text-left mt-[1px] ml-1 text-[0.7rem] text-gray-600 dark:text-gray-400 font-primary"
|
||||||
>
|
>
|
||||||
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -435,7 +435,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex gap-1 w-fit text-center text-sm rounded-full bg-transparent px-1.5"
|
class="flex gap-1 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
|
||||||
bind:this={tagsContainerElement}
|
bind:this={tagsContainerElement}
|
||||||
>
|
>
|
||||||
{#if items.find((item) => item.model?.connection_type === 'local') || items.find((item) => item.model?.connection_type === 'external') || items.find((item) => item.model?.direct) || tags.length > 0}
|
{#if items.find((item) => item.model?.connection_type === 'local') || items.find((item) => item.model?.connection_type === 'external') || items.find((item) => item.model?.direct) || tags.length > 0}
|
||||||
|
@ -500,18 +500,20 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each tags as tag}
|
{#each tags as tag}
|
||||||
<button
|
<Tooltip content={tag}>
|
||||||
class="min-w-fit outline-none px-1.5 py-0.5 {selectedTag === tag
|
<button
|
||||||
? ''
|
class="min-w-fit outline-none px-1.5 py-0.5 {selectedTag === tag
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
? ''
|
||||||
aria-pressed={selectedTag === tag}
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||||
on:click={() => {
|
aria-pressed={selectedTag === tag}
|
||||||
selectedConnectionType = '';
|
on:click={() => {
|
||||||
selectedTag = tag;
|
selectedConnectionType = '';
|
||||||
}}
|
selectedTag = tag;
|
||||||
>
|
}}
|
||||||
{tag}
|
>
|
||||||
</button>
|
{tag.length > 16 ? `${tag.slice(0, 16)}...` : tag}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
||||||
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';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
@ -210,7 +211,7 @@
|
||||||
aria-label="Controls"
|
aria-label="Controls"
|
||||||
>
|
>
|
||||||
<div class=" m-auto self-center">
|
<div class=" m-auto self-center">
|
||||||
<AdjustmentsHorizontal className=" size-5" strokeWidth="1" />
|
<Knobs className=" size-5" strokeWidth="1" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -255,7 +256,7 @@
|
||||||
|
|
||||||
<div class="absolute top-[100%] left-0 right-0 h-fit">
|
<div class="absolute top-[100%] left-0 right-0 h-fit">
|
||||||
{#if !history.currentId && !$chatId && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
|
{#if !history.currentId && !$chatId && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
|
||||||
<div class=" w-full z-30 mt-4">
|
<div class=" w-full z-30">
|
||||||
<div class=" flex flex-col gap-1 w-full">
|
<div class=" flex flex-col gap-1 w-full">
|
||||||
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
|
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
|
||||||
<Banner
|
<Banner
|
||||||
|
|
|
@ -160,7 +160,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full h-full relative">
|
<div class="w-full h-full relative">
|
||||||
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3.5">
|
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<button
|
<button
|
||||||
class="self-center p-0.5"
|
class="self-center p-0.5"
|
||||||
|
|
|
@ -1,15 +1,40 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
import ChatList from './ChatList.svelte';
|
import ChatList from './ChatList.svelte';
|
||||||
import FolderKnowledge from './FolderKnowledge.svelte';
|
import FolderKnowledge from './FolderKnowledge.svelte';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import { getChatListByFolderId } from '$lib/apis/chats';
|
||||||
|
|
||||||
export let folder = null;
|
export let folder = null;
|
||||||
|
|
||||||
let selectedTab = 'chats';
|
let selectedTab = 'chats';
|
||||||
|
|
||||||
|
let chats = null;
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
const setChatList = async () => {
|
||||||
|
chats = null;
|
||||||
|
|
||||||
|
if (folder && folder.id) {
|
||||||
|
const res = await getChatListByFolderId(localStorage.token, folder.id, page);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
chats = res;
|
||||||
|
} else {
|
||||||
|
chats = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chats = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (folder) {
|
||||||
|
setChatList();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -45,7 +70,13 @@
|
||||||
{#if selectedTab === 'knowledge'}
|
{#if selectedTab === 'knowledge'}
|
||||||
<FolderKnowledge />
|
<FolderKnowledge />
|
||||||
{:else if selectedTab === 'chats'}
|
{:else if selectedTab === 'chats'}
|
||||||
<ChatList chats={folder?.items?.chats ?? []} />
|
{#if chats !== null}
|
||||||
|
<ChatList {chats} />
|
||||||
|
{:else}
|
||||||
|
<div class="py-10">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -115,7 +115,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if folder}
|
{#if folder}
|
||||||
<FolderModal bind:show={showFolderModal} edit={true} {folder} onSubmit={updateHandler} />
|
<FolderModal
|
||||||
|
bind:show={showFolderModal}
|
||||||
|
edit={true}
|
||||||
|
folderId={folder.id}
|
||||||
|
onSubmit={updateHandler}
|
||||||
|
/>
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
<DeleteConfirmDialog
|
||||||
bind:show={showDeleteConfirm}
|
bind:show={showDeleteConfirm}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
import { getUserById } from '$lib/apis/users';
|
import { getUserById } from '$lib/apis/users';
|
||||||
|
import User from '$lib/components/icons/User.svelte';
|
||||||
|
import UserProfileImage from './Account/UserProfileImage.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
@ -118,68 +120,6 @@
|
||||||
|
|
||||||
<div id="tab-account" class="flex flex-col h-full justify-between text-sm">
|
<div id="tab-account" class="flex flex-col h-full justify-between text-sm">
|
||||||
<div class=" overflow-y-scroll max-h-[28rem] md:max-h-full">
|
<div class=" overflow-y-scroll max-h-[28rem] md:max-h-full">
|
||||||
<input
|
|
||||||
id="profile-image-input"
|
|
||||||
bind:this={profileImageInputElement}
|
|
||||||
type="file"
|
|
||||||
hidden
|
|
||||||
accept="image/*"
|
|
||||||
on:change={(e) => {
|
|
||||||
const files = profileImageInputElement.files ?? [];
|
|
||||||
let reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
let originalImageUrl = `${event.target.result}`;
|
|
||||||
|
|
||||||
const img = new Image();
|
|
||||||
img.src = originalImageUrl;
|
|
||||||
|
|
||||||
img.onload = function () {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
// Calculate the aspect ratio of the image
|
|
||||||
const aspectRatio = img.width / img.height;
|
|
||||||
|
|
||||||
// Calculate the new width and height to fit within 250x250
|
|
||||||
let newWidth, newHeight;
|
|
||||||
if (aspectRatio > 1) {
|
|
||||||
newWidth = 250 * aspectRatio;
|
|
||||||
newHeight = 250;
|
|
||||||
} else {
|
|
||||||
newWidth = 250;
|
|
||||||
newHeight = 250 / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the canvas size
|
|
||||||
canvas.width = 250;
|
|
||||||
canvas.height = 250;
|
|
||||||
|
|
||||||
// Calculate the position to center the image
|
|
||||||
const offsetX = (250 - newWidth) / 2;
|
|
||||||
const offsetY = (250 - newHeight) / 2;
|
|
||||||
|
|
||||||
// Draw the image on the canvas
|
|
||||||
ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
|
|
||||||
|
|
||||||
// Get the base64 representation of the compressed image
|
|
||||||
const compressedSrc = canvas.toDataURL('image/jpeg');
|
|
||||||
|
|
||||||
// Display the compressed image
|
|
||||||
profileImageUrl = compressedSrc;
|
|
||||||
|
|
||||||
profileImageInputElement.files = null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
files.length > 0 &&
|
|
||||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(files[0]['type'])
|
|
||||||
) {
|
|
||||||
reader.readAsDataURL(files[0]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base font-medium">{$i18n.t('Your Account')}</div>
|
<div class="text-base font-medium">{$i18n.t('Your Account')}</div>
|
||||||
|
@ -192,73 +132,8 @@
|
||||||
<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> -->
|
<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> -->
|
||||||
|
|
||||||
<div class="flex space-x-5 my-4">
|
<div class="flex space-x-5 my-4">
|
||||||
<div class="flex flex-col self-start group">
|
<UserProfileImage bind:profileImageUrl user={$user} />
|
||||||
<div class="self-center flex">
|
|
||||||
<button
|
|
||||||
class="relative rounded-full dark:bg-gray-700"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
profileImageInputElement.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)}
|
|
||||||
alt="profile"
|
|
||||||
class=" rounded-full size-14 md:size-18 object-cover"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition">
|
|
||||||
<div class="p-1 rounded-full bg-white text-black border-gray-100 shadow">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="size-3"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col w-full justify-center mt-2">
|
|
||||||
<button
|
|
||||||
class=" text-xs text-center text-gray-500 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
|
||||||
on:click={async () => {
|
|
||||||
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
|
|
||||||
}}>{$i18n.t('Remove')}</button
|
|
||||||
>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
|
||||||
on:click={async () => {
|
|
||||||
if (canvasPixelTest()) {
|
|
||||||
profileImageUrl = generateInitialsImage(name);
|
|
||||||
} else {
|
|
||||||
toast.info(
|
|
||||||
$i18n.t(
|
|
||||||
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
|
|
||||||
),
|
|
||||||
{
|
|
||||||
duration: 1000 * 10
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}>{$i18n.t('Initials')}</button
|
|
||||||
>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
|
||||||
on:click={async () => {
|
|
||||||
const url = await getGravatarUrl(localStorage.token, $user?.email);
|
|
||||||
|
|
||||||
profileImageUrl = url;
|
|
||||||
}}>{$i18n.t('Gravatar')}</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 flex-col">
|
<div class="flex flex-1 flex-col">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { getGravatarUrl } from '$lib/apis/utils';
|
||||||
|
import { canvasPixelTest, generateInitialsImage } from '$lib/utils';
|
||||||
|
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
export let profileImageUrl;
|
||||||
|
export let user = null;
|
||||||
|
|
||||||
|
let profileImageInputElement;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="profile-image-input"
|
||||||
|
bind:this={profileImageInputElement}
|
||||||
|
type="file"
|
||||||
|
hidden
|
||||||
|
accept="image/*"
|
||||||
|
on:change={(e) => {
|
||||||
|
const files = profileImageInputElement.files ?? [];
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
let originalImageUrl = `${event.target.result}`;
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.src = originalImageUrl;
|
||||||
|
|
||||||
|
img.onload = function () {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Calculate the aspect ratio of the image
|
||||||
|
const aspectRatio = img.width / img.height;
|
||||||
|
|
||||||
|
// Calculate the new width and height to fit within 250x250
|
||||||
|
let newWidth, newHeight;
|
||||||
|
if (aspectRatio > 1) {
|
||||||
|
newWidth = 250 * aspectRatio;
|
||||||
|
newHeight = 250;
|
||||||
|
} else {
|
||||||
|
newWidth = 250;
|
||||||
|
newHeight = 250 / aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the canvas size
|
||||||
|
canvas.width = 250;
|
||||||
|
canvas.height = 250;
|
||||||
|
|
||||||
|
// Calculate the position to center the image
|
||||||
|
const offsetX = (250 - newWidth) / 2;
|
||||||
|
const offsetY = (250 - newHeight) / 2;
|
||||||
|
|
||||||
|
// Draw the image on the canvas
|
||||||
|
ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
|
||||||
|
|
||||||
|
// Get the base64 representation of the compressed image
|
||||||
|
const compressedSrc = canvas.toDataURL('image/jpeg');
|
||||||
|
|
||||||
|
// Display the compressed image
|
||||||
|
profileImageUrl = compressedSrc;
|
||||||
|
|
||||||
|
profileImageInputElement.files = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
files.length > 0 &&
|
||||||
|
['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(files[0]['type'])
|
||||||
|
) {
|
||||||
|
reader.readAsDataURL(files[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col self-start group">
|
||||||
|
<div class="self-center flex">
|
||||||
|
<button
|
||||||
|
class="relative rounded-full dark:bg-gray-700"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
profileImageInputElement.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(user?.name)}
|
||||||
|
alt="profile"
|
||||||
|
class=" rounded-full size-14 md:size-18 object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition">
|
||||||
|
<div class="p-1 rounded-full bg-white text-black border-gray-100 shadow">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-3"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-full justify-center mt-2">
|
||||||
|
<button
|
||||||
|
class=" text-xs text-center text-gray-500 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||||
|
type="button"
|
||||||
|
on:click={async () => {
|
||||||
|
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
|
||||||
|
}}>{$i18n.t('Remove')}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||||
|
type="button"
|
||||||
|
on:click={async () => {
|
||||||
|
if (canvasPixelTest()) {
|
||||||
|
profileImageUrl = generateInitialsImage(user?.name);
|
||||||
|
} else {
|
||||||
|
toast.info(
|
||||||
|
$i18n.t(
|
||||||
|
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
duration: 1000 * 10
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}>{$i18n.t('Initials')}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||||
|
type="button"
|
||||||
|
on:click={async () => {
|
||||||
|
const url = await getGravatarUrl(localStorage.token, user?.email);
|
||||||
|
|
||||||
|
profileImageUrl = url;
|
||||||
|
}}>{$i18n.t('Gravatar')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -2,14 +2,22 @@
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { chats, user, settings, scrollPaginationEnabled, currentChatPage } from '$lib/stores';
|
import {
|
||||||
|
chats,
|
||||||
|
user,
|
||||||
|
settings,
|
||||||
|
scrollPaginationEnabled,
|
||||||
|
currentChatPage,
|
||||||
|
pinnedChats
|
||||||
|
} from '$lib/stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
archiveAllChats,
|
archiveAllChats,
|
||||||
deleteAllChats,
|
deleteAllChats,
|
||||||
getAllChats,
|
getAllChats,
|
||||||
getChatList,
|
getChatList,
|
||||||
importChat
|
importChat,
|
||||||
|
getPinnedChatList
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
@ -74,6 +82,7 @@
|
||||||
|
|
||||||
currentChatPage.set(1);
|
currentChatPage.set(1);
|
||||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||||
|
pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||||
scrollPaginationEnabled.set(true);
|
scrollPaginationEnabled.set(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -92,6 +101,7 @@
|
||||||
|
|
||||||
currentChatPage.set(1);
|
currentChatPage.set(1);
|
||||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||||
|
pinnedChats.set([]);
|
||||||
scrollPaginationEnabled.set(true);
|
scrollPaginationEnabled.set(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,7 @@
|
||||||
let chatFadeStreamingText = true;
|
let chatFadeStreamingText = true;
|
||||||
let collapseCodeBlocks = false;
|
let collapseCodeBlocks = false;
|
||||||
let expandDetails = false;
|
let expandDetails = false;
|
||||||
|
let showChatTitleInTab = true;
|
||||||
|
|
||||||
let showFloatingActionButtons = true;
|
let showFloatingActionButtons = true;
|
||||||
let floatingActionButtons = null;
|
let floatingActionButtons = null;
|
||||||
|
@ -224,6 +225,7 @@
|
||||||
temporaryChatByDefault = $settings?.temporaryChatByDefault ?? false;
|
temporaryChatByDefault = $settings?.temporaryChatByDefault ?? false;
|
||||||
chatDirection = $settings?.chatDirection ?? 'auto';
|
chatDirection = $settings?.chatDirection ?? 'auto';
|
||||||
userLocation = $settings?.userLocation ?? false;
|
userLocation = $settings?.userLocation ?? false;
|
||||||
|
showChatTitleInTab = $settings?.showChatTitleInTab ?? true;
|
||||||
|
|
||||||
notificationSound = $settings?.notificationSound ?? true;
|
notificationSound = $settings?.notificationSound ?? true;
|
||||||
notificationSoundAlways = $settings?.notificationSoundAlways ?? false;
|
notificationSoundAlways = $settings?.notificationSoundAlways ?? false;
|
||||||
|
@ -329,6 +331,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div id="use-chat-title-as-tab-title-label" class=" self-center text-xs">
|
||||||
|
{$i18n.t('Display chat title in tab')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 p-1">
|
||||||
|
<Switch
|
||||||
|
ariaLabelledbyId="use-chat-title-as-tab-title-label"
|
||||||
|
tooltip={true}
|
||||||
|
bind:state={showChatTitleInTab}
|
||||||
|
on:change={() => {
|
||||||
|
saveSettings({ showChatTitleInTab });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="py-0.5 flex w-full justify-between">
|
<div class="py-0.5 flex w-full justify-between">
|
||||||
<div id="notification-sound-label" class=" self-center text-xs">
|
<div id="notification-sound-label" class=" self-center text-xs">
|
||||||
|
|
|
@ -46,8 +46,8 @@
|
||||||
<div class=" text-sm dark:text-gray-300 mb-1">
|
<div class=" text-sm dark:text-gray-300 mb-1">
|
||||||
{#each selectedTools as tool}
|
{#each selectedTools as tool}
|
||||||
<Collapsible buttonClassName="w-full mb-0.5">
|
<Collapsible buttonClassName="w-full mb-0.5">
|
||||||
<div>
|
<div class="truncate">
|
||||||
<div class="text-sm font-medium dark:text-gray-100 text-gray-800">
|
<div class="text-sm font-medium dark:text-gray-100 text-gray-800 truncate">
|
||||||
{tool?.name}
|
{tool?.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
export let title = 'Embedded Content';
|
export let title = 'Embedded Content';
|
||||||
export let initialHeight: number | null = null; // initial height in px, null = auto
|
export let initialHeight: number | null = null; // initial height in px, null = auto
|
||||||
|
|
||||||
|
export let iframeClassName = 'w-full rounded-2xl';
|
||||||
|
|
||||||
export let args = null;
|
export let args = null;
|
||||||
|
|
||||||
export let allowScripts = true;
|
export let allowScripts = true;
|
||||||
|
@ -174,7 +176,7 @@ window.Chart = parent.Chart; // Chart previously assigned on parent
|
||||||
bind:this={iframe}
|
bind:this={iframe}
|
||||||
srcdoc={iframeDoc}
|
srcdoc={iframeDoc}
|
||||||
{title}
|
{title}
|
||||||
class="w-full rounded-2xl"
|
class={iframeClassName}
|
||||||
style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
|
style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
|
||||||
width="100%"
|
width="100%"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
|
@ -187,7 +189,7 @@ window.Chart = parent.Chart; // Chart previously assigned on parent
|
||||||
bind:this={iframe}
|
bind:this={iframe}
|
||||||
src={iframeSrc}
|
src={iframeSrc}
|
||||||
{title}
|
{title}
|
||||||
class="w-full rounded-2xl"
|
class={iframeClassName}
|
||||||
style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
|
style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
|
||||||
width="100%"
|
width="100%"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
|
|
|
@ -1030,6 +1030,19 @@
|
||||||
// For all other cases, let ProseMirror perform its default paste behavior.
|
// For all other cases, let ProseMirror perform its default paste behavior.
|
||||||
view.dispatch(view.state.tr.scrollIntoView());
|
view.dispatch(view.state.tr.scrollIntoView());
|
||||||
return false;
|
return false;
|
||||||
|
},
|
||||||
|
copy: (view, event: ClipboardEvent) => {
|
||||||
|
if (!event.clipboardData) return false;
|
||||||
|
if (richText) return false; // Let ProseMirror handle normal copy in rich text mode
|
||||||
|
|
||||||
|
const plain = editor.getText();
|
||||||
|
const html = editor.getHTML();
|
||||||
|
|
||||||
|
event.clipboardData.setData('text/plain', plain.replaceAll('\n\n', '\n'));
|
||||||
|
event.clipboardData.setData('text/html', html);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><path d="M10.25 4.75L6.75 8.25L10.25 11.75" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
></path><path
|
||||||
|
d="M6.75 8.25L12.75 8.25C14.9591 8.25 16.75 10.0409 16.75 12.25V19.25"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
></path></svg
|
||||||
|
>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"
|
||||||
|
/>
|
||||||
|
</svg>
|
|
@ -1,19 +1,26 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { archiveChatById, getAllArchivedChats, getArchivedChatList } from '$lib/apis/chats';
|
import {
|
||||||
|
archiveChatById,
|
||||||
|
getAllArchivedChats,
|
||||||
|
getArchivedChatList,
|
||||||
|
unarchiveAllChats
|
||||||
|
} from '$lib/apis/chats';
|
||||||
|
|
||||||
import ChatsModal from './ChatsModal.svelte';
|
import ChatsModal from './ChatsModal.svelte';
|
||||||
import UnarchiveAllConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import UnarchiveAllConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
|
import Spinner from '../common/Spinner.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let onUpdate = () => {};
|
export let onUpdate = () => {};
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
let chatList = null;
|
let chatList = null;
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
|
||||||
|
@ -105,13 +112,17 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const unarchiveAllHandler = async () => {
|
const unarchiveAllHandler = async () => {
|
||||||
const chats = await getAllArchivedChats(localStorage.token);
|
loading = true;
|
||||||
for (const chat of chats) {
|
try {
|
||||||
await archiveChatById(localStorage.token, chat.id);
|
await unarchiveAllChats(localStorage.token);
|
||||||
|
toast.success($i18n.t('All chats have been unarchived.'));
|
||||||
|
onUpdate();
|
||||||
|
await init();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate();
|
|
||||||
init();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
@ -152,15 +163,21 @@
|
||||||
<div class="flex flex-wrap text-sm font-medium gap-1.5 mt-2 m-1 justify-end w-full">
|
<div class="flex flex-wrap text-sm font-medium gap-1.5 mt-2 m-1 justify-end w-full">
|
||||||
<button
|
<button
|
||||||
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-800 rounded-3xl"
|
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-800 rounded-3xl"
|
||||||
|
disabled={loading}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showUnarchiveAllConfirmDialog = true;
|
showUnarchiveAllConfirmDialog = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Unarchive All Archived Chats')}
|
{#if loading}
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Unarchive All Archived Chats')}
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-800 rounded-3xl"
|
class="px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-800 rounded-3xl"
|
||||||
|
disabled={loading}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
exportChatsHandler();
|
exportChatsHandler();
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
theme,
|
theme,
|
||||||
user,
|
user,
|
||||||
settings,
|
settings,
|
||||||
folders
|
folders,
|
||||||
|
showEmbeds
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { getChatById } from '$lib/apis/chats';
|
import { getChatById } from '$lib/apis/chats';
|
||||||
|
@ -319,6 +320,7 @@
|
||||||
await showControls.set(true);
|
await showControls.set(true);
|
||||||
await showOverview.set(false);
|
await showOverview.set(false);
|
||||||
await showArtifacts.set(false);
|
await showArtifacts.set(false);
|
||||||
|
await showEmbeds.set(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AdjustmentsHorizontal className=" size-4" strokeWidth="1.5" />
|
<AdjustmentsHorizontal className=" size-4" strokeWidth="1.5" />
|
||||||
|
@ -333,6 +335,7 @@
|
||||||
await showControls.set(true);
|
await showControls.set(true);
|
||||||
await showOverview.set(true);
|
await showOverview.set(true);
|
||||||
await showArtifacts.set(false);
|
await showArtifacts.set(false);
|
||||||
|
await showEmbeds.set(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Map className=" size-4" strokeWidth="1.5" />
|
<Map className=" size-4" strokeWidth="1.5" />
|
||||||
|
@ -346,6 +349,7 @@
|
||||||
await showControls.set(true);
|
await showControls.set(true);
|
||||||
await showArtifacts.set(true);
|
await showArtifacts.set(true);
|
||||||
await showOverview.set(false);
|
await showOverview.set(false);
|
||||||
|
await showEmbeds.set(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Cube className=" size-4" strokeWidth="1.5" />
|
<Cube className=" size-4" strokeWidth="1.5" />
|
||||||
|
|
|
@ -80,7 +80,10 @@
|
||||||
let allChatsLoaded = false;
|
let allChatsLoaded = false;
|
||||||
|
|
||||||
let showCreateFolderModal = false;
|
let showCreateFolderModal = false;
|
||||||
|
|
||||||
let folders = {};
|
let folders = {};
|
||||||
|
let folderRegistry = {};
|
||||||
|
|
||||||
let newFolderId = null;
|
let newFolderId = null;
|
||||||
|
|
||||||
const initFolders = async () => {
|
const initFolders = async () => {
|
||||||
|
@ -122,6 +125,13 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
for (const folderId in folders) {
|
||||||
|
if (folders[folderId] && folders[folderId].is_expanded) {
|
||||||
|
folderRegistry[folderId]?.setFolderItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFolder = async ({ name, data }) => {
|
const createFolder = async ({ name, data }) => {
|
||||||
|
@ -922,6 +932,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Folders
|
<Folders
|
||||||
|
bind:folderRegistry
|
||||||
{folders}
|
{folders}
|
||||||
{shiftKey}
|
{shiftKey}
|
||||||
onDelete={(folderId) => {
|
onDelete={(folderId) => {
|
||||||
|
@ -981,6 +992,8 @@
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
folderRegistry[chat.folder_id]?.setFolderItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chat.pinned) {
|
if (chat.pinned) {
|
||||||
|
|
|
@ -51,6 +51,8 @@
|
||||||
export let selected = false;
|
export let selected = false;
|
||||||
export let shiftKey = false;
|
export let shiftKey = false;
|
||||||
|
|
||||||
|
export let onDragEnd = () => {};
|
||||||
|
|
||||||
let chat = null;
|
let chat = null;
|
||||||
|
|
||||||
let mouseOver = false;
|
let mouseOver = false;
|
||||||
|
@ -201,11 +203,13 @@
|
||||||
y = event.clientY;
|
y = event.clientY;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDragEnd = (event) => {
|
const onDragEndHandler = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
itemElement.style.opacity = '1'; // Reset visual cue after drag
|
itemElement.style.opacity = '1'; // Reset visual cue after drag
|
||||||
dragged = false;
|
dragged = false;
|
||||||
|
|
||||||
|
onDragEnd(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClickOutside = (event) => {
|
const onClickOutside = (event) => {
|
||||||
|
@ -225,7 +229,7 @@
|
||||||
// Event listener for when dragging occurs (optional)
|
// Event listener for when dragging occurs (optional)
|
||||||
itemElement.addEventListener('drag', onDrag);
|
itemElement.addEventListener('drag', onDrag);
|
||||||
// Event listener for when dragging ends
|
// Event listener for when dragging ends
|
||||||
itemElement.addEventListener('dragend', onDragEnd);
|
itemElement.addEventListener('dragend', onDragEndHandler);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -235,7 +239,7 @@
|
||||||
|
|
||||||
itemElement.removeEventListener('dragstart', onDragStart);
|
itemElement.removeEventListener('dragstart', onDragStart);
|
||||||
itemElement.removeEventListener('drag', onDrag);
|
itemElement.removeEventListener('drag', onDrag);
|
||||||
itemElement.removeEventListener('dragend', onDragEnd);
|
itemElement.removeEventListener('dragend', onDragEndHandler);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import RecursiveFolder from './RecursiveFolder.svelte';
|
import RecursiveFolder from './RecursiveFolder.svelte';
|
||||||
|
|
||||||
|
export let folderRegistry = {};
|
||||||
|
|
||||||
export let folders = {};
|
export let folders = {};
|
||||||
export let shiftKey = false;
|
export let shiftKey = false;
|
||||||
|
|
||||||
|
@ -18,15 +21,23 @@
|
||||||
sensitivity: 'base'
|
sensitivity: 'base'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onItemMove = (e) => {
|
||||||
|
if (e.originFolderId) {
|
||||||
|
folderRegistry[e.originFolderId]?.setFolderItems();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each folderList as folderId (folderId)}
|
{#each folderList as folderId (folderId)}
|
||||||
<RecursiveFolder
|
<RecursiveFolder
|
||||||
className=""
|
className=""
|
||||||
|
bind:folderRegistry
|
||||||
{folders}
|
{folders}
|
||||||
{folderId}
|
{folderId}
|
||||||
{shiftKey}
|
{shiftKey}
|
||||||
{onDelete}
|
{onDelete}
|
||||||
|
{onItemMove}
|
||||||
on:import={(e) => {
|
on:import={(e) => {
|
||||||
dispatch('import', e.detail);
|
dispatch('import', e.detail);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -12,15 +12,16 @@
|
||||||
|
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
import Knowledge from '$lib/components/workspace/Models/Knowledge.svelte';
|
import Knowledge from '$lib/components/workspace/Models/Knowledge.svelte';
|
||||||
|
import { getFolderById } from '$lib/apis/folders';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let onSubmit: Function = (e) => {};
|
export let onSubmit: Function = (e) => {};
|
||||||
|
|
||||||
|
export let folderId = null;
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
|
|
||||||
export let folder = null;
|
let folder = null;
|
||||||
|
|
||||||
let name = '';
|
let name = '';
|
||||||
let meta = {
|
let meta = {
|
||||||
background_image_url: null
|
background_image_url: null
|
||||||
|
@ -50,17 +51,24 @@
|
||||||
loading = false;
|
loading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = () => {
|
const init = async () => {
|
||||||
name = folder.name;
|
if (folderId) {
|
||||||
meta = folder.meta || {
|
folder = await getFolderById(localStorage.token, folderId).catch((error) => {
|
||||||
background_image_url: null
|
toast.error(`${error}`);
|
||||||
};
|
return null;
|
||||||
data = folder.data || {
|
});
|
||||||
system_prompt: '',
|
|
||||||
files: []
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(folder);
|
name = folder.name;
|
||||||
|
meta = folder.meta || {
|
||||||
|
background_image_url: null
|
||||||
|
};
|
||||||
|
data = folder.data || {
|
||||||
|
system_prompt: '',
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
focusInput();
|
||||||
};
|
};
|
||||||
|
|
||||||
const focusInput = async () => {
|
const focusInput = async () => {
|
||||||
|
@ -73,10 +81,6 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (show) {
|
$: if (show) {
|
||||||
focusInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (folder) {
|
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import { chatId, mobile, selectedFolder, showSidebar } from '$lib/stores';
|
import { chatId, mobile, selectedFolder, showSidebar } from '$lib/stores';
|
||||||
|
@ -16,11 +17,13 @@
|
||||||
deleteFolderById,
|
deleteFolderById,
|
||||||
updateFolderIsExpandedById,
|
updateFolderIsExpandedById,
|
||||||
updateFolderById,
|
updateFolderById,
|
||||||
updateFolderParentIdById
|
updateFolderParentIdById,
|
||||||
|
getFolderById
|
||||||
} from '$lib/apis/folders';
|
} from '$lib/apis/folders';
|
||||||
import {
|
import {
|
||||||
getChatById,
|
getChatById,
|
||||||
getChatsByFolderId,
|
getChatsByFolderId,
|
||||||
|
getChatListByFolderId,
|
||||||
importChat,
|
importChat,
|
||||||
updateChatFolderIdById
|
updateChatFolderIdById
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
|
@ -37,9 +40,10 @@
|
||||||
import FolderMenu from './Folders/FolderMenu.svelte';
|
import FolderMenu from './Folders/FolderMenu.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import FolderModal from './Folders/FolderModal.svelte';
|
import FolderModal from './Folders/FolderModal.svelte';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import Emoji from '$lib/components/common/Emoji.svelte';
|
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
|
||||||
|
export let folderRegistry = {};
|
||||||
export let open = false;
|
export let open = false;
|
||||||
|
|
||||||
export let folders;
|
export let folders;
|
||||||
|
@ -51,6 +55,7 @@
|
||||||
export let parentDragged = false;
|
export let parentDragged = false;
|
||||||
|
|
||||||
export let onDelete = (e) => {};
|
export let onDelete = (e) => {};
|
||||||
|
export let onItemMove = (e) => {};
|
||||||
|
|
||||||
let folderElement;
|
let folderElement;
|
||||||
|
|
||||||
|
@ -171,6 +176,12 @@
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onItemMove({
|
||||||
|
originFolderId: chat.folder_id,
|
||||||
|
targetFolderId: folderId,
|
||||||
|
e
|
||||||
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
dispatch('update');
|
dispatch('update');
|
||||||
}
|
}
|
||||||
|
@ -182,6 +193,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFolderItems();
|
||||||
draggedOver = false;
|
draggedOver = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -234,6 +246,10 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
folderRegistry[folderId] = {
|
||||||
|
setFolderItems: () => setFolderItems()
|
||||||
|
};
|
||||||
|
|
||||||
open = folders[folderId].is_expanded;
|
open = folders[folderId].is_expanded;
|
||||||
if (folderElement) {
|
if (folderElement) {
|
||||||
folderElement.addEventListener('dragover', onDragOver);
|
folderElement.addEventListener('dragover', onDragOver);
|
||||||
|
@ -250,7 +266,6 @@
|
||||||
|
|
||||||
if (folders[folderId]?.new) {
|
if (folders[folderId]?.new) {
|
||||||
delete folders[folderId].new;
|
delete folders[folderId].new;
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
renameHandler();
|
renameHandler();
|
||||||
}
|
}
|
||||||
|
@ -314,9 +329,15 @@
|
||||||
toast.success($i18n.t('Folder updated successfully'));
|
toast.success($i18n.t('Folder updated successfully'));
|
||||||
|
|
||||||
if ($selectedFolder?.id === folderId) {
|
if ($selectedFolder?.id === folderId) {
|
||||||
selectedFolder.set(folders[folderId]);
|
const folder = await getFolderById(localStorage.token, folderId).catch((error) => {
|
||||||
}
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folder) {
|
||||||
|
selectedFolder.set(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
dispatch('update');
|
dispatch('update');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -339,6 +360,32 @@
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let chats = null;
|
||||||
|
export const setFolderItems = async () => {
|
||||||
|
await tick();
|
||||||
|
if (open) {
|
||||||
|
chats = await getChatListByFolderId(localStorage.token, folderId).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($selectedFolder?.id === folderId) {
|
||||||
|
const folder = await getFolderById(localStorage.token, folderId).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folder) {
|
||||||
|
selectedFolder.set(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chats = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: setFolderItems(open);
|
||||||
|
|
||||||
const renameHandler = async () => {
|
const renameHandler = async () => {
|
||||||
console.log('Edit');
|
console.log('Edit');
|
||||||
await tick();
|
await tick();
|
||||||
|
@ -388,12 +435,7 @@
|
||||||
</div>
|
</div>
|
||||||
</DeleteConfirmDialog>
|
</DeleteConfirmDialog>
|
||||||
|
|
||||||
<FolderModal
|
<FolderModal bind:show={showFolderModal} edit={true} {folderId} onSubmit={updateHandler} />
|
||||||
bind:show={showFolderModal}
|
|
||||||
edit={true}
|
|
||||||
folder={folders[folderId]}
|
|
||||||
onSubmit={updateHandler}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if dragged && x && y}
|
{#if dragged && x && y}
|
||||||
<DragGhost {x} {y}>
|
<DragGhost {x} {y}>
|
||||||
|
@ -419,8 +461,6 @@
|
||||||
bind:open
|
bind:open
|
||||||
className="w-full"
|
className="w-full"
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
hide={(folders[folderId]?.childrenIds ?? []).length === 0 &&
|
|
||||||
(folders[folderId].items?.chats ?? []).length === 0}
|
|
||||||
onChange={(state) => {
|
onChange={(state) => {
|
||||||
dispatch('open', state);
|
dispatch('open', state);
|
||||||
}}
|
}}
|
||||||
|
@ -450,7 +490,14 @@
|
||||||
clickTimer = setTimeout(async () => {
|
clickTimer = setTimeout(async () => {
|
||||||
await goto('/');
|
await goto('/');
|
||||||
|
|
||||||
selectedFolder.set(folders[folderId]);
|
const folder = await getFolderById(localStorage.token, folderId).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folder) {
|
||||||
|
selectedFolder.set(folder);
|
||||||
|
}
|
||||||
|
|
||||||
if ($mobile) {
|
if ($mobile) {
|
||||||
showSidebar.set(!$showSidebar);
|
showSidebar.set(!$showSidebar);
|
||||||
|
@ -466,6 +513,7 @@
|
||||||
class="text-gray-500 dark:text-gray-500 transition-all p-1 hover:bg-gray-200 dark:hover:bg-gray-850 rounded-lg"
|
class="text-gray-500 dark:text-gray-500 transition-all p-1 hover:bg-gray-200 dark:hover:bg-gray-850 rounded-lg"
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
open = !open;
|
open = !open;
|
||||||
isExpandedUpdateDebounceHandler();
|
isExpandedUpdateDebounceHandler();
|
||||||
}}
|
}}
|
||||||
|
@ -548,7 +596,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div slot="content" class="w-full">
|
<div slot="content" class="w-full">
|
||||||
{#if (folders[folderId]?.childrenIds ?? []).length > 0 || (folders[folderId].items?.chats ?? []).length > 0}
|
{#if (folders[folderId]?.childrenIds ?? []).length > 0 || (chats ?? []).length > 0}
|
||||||
<div
|
<div
|
||||||
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
|
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
|
||||||
>
|
>
|
||||||
|
@ -564,10 +612,12 @@
|
||||||
|
|
||||||
{#each children as childFolder (`${folderId}-${childFolder.id}`)}
|
{#each children as childFolder (`${folderId}-${childFolder.id}`)}
|
||||||
<svelte:self
|
<svelte:self
|
||||||
|
bind:folderRegistry
|
||||||
{folders}
|
{folders}
|
||||||
folderId={childFolder.id}
|
folderId={childFolder.id}
|
||||||
{shiftKey}
|
{shiftKey}
|
||||||
parentDragged={dragged}
|
parentDragged={dragged}
|
||||||
|
{onItemMove}
|
||||||
{onDelete}
|
{onDelete}
|
||||||
on:import={(e) => {
|
on:import={(e) => {
|
||||||
dispatch('import', e.detail);
|
dispatch('import', e.detail);
|
||||||
|
@ -582,18 +632,22 @@
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if folders[folderId].items?.chats}
|
{#each chats ?? [] as chat (chat.id)}
|
||||||
{#each folders[folderId].items.chats as chat (chat.id)}
|
<ChatItem
|
||||||
<ChatItem
|
id={chat.id}
|
||||||
id={chat.id}
|
title={chat.title}
|
||||||
title={chat.title}
|
{shiftKey}
|
||||||
{shiftKey}
|
on:change={(e) => {
|
||||||
on:change={(e) => {
|
dispatch('change', e.detail);
|
||||||
dispatch('change', e.detail);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if chats === null}
|
||||||
|
<div class="flex justify-center items-center p-2">
|
||||||
|
<Spinner className="size-4 text-gray-500" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -340,7 +340,7 @@
|
||||||
showDeleteConfirm = false;
|
showDeleteConfirm = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" text-sm text-gray-500">
|
<div class=" text-sm text-gray-500 truncate">
|
||||||
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedNote.title}</span>.
|
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedNote.title}</span>.
|
||||||
</div>
|
</div>
|
||||||
</DeleteConfirmDialog>
|
</DeleteConfirmDialog>
|
||||||
|
|
|
@ -296,7 +296,7 @@
|
||||||
<div
|
<div
|
||||||
class="flex justify-between flex-col sm:flex-row items-start sm:items-center gap-2 mt-2"
|
class="flex justify-between flex-col sm:flex-row items-start sm:items-center gap-2 mt-2"
|
||||||
>
|
>
|
||||||
<div class="flex-1 shrink-0">
|
<div class="shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="px-3.5 py-1.5 text-sm font-medium bg-gray-50 hover:bg-gray-100 text-gray-900 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition rounded-lg shrink-0 {($settings?.highContrastMode ??
|
class="px-3.5 py-1.5 text-sm font-medium bg-gray-50 hover:bg-gray-100 text-gray-900 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition rounded-lg shrink-0 {($settings?.highContrastMode ??
|
||||||
|
|
|
@ -305,16 +305,18 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#each tags as tag}
|
{#each tags as tag}
|
||||||
<button
|
<Tooltip content={tag}>
|
||||||
class="min-w-fit outline-none p-1.5 {selectedTag === tag
|
<button
|
||||||
? ''
|
class="min-w-fit outline-none p-1.5 {selectedTag === tag
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
? ''
|
||||||
on:click={() => {
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||||
selectedTag = tag;
|
on:click={() => {
|
||||||
}}
|
selectedTag = tag;
|
||||||
>
|
}}
|
||||||
{tag}
|
>
|
||||||
</button>
|
{tag.length > 32 ? `${tag.slice(0, 32)}...` : tag}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
deleteHandler(deletePrompt);
|
deleteHandler(deletePrompt);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" text-sm text-gray-500">
|
<div class=" text-sm text-gray-500 truncate">
|
||||||
{$i18n.t('This will delete')} <span class=" font-semibold">{deletePrompt.command}</span>.
|
{$i18n.t('This will delete')} <span class=" font-semibold">{deletePrompt.command}</span>.
|
||||||
</div>
|
</div>
|
||||||
</DeleteConfirmDialog>
|
</DeleteConfirmDialog>
|
||||||
|
|
|
@ -522,7 +522,7 @@
|
||||||
deleteHandler(selectedTool);
|
deleteHandler(selectedTool);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" text-sm text-gray-500">
|
<div class=" text-sm text-gray-500 truncate">
|
||||||
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedTool.name}</span>.
|
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedTool.name}</span>.
|
||||||
</div>
|
</div>
|
||||||
</DeleteConfirmDialog>
|
</DeleteConfirmDialog>
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"Advanced Params": "المعلمات المتقدمة",
|
"Advanced Params": "المعلمات المتقدمة",
|
||||||
"AI": "",
|
"AI": "",
|
||||||
"All": "",
|
"All": "",
|
||||||
|
"All chats have been unarchived.": "",
|
||||||
"All Documents": "جميع الملفات",
|
"All Documents": "جميع الملفات",
|
||||||
"All models deleted successfully": "",
|
"All models deleted successfully": "",
|
||||||
"Allow Call": "",
|
"Allow Call": "",
|
||||||
|
@ -430,6 +431,7 @@
|
||||||
"Discover, download, and explore custom tools": "",
|
"Discover, download, and explore custom tools": "",
|
||||||
"Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج",
|
"Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج",
|
||||||
"Display": "",
|
"Display": "",
|
||||||
|
"Display chat title in tab": "",
|
||||||
"Display Emoji in Call": "",
|
"Display Emoji in Call": "",
|
||||||
"Display Multi-model Responses in Tabs": "",
|
"Display Multi-model Responses in Tabs": "",
|
||||||
"Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة",
|
"Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة",
|
||||||
|
@ -691,6 +693,7 @@
|
||||||
"Failed to extract content from the file.": "",
|
"Failed to extract content from the file.": "",
|
||||||
"Failed to fetch models": "",
|
"Failed to fetch models": "",
|
||||||
"Failed to generate title": "",
|
"Failed to generate title": "",
|
||||||
|
"Failed to import models": "",
|
||||||
"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": "",
|
||||||
|
@ -844,6 +847,7 @@
|
||||||
"Import Presets": "",
|
"Import Presets": "",
|
||||||
"Import Prompt Suggestions": "",
|
"Import Prompt Suggestions": "",
|
||||||
"Import Prompts": "مطالبات الاستيراد",
|
"Import Prompts": "مطالبات الاستيراد",
|
||||||
|
"Import successful": "",
|
||||||
"Import Tools": "",
|
"Import Tools": "",
|
||||||
"Important Update": "تحديث مهم",
|
"Important Update": "تحديث مهم",
|
||||||
"In order to force OCR, performing OCR must be enabled.": "",
|
"In order to force OCR, performing OCR must be enabled.": "",
|
||||||
|
@ -881,6 +885,7 @@
|
||||||
"join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.",
|
"join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.",
|
||||||
"JSON": "JSON",
|
"JSON": "JSON",
|
||||||
"JSON Preview": "معاينة JSON",
|
"JSON Preview": "معاينة JSON",
|
||||||
|
"JSON Spec": "",
|
||||||
"July": "يوليو",
|
"July": "يوليو",
|
||||||
"June": "يونيو",
|
"June": "يونيو",
|
||||||
"Jupyter Auth": "",
|
"Jupyter Auth": "",
|
||||||
|
@ -1014,6 +1019,7 @@
|
||||||
"Models": "الموديلات",
|
"Models": "الموديلات",
|
||||||
"Models Access": "",
|
"Models Access": "",
|
||||||
"Models configuration saved successfully": "",
|
"Models configuration saved successfully": "",
|
||||||
|
"Models imported successfully": "",
|
||||||
"Models Public Sharing": "",
|
"Models Public Sharing": "",
|
||||||
"Mojeek Search API Key": "",
|
"Mojeek Search API Key": "",
|
||||||
"More": "المزيد",
|
"More": "المزيد",
|
||||||
|
@ -1073,6 +1079,7 @@
|
||||||
"Note deleted successfully": "",
|
"Note deleted successfully": "",
|
||||||
"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": "",
|
||||||
"Notification Sound": "",
|
"Notification Sound": "",
|
||||||
"Notification Webhook": "",
|
"Notification Webhook": "",
|
||||||
"Notifications": "إشعارات",
|
"Notifications": "إشعارات",
|
||||||
|
@ -1122,6 +1129,7 @@
|
||||||
"OpenAI API settings updated": "",
|
"OpenAI API settings updated": "",
|
||||||
"OpenAI URL/Key required.": "URL/مفتاح OpenAI.مطلوب عنوان ",
|
"OpenAI URL/Key required.": "URL/مفتاح OpenAI.مطلوب عنوان ",
|
||||||
"OpenAPI": "",
|
"OpenAPI": "",
|
||||||
|
"OpenAPI Spec": "",
|
||||||
"openapi.json URL or Path": "",
|
"openapi.json URL or Path": "",
|
||||||
"Optional": "",
|
"Optional": "",
|
||||||
"Options for running a local vision-language model in the picture description. The parameters refer to a model hosted on Hugging Face. This parameter is mutually exclusive with picture_description_api.": "",
|
"Options for running a local vision-language model in the picture description. The parameters refer to a model hosted on Hugging Face. This parameter is mutually exclusive with picture_description_api.": "",
|
||||||
|
@ -1179,6 +1187,7 @@
|
||||||
"Please enter a message or attach a file.": "",
|
"Please enter a message or attach a file.": "",
|
||||||
"Please enter a prompt": "",
|
"Please enter a prompt": "",
|
||||||
"Please enter a valid ID": "",
|
"Please enter a valid ID": "",
|
||||||
|
"Please enter a valid JSON spec": "",
|
||||||
"Please enter a valid path": "",
|
"Please enter a valid path": "",
|
||||||
"Please enter a valid URL": "",
|
"Please enter a valid URL": "",
|
||||||
"Please enter a valid URL.": "",
|
"Please enter a valid URL.": "",
|
||||||
|
@ -1188,6 +1197,7 @@
|
||||||
"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": "",
|
||||||
|
"Please select a valid JSON file": "",
|
||||||
"Please wait until all files are uploaded.": "",
|
"Please wait until all files are uploaded.": "",
|
||||||
"Port": "",
|
"Port": "",
|
||||||
"Positive attitude": "موقف ايجابي",
|
"Positive attitude": "موقف ايجابي",
|
||||||
|
@ -1258,8 +1268,10 @@
|
||||||
"Remove this tag from list": "",
|
"Remove this tag from list": "",
|
||||||
"Rename": "إعادة تسمية",
|
"Rename": "إعادة تسمية",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
|
"Reply": "",
|
||||||
"Reply in Thread": "",
|
"Reply in Thread": "",
|
||||||
"Reply to thread...": "",
|
"Reply to thread...": "",
|
||||||
|
"Replying to {{NAME}}": "",
|
||||||
"required": "",
|
"required": "",
|
||||||
"Reranking Engine": "",
|
"Reranking Engine": "",
|
||||||
"Reranking Model": "إعادة تقييم النموذج",
|
"Reranking Model": "إعادة تقييم النموذج",
|
||||||
|
@ -1517,6 +1529,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 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)": "",
|
"This option controls how long the model will stay loaded into memory following the request (default: 5m)": "",
|
||||||
|
@ -1595,6 +1608,7 @@
|
||||||
"Unarchive Chat": "",
|
"Unarchive Chat": "",
|
||||||
"Underline": "",
|
"Underline": "",
|
||||||
"Unknown": "",
|
"Unknown": "",
|
||||||
|
"Unknown User": "",
|
||||||
"Unloads {{FROM_NOW}}": "",
|
"Unloads {{FROM_NOW}}": "",
|
||||||
"Unlock mysteries": "",
|
"Unlock mysteries": "",
|
||||||
"Unpin": "",
|
"Unpin": "",
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"Advanced Params": "المعلمات المتقدمة",
|
"Advanced Params": "المعلمات المتقدمة",
|
||||||
"AI": "",
|
"AI": "",
|
||||||
"All": "الكل",
|
"All": "الكل",
|
||||||
|
"All chats have been unarchived.": "",
|
||||||
"All Documents": "جميع المستندات",
|
"All Documents": "جميع المستندات",
|
||||||
"All models deleted successfully": "تم حذف جميع النماذج بنجاح",
|
"All models deleted successfully": "تم حذف جميع النماذج بنجاح",
|
||||||
"Allow Call": "",
|
"Allow Call": "",
|
||||||
|
@ -430,6 +431,7 @@
|
||||||
"Discover, download, and explore custom tools": "اكتشف، حمّل، واستعرض الأدوات المخصصة",
|
"Discover, download, and explore custom tools": "اكتشف، حمّل، واستعرض الأدوات المخصصة",
|
||||||
"Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج",
|
"Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج",
|
||||||
"Display": "العرض",
|
"Display": "العرض",
|
||||||
|
"Display chat title in tab": "",
|
||||||
"Display Emoji in Call": "عرض الرموز التعبيرية أثناء المكالمة",
|
"Display Emoji in Call": "عرض الرموز التعبيرية أثناء المكالمة",
|
||||||
"Display Multi-model Responses in Tabs": "",
|
"Display Multi-model Responses in Tabs": "",
|
||||||
"Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة",
|
"Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة",
|
||||||
|
@ -691,6 +693,7 @@
|
||||||
"Failed to extract content from the file.": "",
|
"Failed to extract content from the file.": "",
|
||||||
"Failed to fetch models": "فشل في جلب النماذج",
|
"Failed to fetch models": "فشل في جلب النماذج",
|
||||||
"Failed to generate title": "",
|
"Failed to generate title": "",
|
||||||
|
"Failed to import models": "",
|
||||||
"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": "",
|
||||||
|
@ -844,6 +847,7 @@
|
||||||
"Import Presets": "استيراد الإعدادات المسبقة",
|
"Import Presets": "استيراد الإعدادات المسبقة",
|
||||||
"Import Prompt Suggestions": "",
|
"Import Prompt Suggestions": "",
|
||||||
"Import Prompts": "مطالبات الاستيراد",
|
"Import Prompts": "مطالبات الاستيراد",
|
||||||
|
"Import successful": "",
|
||||||
"Import Tools": "استيراد الأدوات",
|
"Import Tools": "استيراد الأدوات",
|
||||||
"Important Update": "تحديث مهم",
|
"Important Update": "تحديث مهم",
|
||||||
"In order to force OCR, performing OCR must be enabled.": "",
|
"In order to force OCR, performing OCR must be enabled.": "",
|
||||||
|
@ -881,6 +885,7 @@
|
||||||
"join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.",
|
"join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.",
|
||||||
"JSON": "JSON",
|
"JSON": "JSON",
|
||||||
"JSON Preview": "معاينة JSON",
|
"JSON Preview": "معاينة JSON",
|
||||||
|
"JSON Spec": "",
|
||||||
"July": "يوليو",
|
"July": "يوليو",
|
||||||
"June": "يونيو",
|
"June": "يونيو",
|
||||||
"Jupyter Auth": "مصادقة Jupyter",
|
"Jupyter Auth": "مصادقة Jupyter",
|
||||||
|
@ -1014,6 +1019,7 @@
|
||||||
"Models": "الموديلات",
|
"Models": "الموديلات",
|
||||||
"Models Access": "الوصول إلى النماذج",
|
"Models Access": "الوصول إلى النماذج",
|
||||||
"Models configuration saved successfully": "تم حفظ إعدادات النماذج بنجاح",
|
"Models configuration saved successfully": "تم حفظ إعدادات النماذج بنجاح",
|
||||||
|
"Models imported successfully": "",
|
||||||
"Models Public Sharing": "",
|
"Models Public Sharing": "",
|
||||||
"Mojeek Search API Key": "مفتاح API لـ Mojeek Search",
|
"Mojeek Search API Key": "مفتاح API لـ Mojeek Search",
|
||||||
"More": "المزيد",
|
"More": "المزيد",
|
||||||
|
@ -1073,6 +1079,7 @@
|
||||||
"Note deleted successfully": "",
|
"Note deleted successfully": "",
|
||||||
"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": "",
|
||||||
"Notification Sound": "صوت الإشعارات",
|
"Notification Sound": "صوت الإشعارات",
|
||||||
"Notification Webhook": "رابط Webhook للإشعارات",
|
"Notification Webhook": "رابط Webhook للإشعارات",
|
||||||
"Notifications": "إشعارات",
|
"Notifications": "إشعارات",
|
||||||
|
@ -1122,6 +1129,7 @@
|
||||||
"OpenAI API settings updated": "تم تحديث إعدادات OpenAI API",
|
"OpenAI API settings updated": "تم تحديث إعدادات OpenAI API",
|
||||||
"OpenAI URL/Key required.": "URL/مفتاح OpenAI.مطلوب عنوان ",
|
"OpenAI URL/Key required.": "URL/مفتاح OpenAI.مطلوب عنوان ",
|
||||||
"OpenAPI": "",
|
"OpenAPI": "",
|
||||||
|
"OpenAPI Spec": "",
|
||||||
"openapi.json URL or Path": "",
|
"openapi.json URL or Path": "",
|
||||||
"Optional": "",
|
"Optional": "",
|
||||||
"Options for running a local vision-language model in the picture description. The parameters refer to a model hosted on Hugging Face. This parameter is mutually exclusive with picture_description_api.": "",
|
"Options for running a local vision-language model in the picture description. The parameters refer to a model hosted on Hugging Face. This parameter is mutually exclusive with picture_description_api.": "",
|
||||||
|
@ -1179,6 +1187,7 @@
|
||||||
"Please enter a message or attach a file.": "",
|
"Please enter a message or attach a file.": "",
|
||||||
"Please enter a prompt": "الرجاء إدخال توجيه",
|
"Please enter a prompt": "الرجاء إدخال توجيه",
|
||||||
"Please enter a valid ID": "",
|
"Please enter a valid ID": "",
|
||||||
|
"Please enter a valid JSON spec": "",
|
||||||
"Please enter a valid path": "",
|
"Please enter a valid path": "",
|
||||||
"Please enter a valid URL": "",
|
"Please enter a valid URL": "",
|
||||||
"Please enter a valid URL.": "",
|
"Please enter a valid URL.": "",
|
||||||
|
@ -1188,6 +1197,7 @@
|
||||||
"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": "الرجاء اختيار سبب",
|
||||||
|
"Please select a valid JSON file": "",
|
||||||
"Please wait until all files are uploaded.": "",
|
"Please wait until all files are uploaded.": "",
|
||||||
"Port": "المنفذ",
|
"Port": "المنفذ",
|
||||||
"Positive attitude": "موقف ايجابي",
|
"Positive attitude": "موقف ايجابي",
|
||||||
|
@ -1258,8 +1268,10 @@
|
||||||
"Remove this tag from list": "",
|
"Remove this tag from list": "",
|
||||||
"Rename": "إعادة تسمية",
|
"Rename": "إعادة تسمية",
|
||||||
"Reorder Models": "إعادة ترتيب النماذج",
|
"Reorder Models": "إعادة ترتيب النماذج",
|
||||||
|
"Reply": "",
|
||||||
"Reply in Thread": "الرد داخل سلسلة الرسائل",
|
"Reply in Thread": "الرد داخل سلسلة الرسائل",
|
||||||
"Reply to thread...": "",
|
"Reply to thread...": "",
|
||||||
|
"Replying to {{NAME}}": "",
|
||||||
"required": "",
|
"required": "",
|
||||||
"Reranking Engine": "",
|
"Reranking Engine": "",
|
||||||
"Reranking Model": "إعادة تقييم النموذج",
|
"Reranking Model": "إعادة تقييم النموذج",
|
||||||
|
@ -1517,6 +1529,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 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)": "",
|
"This option controls how long the model will stay loaded into memory following the request (default: 5m)": "",
|
||||||
|
@ -1595,6 +1608,7 @@
|
||||||
"Unarchive Chat": "إلغاء أرشفة المحادثة",
|
"Unarchive Chat": "إلغاء أرشفة المحادثة",
|
||||||
"Underline": "",
|
"Underline": "",
|
||||||
"Unknown": "",
|
"Unknown": "",
|
||||||
|
"Unknown User": "",
|
||||||
"Unloads {{FROM_NOW}}": "",
|
"Unloads {{FROM_NOW}}": "",
|
||||||
"Unlock mysteries": "اكشف الأسرار",
|
"Unlock mysteries": "اكشف الأسرار",
|
||||||
"Unpin": "إزالة التثبيت",
|
"Unpin": "إزالة التثبيت",
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"Advanced Params": "Разширени параметри",
|
"Advanced Params": "Разширени параметри",
|
||||||
"AI": "",
|
"AI": "",
|
||||||
"All": "",
|
"All": "",
|
||||||
|
"All chats have been unarchived.": "",
|
||||||
"All Documents": "Всички Документи",
|
"All Documents": "Всички Документи",
|
||||||
"All models deleted successfully": "Всички модели са изтрити успешно",
|
"All models deleted successfully": "Всички модели са изтрити успешно",
|
||||||
"Allow Call": "",
|
"Allow Call": "",
|
||||||
|
@ -430,6 +431,7 @@
|
||||||
"Discover, download, and explore custom tools": "Открийте, изтеглете и разгледайте персонализирани инструменти",
|
"Discover, download, and explore custom tools": "Открийте, изтеглете и разгледайте персонализирани инструменти",
|
||||||
"Discover, download, and explore model presets": "Откриване, сваляне и преглед на пресетове на модели",
|
"Discover, download, and explore model presets": "Откриване, сваляне и преглед на пресетове на модели",
|
||||||
"Display": "Показване",
|
"Display": "Показване",
|
||||||
|
"Display chat title in tab": "",
|
||||||
"Display Emoji in Call": "Показване на емотикони в обаждането",
|
"Display Emoji in Call": "Показване на емотикони в обаждането",
|
||||||
"Display Multi-model Responses in Tabs": "",
|
"Display Multi-model Responses in Tabs": "",
|
||||||
"Display the username instead of You in the Chat": "Показване на потребителското име вместо Вие в чата",
|
"Display the username instead of You in the Chat": "Показване на потребителското име вместо Вие в чата",
|
||||||
|
@ -691,6 +693,7 @@
|
||||||
"Failed to extract content from the file.": "",
|
"Failed to extract content from the file.": "",
|
||||||
"Failed to fetch models": "Неуспешно извличане на модели",
|
"Failed to fetch models": "Неуспешно извличане на модели",
|
||||||
"Failed to generate title": "",
|
"Failed to generate title": "",
|
||||||
|
"Failed to import models": "",
|
||||||
"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": "",
|
||||||
|
@ -844,6 +847,7 @@
|
||||||
"Import Presets": "Импортиране на предварителни настройки",
|
"Import Presets": "Импортиране на предварителни настройки",
|
||||||
"Import Prompt Suggestions": "",
|
"Import Prompt Suggestions": "",
|
||||||
"Import Prompts": "Импортване на промптове",
|
"Import Prompts": "Импортване на промптове",
|
||||||
|
"Import successful": "",
|
||||||
"Import Tools": "Импортиране на инструменти",
|
"Import Tools": "Импортиране на инструменти",
|
||||||
"Important Update": "Важна актуализация",
|
"Important Update": "Важна актуализация",
|
||||||
"In order to force OCR, performing OCR must be enabled.": "",
|
"In order to force OCR, performing OCR must be enabled.": "",
|
||||||
|
@ -881,6 +885,7 @@
|
||||||
"join our Discord for help.": "свържете се с нашия Discord за помощ.",
|
"join our Discord for help.": "свържете се с нашия Discord за помощ.",
|
||||||
"JSON": "JSON",
|
"JSON": "JSON",
|
||||||
"JSON Preview": "JSON Преглед",
|
"JSON Preview": "JSON Преглед",
|
||||||
|
"JSON Spec": "",
|
||||||
"July": "Юли",
|
"July": "Юли",
|
||||||
"June": "Юни",
|
"June": "Юни",
|
||||||
"Jupyter Auth": "Jupyter удостоверяване",
|
"Jupyter Auth": "Jupyter удостоверяване",
|
||||||
|
@ -1014,6 +1019,7 @@
|
||||||
"Models": "Модели",
|
"Models": "Модели",
|
||||||
"Models Access": "Достъп до модели",
|
"Models Access": "Достъп до модели",
|
||||||
"Models configuration saved successfully": "Конфигурацията на моделите е запазена успешно",
|
"Models configuration saved successfully": "Конфигурацията на моделите е запазена успешно",
|
||||||
|
"Models imported successfully": "",
|
||||||
"Models Public Sharing": "Споделяне на моделите публично",
|
"Models Public Sharing": "Споделяне на моделите публично",
|
||||||
"Mojeek Search API Key": "API ключ за Mojeek Search",
|
"Mojeek Search API Key": "API ключ за Mojeek Search",
|
||||||
"More": "Повече",
|
"More": "Повече",
|
||||||
|
@ -1073,6 +1079,7 @@
|
||||||
"Note deleted successfully": "",
|
"Note deleted successfully": "",
|
||||||
"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": "",
|
||||||
"Notification Sound": "Звук за известия",
|
"Notification Sound": "Звук за известия",
|
||||||
"Notification Webhook": "Webhook за известия",
|
"Notification Webhook": "Webhook за известия",
|
||||||
"Notifications": "Известия",
|
"Notifications": "Известия",
|
||||||
|
@ -1122,6 +1129,7 @@
|
||||||
"OpenAI API settings updated": "Настройките на OpenAI API са актуализирани",
|
"OpenAI API settings updated": "Настройките на OpenAI API са актуализирани",
|
||||||
"OpenAI URL/Key required.": "OpenAI URL/Key е задължителен.",
|
"OpenAI URL/Key required.": "OpenAI URL/Key е задължителен.",
|
||||||
"OpenAPI": "",
|
"OpenAPI": "",
|
||||||
|
"OpenAPI Spec": "",
|
||||||
"openapi.json URL or Path": "",
|
"openapi.json URL or Path": "",
|
||||||
"Optional": "",
|
"Optional": "",
|
||||||
"Options for running a local vision-language model in the picture description. The parameters refer to a model hosted on Hugging Face. This parameter is mutually exclusive with picture_description_api.": "",
|
"Options for running a local vision-language model in the picture description. The parameters refer to a model hosted on Hugging Face. This parameter is mutually exclusive with picture_description_api.": "",
|
||||||
|
@ -1179,6 +1187,7 @@
|
||||||
"Please enter a message or attach a file.": "",
|
"Please enter a message or attach a file.": "",
|
||||||
"Please enter a prompt": "Моля, въведете промпт",
|
"Please enter a prompt": "Моля, въведете промпт",
|
||||||
"Please enter a valid ID": "",
|
"Please enter a valid ID": "",
|
||||||
|
"Please enter a valid JSON spec": "",
|
||||||
"Please enter a valid path": "",
|
"Please enter a valid path": "",
|
||||||
"Please enter a valid URL": "",
|
"Please enter a valid URL": "",
|
||||||
"Please enter a valid URL.": "",
|
"Please enter a valid URL.": "",
|
||||||
|
@ -1188,6 +1197,7 @@
|
||||||
"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": "Моля, изберете причина",
|
||||||
|
"Please select a valid JSON file": "",
|
||||||
"Please wait until all files are uploaded.": "",
|
"Please wait until all files are uploaded.": "",
|
||||||
"Port": "Порт",
|
"Port": "Порт",
|
||||||
"Positive attitude": "Позитивно отношение",
|
"Positive attitude": "Позитивно отношение",
|
||||||
|
@ -1258,8 +1268,10 @@
|
||||||
"Remove this tag from list": "",
|
"Remove this tag from list": "",
|
||||||
"Rename": "Преименуване",
|
"Rename": "Преименуване",
|
||||||
"Reorder Models": "Преорганизиране на моделите",
|
"Reorder Models": "Преорганизиране на моделите",
|
||||||
|
"Reply": "",
|
||||||
"Reply in Thread": "Отговори в тред",
|
"Reply in Thread": "Отговори в тред",
|
||||||
"Reply to thread...": "",
|
"Reply to thread...": "",
|
||||||
|
"Replying to {{NAME}}": "",
|
||||||
"required": "",
|
"required": "",
|
||||||
"Reranking Engine": "Двигател за пренареждане",
|
"Reranking Engine": "Двигател за пренареждане",
|
||||||
"Reranking Model": "Модел за преподреждане",
|
"Reranking Model": "Модел за преподреждане",
|
||||||
|
@ -1513,6 +1525,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 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)": "",
|
"This option controls how long the model will stay loaded into memory following the request (default: 5m)": "",
|
||||||
|
@ -1591,6 +1604,7 @@
|
||||||
"Unarchive Chat": "Разархивирай чат",
|
"Unarchive Chat": "Разархивирай чат",
|
||||||
"Underline": "",
|
"Underline": "",
|
||||||
"Unknown": "",
|
"Unknown": "",
|
||||||
|
"Unknown User": "",
|
||||||
"Unloads {{FROM_NOW}}": "",
|
"Unloads {{FROM_NOW}}": "",
|
||||||
"Unlock mysteries": "Разкрий мистерии",
|
"Unlock mysteries": "Разкрий мистерии",
|
||||||
"Unpin": "Откачи",
|
"Unpin": "Откачи",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue