mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-15 13:55:19 +00:00
Merge branch 'open-webui:dev' into dev
This commit is contained in:
commit
e4f27ab75f
58 changed files with 1988 additions and 566 deletions
|
|
@ -2767,6 +2767,12 @@ WEB_SEARCH_TRUST_ENV = PersistentConfig(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
OLLAMA_CLOUD_WEB_SEARCH_API_KEY = PersistentConfig(
|
||||||
|
"OLLAMA_CLOUD_WEB_SEARCH_API_KEY",
|
||||||
|
"rag.web.search.ollama_cloud_api_key",
|
||||||
|
os.getenv("OLLAMA_CLOUD_API_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
SEARXNG_QUERY_URL = PersistentConfig(
|
SEARXNG_QUERY_URL = PersistentConfig(
|
||||||
"SEARXNG_QUERY_URL",
|
"SEARXNG_QUERY_URL",
|
||||||
"rag.web.search.searxng_query_url",
|
"rag.web.search.searxng_query_url",
|
||||||
|
|
|
||||||
|
|
@ -474,6 +474,10 @@ ENABLE_OAUTH_ID_TOKEN_COOKIE = (
|
||||||
os.environ.get("ENABLE_OAUTH_ID_TOKEN_COOKIE", "True").lower() == "true"
|
os.environ.get("ENABLE_OAUTH_ID_TOKEN_COOKIE", "True").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.environ.get(
|
||||||
|
"OAUTH_CLIENT_INFO_ENCRYPTION_KEY", WEBUI_SECRET_KEY
|
||||||
|
)
|
||||||
|
|
||||||
OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get(
|
OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get(
|
||||||
"OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY
|
"OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,11 @@ from starlette.middleware.sessions import SessionMiddleware
|
||||||
from starlette.responses import Response, StreamingResponse
|
from starlette.responses import Response, StreamingResponse
|
||||||
from starlette.datastructures import Headers
|
from starlette.datastructures import Headers
|
||||||
|
|
||||||
|
from starsessions import (
|
||||||
|
SessionMiddleware as StarSessionsMiddleware,
|
||||||
|
SessionAutoloadMiddleware,
|
||||||
|
)
|
||||||
|
from starsessions.stores.redis import RedisStore
|
||||||
|
|
||||||
from open_webui.utils import logger
|
from open_webui.utils import logger
|
||||||
from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware
|
from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware
|
||||||
|
|
@ -269,6 +274,7 @@ from open_webui.config import (
|
||||||
WEB_SEARCH_CONCURRENT_REQUESTS,
|
WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||||
WEB_SEARCH_TRUST_ENV,
|
WEB_SEARCH_TRUST_ENV,
|
||||||
WEB_SEARCH_DOMAIN_FILTER_LIST,
|
WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
|
OLLAMA_CLOUD_WEB_SEARCH_API_KEY,
|
||||||
JINA_API_KEY,
|
JINA_API_KEY,
|
||||||
SEARCHAPI_API_KEY,
|
SEARCHAPI_API_KEY,
|
||||||
SEARCHAPI_ENGINE,
|
SEARCHAPI_ENGINE,
|
||||||
|
|
@ -467,7 +473,12 @@ from open_webui.utils.auth import (
|
||||||
get_verified_user,
|
get_verified_user,
|
||||||
)
|
)
|
||||||
from open_webui.utils.plugin import install_tool_and_function_dependencies
|
from open_webui.utils.plugin import install_tool_and_function_dependencies
|
||||||
from open_webui.utils.oauth import OAuthManager
|
from open_webui.utils.oauth import (
|
||||||
|
OAuthManager,
|
||||||
|
OAuthClientManager,
|
||||||
|
decrypt_data,
|
||||||
|
OAuthClientInformationFull,
|
||||||
|
)
|
||||||
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
||||||
from open_webui.utils.redis import get_redis_connection
|
from open_webui.utils.redis import get_redis_connection
|
||||||
|
|
||||||
|
|
@ -597,9 +608,14 @@ app = FastAPI(
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# For Open WebUI OIDC/OAuth2
|
||||||
oauth_manager = OAuthManager(app)
|
oauth_manager = OAuthManager(app)
|
||||||
app.state.oauth_manager = oauth_manager
|
app.state.oauth_manager = oauth_manager
|
||||||
|
|
||||||
|
# For Integrations
|
||||||
|
oauth_client_manager = OAuthClientManager(app)
|
||||||
|
app.state.oauth_client_manager = oauth_client_manager
|
||||||
|
|
||||||
app.state.instance_id = None
|
app.state.instance_id = None
|
||||||
app.state.config = AppConfig(
|
app.state.config = AppConfig(
|
||||||
redis_url=REDIS_URL,
|
redis_url=REDIS_URL,
|
||||||
|
|
@ -883,6 +899,8 @@ app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = BYPASS_WEB_SEARCH_WEB_LOADER
|
||||||
|
|
||||||
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
|
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
|
||||||
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
|
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
|
||||||
|
|
||||||
|
app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY = OLLAMA_CLOUD_WEB_SEARCH_API_KEY
|
||||||
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
|
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
|
||||||
app.state.config.YACY_QUERY_URL = YACY_QUERY_URL
|
app.state.config.YACY_QUERY_URL = YACY_QUERY_URL
|
||||||
app.state.config.YACY_USERNAME = YACY_USERNAME
|
app.state.config.YACY_USERNAME = YACY_USERNAME
|
||||||
|
|
@ -1873,14 +1891,78 @@ async def get_current_usage(user=Depends(get_verified_user)):
|
||||||
# OAuth Login & Callback
|
# OAuth Login & Callback
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize OAuth client manager with any MCP tool servers using OAuth 2.1
|
||||||
|
if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0:
|
||||||
|
for tool_server_connection in app.state.config.TOOL_SERVER_CONNECTIONS:
|
||||||
|
if tool_server_connection.get("type", "openapi") == "mcp":
|
||||||
|
server_id = tool_server_connection.get("info", {}).get("id")
|
||||||
|
auth_type = tool_server_connection.get("auth_type", "none")
|
||||||
|
if server_id and auth_type == "oauth_2.1":
|
||||||
|
oauth_client_info = tool_server_connection.get("info", {}).get(
|
||||||
|
"oauth_client_info", ""
|
||||||
|
)
|
||||||
|
|
||||||
|
oauth_client_info = decrypt_data(oauth_client_info)
|
||||||
|
app.state.oauth_client_manager.add_client(
|
||||||
|
f"mcp:{server_id}", OAuthClientInformationFull(**oauth_client_info)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# SessionMiddleware is used by authlib for oauth
|
# SessionMiddleware is used by authlib for oauth
|
||||||
if len(OAUTH_PROVIDERS) > 0:
|
if len(OAUTH_PROVIDERS) > 0:
|
||||||
app.add_middleware(
|
try:
|
||||||
SessionMiddleware,
|
if REDIS_URL:
|
||||||
secret_key=WEBUI_SECRET_KEY,
|
redis_session_store = RedisStore(
|
||||||
session_cookie="oui-session",
|
url=REDIS_URL,
|
||||||
same_site=WEBUI_SESSION_COOKIE_SAME_SITE,
|
prefix=(
|
||||||
https_only=WEBUI_SESSION_COOKIE_SECURE,
|
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.get("/oauth/clients/{client_id}/authorize")
|
||||||
|
async def oauth_client_authorize(
|
||||||
|
client_id: str,
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
|
):
|
||||||
|
return await oauth_client_manager.handle_authorize(request, client_id=client_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/oauth/clients/{client_id}/callback")
|
||||||
|
async def oauth_client_callback(
|
||||||
|
client_id: str,
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
|
):
|
||||||
|
return await oauth_client_manager.handle_callback(
|
||||||
|
request,
|
||||||
|
client_id=client_id,
|
||||||
|
user_id=user.id if user else None,
|
||||||
|
response=response,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1895,8 +1977,9 @@ async def oauth_login(provider: str, request: Request):
|
||||||
# - This is considered insecure in general, as OAuth providers do not always verify email addresses
|
# - This is considered insecure in general, as OAuth providers do not always verify email addresses
|
||||||
# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user
|
# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user
|
||||||
# - Email addresses are considered unique, so we fail registration if the email address is already taken
|
# - Email addresses are considered unique, so we fail registration if the email address is already taken
|
||||||
@app.get("/oauth/{provider}/callback")
|
@app.get("/oauth/{provider}/callback") # Legacy endpoint
|
||||||
async def oauth_callback(provider: str, request: Request, response: Response):
|
@app.get("/oauth/{provider}/login/callback")
|
||||||
|
async def oauth_login_callback(provider: str, request: Request, response: Response):
|
||||||
return await oauth_manager.handle_callback(request, provider, response)
|
return await oauth_manager.handle_callback(request, provider, response)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,10 @@ class ChannelModel(BaseModel):
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelResponse(ChannelModel):
|
||||||
|
write_access: bool = False
|
||||||
|
|
||||||
|
|
||||||
class ChannelForm(BaseModel):
|
class ChannelForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
|
|
||||||
|
|
@ -492,11 +492,16 @@ class ChatTable:
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
|
include_folders: bool = False,
|
||||||
skip: Optional[int] = None,
|
skip: Optional[int] = None,
|
||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
) -> list[ChatTitleIdResponse]:
|
) -> list[ChatTitleIdResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
query = db.query(Chat).filter_by(user_id=user_id).filter_by(folder_id=None)
|
query = db.query(Chat).filter_by(user_id=user_id)
|
||||||
|
|
||||||
|
if not include_folders:
|
||||||
|
query = query.filter_by(folder_id=None)
|
||||||
|
|
||||||
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
||||||
|
|
||||||
if not include_archived:
|
if not include_archived:
|
||||||
|
|
@ -943,6 +948,16 @@ class ChatTable:
|
||||||
|
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
def count_chats_by_folder_id_and_user_id(self, folder_id: str, user_id: str) -> int:
|
||||||
|
with get_db() as db:
|
||||||
|
query = db.query(Chat).filter_by(user_id=user_id)
|
||||||
|
|
||||||
|
query = query.filter_by(folder_id=folder_id)
|
||||||
|
count = query.count()
|
||||||
|
|
||||||
|
log.info(f"Count of chats for folder '{folder_id}': {count}")
|
||||||
|
return count
|
||||||
|
|
||||||
def delete_tag_by_id_and_user_id_and_tag_name(
|
def delete_tag_by_id_and_user_id_and_tag_name(
|
||||||
self, id: str, user_id: str, tag_name: str
|
self, id: str, user_id: str, tag_name: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,17 @@ class FilesTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_file_by_id_and_user_id(self, id: str, user_id: str) -> Optional[FileModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
try:
|
||||||
|
file = db.query(File).filter_by(id=id, user_id=user_id).first()
|
||||||
|
if file:
|
||||||
|
return FileModel.model_validate(file)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_file_metadata_by_id(self, id: str) -> Optional[FileMetadataResponse]:
|
def get_file_metadata_by_id(self, id: str) -> Optional[FileMetadataResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,26 @@ class OAuthSessionTable:
|
||||||
log.error(f"Error getting OAuth session by ID: {e}")
|
log.error(f"Error getting OAuth session by ID: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_session_by_provider_and_user_id(
|
||||||
|
self, provider: str, user_id: str
|
||||||
|
) -> Optional[OAuthSessionModel]:
|
||||||
|
"""Get OAuth session by provider and user ID"""
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
session = (
|
||||||
|
db.query(OAuthSession)
|
||||||
|
.filter_by(provider=provider, user_id=user_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if session:
|
||||||
|
session.token = self._decrypt_token(session.token)
|
||||||
|
return OAuthSessionModel.model_validate(session)
|
||||||
|
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth session by provider and user ID: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def get_sessions_by_user_id(self, user_id: str) -> List[OAuthSessionModel]:
|
def get_sessions_by_user_id(self, user_id: str) -> List[OAuthSessionModel]:
|
||||||
"""Get all OAuth sessions for a user"""
|
"""Get all OAuth sessions for a user"""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ class ToolResponse(BaseModel):
|
||||||
class ToolUserResponse(ToolResponse):
|
class ToolUserResponse(ToolResponse):
|
||||||
user: Optional[UserResponse] = None
|
user: Optional[UserResponse] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|
||||||
class ToolForm(BaseModel):
|
class ToolForm(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
|
|
||||||
51
backend/open_webui/retrieval/web/ollama.py
Normal file
51
backend/open_webui/retrieval/web/ollama.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
from open_webui.retrieval.web.main import SearchResult
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
|
def search_ollama_cloud(
|
||||||
|
url: str,
|
||||||
|
api_key: str,
|
||||||
|
query: str,
|
||||||
|
count: int,
|
||||||
|
filter_list: Optional[list[str]] = None,
|
||||||
|
) -> list[SearchResult]:
|
||||||
|
"""Search using Ollama Search API and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key (str): A Ollama Search API key
|
||||||
|
query (str): The query to search for
|
||||||
|
count (int): Number of results to return
|
||||||
|
filter_list (Optional[list[str]]): List of domains to filter results by
|
||||||
|
"""
|
||||||
|
log.info(f"Searching with Ollama for query: {query}")
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||||
|
payload = {"query": query, "max_results": count}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{url}/api/web_search", headers=headers, json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
results = data.get("results", [])
|
||||||
|
log.info(f"Found {len(results)} results")
|
||||||
|
|
||||||
|
return [
|
||||||
|
SearchResult(
|
||||||
|
link=result.get("url", ""),
|
||||||
|
title=result.get("title", ""),
|
||||||
|
snippet=result.get("content", ""),
|
||||||
|
)
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error searching Ollama: {e}")
|
||||||
|
return []
|
||||||
|
|
@ -10,7 +10,13 @@ from pydantic import BaseModel
|
||||||
from open_webui.socket.main import sio, get_user_ids_from_room
|
from open_webui.socket.main import sio, get_user_ids_from_room
|
||||||
from open_webui.models.users import Users, UserNameResponse
|
from open_webui.models.users import Users, UserNameResponse
|
||||||
|
|
||||||
from open_webui.models.channels import Channels, ChannelModel, ChannelForm
|
from open_webui.models.groups import Groups
|
||||||
|
from open_webui.models.channels import (
|
||||||
|
Channels,
|
||||||
|
ChannelModel,
|
||||||
|
ChannelForm,
|
||||||
|
ChannelResponse,
|
||||||
|
)
|
||||||
from open_webui.models.messages import (
|
from open_webui.models.messages import (
|
||||||
Messages,
|
Messages,
|
||||||
MessageModel,
|
MessageModel,
|
||||||
|
|
@ -80,7 +86,7 @@ async def create_new_channel(form_data: ChannelForm, user=Depends(get_admin_user
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}", response_model=Optional[ChannelModel])
|
@router.get("/{id}", response_model=Optional[ChannelResponse])
|
||||||
async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
|
async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
channel = Channels.get_channel_by_id(id)
|
channel = Channels.get_channel_by_id(id)
|
||||||
if not channel:
|
if not channel:
|
||||||
|
|
@ -95,7 +101,16 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
|
|
||||||
return ChannelModel(**channel.model_dump())
|
write_access = has_access(
|
||||||
|
user.id, type="write", access_control=channel.access_control, strict=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return ChannelResponse(
|
||||||
|
**{
|
||||||
|
**channel.model_dump(),
|
||||||
|
"write_access": write_access or user.role == "admin",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
|
@ -275,6 +290,7 @@ async def model_response_handler(request, channel, message, user):
|
||||||
)
|
)
|
||||||
|
|
||||||
thread_history = []
|
thread_history = []
|
||||||
|
images = []
|
||||||
message_users = {}
|
message_users = {}
|
||||||
|
|
||||||
for thread_message in thread_messages:
|
for thread_message in thread_messages:
|
||||||
|
|
@ -303,6 +319,11 @@ async def model_response_handler(request, channel, message, user):
|
||||||
f"{username}: {replace_mentions(thread_message.content)}"
|
f"{username}: {replace_mentions(thread_message.content)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
thread_message_files = thread_message.data.get("files", [])
|
||||||
|
for file in thread_message_files:
|
||||||
|
if file.get("type", "") == "image":
|
||||||
|
images.append(file.get("url", ""))
|
||||||
|
|
||||||
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)}, an AI assistant participating in a threaded conversation. Be helpful, concise, and conversational."
|
||||||
|
|
@ -313,14 +334,29 @@ async def model_response_handler(request, channel, message, user):
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
content = f"{user.name if user else 'User'}: {message_content}"
|
||||||
|
if images:
|
||||||
|
content = [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": content,
|
||||||
|
},
|
||||||
|
*[
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": image,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for image in images
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
form_data = {
|
form_data = {
|
||||||
"model": model_id,
|
"model": model_id,
|
||||||
"messages": [
|
"messages": [
|
||||||
system_message,
|
system_message,
|
||||||
{
|
{"role": "user", "content": content},
|
||||||
"role": "user",
|
|
||||||
"content": f"{user.name if user else 'User'}: {message_content}",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
"stream": False,
|
"stream": False,
|
||||||
}
|
}
|
||||||
|
|
@ -362,7 +398,7 @@ async def new_message_handler(
|
||||||
)
|
)
|
||||||
|
|
||||||
if user.role != "admin" and not has_access(
|
if user.role != "admin" and not has_access(
|
||||||
user.id, type="read", access_control=channel.access_control
|
user.id, type="write", access_control=channel.access_control, strict=False
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -658,7 +694,7 @@ async def add_reaction_to_message(
|
||||||
)
|
)
|
||||||
|
|
||||||
if user.role != "admin" and not has_access(
|
if user.role != "admin" and not has_access(
|
||||||
user.id, type="read", access_control=channel.access_control
|
user.id, type="write", access_control=channel.access_control, strict=False
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -724,7 +760,7 @@ async def remove_reaction_by_id_and_user_id_and_name(
|
||||||
)
|
)
|
||||||
|
|
||||||
if user.role != "admin" and not has_access(
|
if user.role != "admin" and not has_access(
|
||||||
user.id, type="read", access_control=channel.access_control
|
user.id, type="write", access_control=channel.access_control, strict=False
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -806,7 +842,9 @@ async def delete_message_by_id(
|
||||||
if (
|
if (
|
||||||
user.role != "admin"
|
user.role != "admin"
|
||||||
and message.user_id != user.id
|
and message.user_id != user.id
|
||||||
and not has_access(user.id, type="read", access_control=channel.access_control)
|
and not has_access(
|
||||||
|
user.id, type="write", access_control=channel.access_control, strict=False
|
||||||
|
)
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ router = APIRouter()
|
||||||
@router.get("/", response_model=list[ChatTitleIdResponse])
|
@router.get("/", response_model=list[ChatTitleIdResponse])
|
||||||
@router.get("/list", response_model=list[ChatTitleIdResponse])
|
@router.get("/list", response_model=list[ChatTitleIdResponse])
|
||||||
def get_session_user_chat_list(
|
def get_session_user_chat_list(
|
||||||
user=Depends(get_verified_user), page: Optional[int] = None
|
user=Depends(get_verified_user),
|
||||||
|
page: Optional[int] = None,
|
||||||
|
include_folders: Optional[bool] = False,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
if page is not None:
|
if page is not None:
|
||||||
|
|
@ -45,10 +47,12 @@ def get_session_user_chat_list(
|
||||||
skip = (page - 1) * limit
|
skip = (page - 1) * limit
|
||||||
|
|
||||||
return Chats.get_chat_title_id_list_by_user_id(
|
return Chats.get_chat_title_id_list_by_user_id(
|
||||||
user.id, skip=skip, limit=limit
|
user.id, include_folders=include_folders, skip=skip, limit=limit
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return Chats.get_chat_title_id_list_by_user_id(user.id)
|
return Chats.get_chat_title_id_list_by_user_id(
|
||||||
|
user.id, include_folders=include_folders
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
from fastapi import APIRouter, Depends, Request, HTTPException
|
from fastapi import APIRouter, Depends, Request, HTTPException
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -17,6 +18,14 @@ from open_webui.utils.mcp.client import MCPClient
|
||||||
|
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
from open_webui.utils.oauth import (
|
||||||
|
get_discovery_urls,
|
||||||
|
get_oauth_client_info_with_dynamic_client_registration,
|
||||||
|
encrypt_data,
|
||||||
|
decrypt_data,
|
||||||
|
OAuthClientInformationFull,
|
||||||
|
)
|
||||||
|
from mcp.shared.auth import OAuthMetadata
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -86,6 +95,43 @@ async def set_connections_config(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthClientRegistrationForm(BaseModel):
|
||||||
|
url: str
|
||||||
|
client_id: str
|
||||||
|
client_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/oauth/clients/register")
|
||||||
|
async def register_oauth_client(
|
||||||
|
request: Request,
|
||||||
|
form_data: OAuthClientRegistrationForm,
|
||||||
|
type: Optional[str] = None,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
oauth_client_id = form_data.client_id
|
||||||
|
if type:
|
||||||
|
oauth_client_id = f"{type}:{form_data.client_id}"
|
||||||
|
|
||||||
|
oauth_client_info = (
|
||||||
|
await get_oauth_client_info_with_dynamic_client_registration(
|
||||||
|
request, oauth_client_id, form_data.url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"oauth_client_info": encrypt_data(
|
||||||
|
oauth_client_info.model_dump(mode="json")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Failed to register OAuth client: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Failed to register OAuth client",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# ToolServers Config
|
# ToolServers Config
|
||||||
############################
|
############################
|
||||||
|
|
@ -122,8 +168,29 @@ async def set_tool_servers_config(
|
||||||
request.app.state.config.TOOL_SERVER_CONNECTIONS = [
|
request.app.state.config.TOOL_SERVER_CONNECTIONS = [
|
||||||
connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS
|
connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS
|
||||||
]
|
]
|
||||||
|
|
||||||
await set_tool_servers(request)
|
await set_tool_servers(request)
|
||||||
|
|
||||||
|
for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
|
||||||
|
server_type = connection.get("type", "openapi")
|
||||||
|
if server_type == "mcp":
|
||||||
|
server_id = connection.get("info", {}).get("id")
|
||||||
|
auth_type = connection.get("auth_type", "none")
|
||||||
|
if auth_type == "oauth_2.1" and server_id:
|
||||||
|
try:
|
||||||
|
oauth_client_info = connection.get("info", {}).get(
|
||||||
|
"oauth_client_info", ""
|
||||||
|
)
|
||||||
|
oauth_client_info = decrypt_data(oauth_client_info)
|
||||||
|
|
||||||
|
await request.app.state.oauth_client_manager.add_client(
|
||||||
|
f"{server_type}:{server_id}",
|
||||||
|
OAuthClientInformationFull(**oauth_client_info),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Failed to add OAuth client for MCP tool server: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
|
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
|
||||||
}
|
}
|
||||||
|
|
@ -138,46 +205,79 @@ async def verify_tool_servers_config(
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if form_data.type == "mcp":
|
if form_data.type == "mcp":
|
||||||
try:
|
if form_data.auth_type == "oauth_2.1":
|
||||||
client = MCPClient()
|
discovery_urls = get_discovery_urls(form_data.url)
|
||||||
auth = None
|
async with aiohttp.ClientSession() as session:
|
||||||
headers = None
|
async with session.get(
|
||||||
|
discovery_urls[0]
|
||||||
|
) as oauth_server_metadata_response:
|
||||||
|
if oauth_server_metadata_response.status != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Failed to fetch OAuth 2.1 discovery document from {discovery_urls[0]}",
|
||||||
|
)
|
||||||
|
|
||||||
token = None
|
try:
|
||||||
if form_data.auth_type == "bearer":
|
oauth_server_metadata = OAuthMetadata.model_validate(
|
||||||
token = form_data.key
|
await oauth_server_metadata_response.json()
|
||||||
elif form_data.auth_type == "session":
|
)
|
||||||
token = request.state.token.credentials
|
return {
|
||||||
elif form_data.auth_type == "system_oauth":
|
"status": True,
|
||||||
try:
|
"oauth_server_metadata": oauth_server_metadata.model_dump(
|
||||||
if request.cookies.get("oauth_session_id", None):
|
mode="json"
|
||||||
token = (
|
),
|
||||||
await request.app.state.oauth_manager.get_oauth_token(
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.info(
|
||||||
|
f"Failed to parse OAuth 2.1 discovery document: {e}"
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_urls[0]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Failed to fetch OAuth 2.1 discovery document from {discovery_urls[0]}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
client = MCPClient()
|
||||||
|
headers = None
|
||||||
|
|
||||||
|
token = None
|
||||||
|
if form_data.auth_type == "bearer":
|
||||||
|
token = form_data.key
|
||||||
|
elif form_data.auth_type == "session":
|
||||||
|
token = request.state.token.credentials
|
||||||
|
elif form_data.auth_type == "system_oauth":
|
||||||
|
try:
|
||||||
|
if request.cookies.get("oauth_session_id", None):
|
||||||
|
token = await request.app.state.oauth_manager.get_oauth_token(
|
||||||
user.id,
|
user.id,
|
||||||
request.cookies.get("oauth_session_id", None),
|
request.cookies.get("oauth_session_id", None),
|
||||||
)
|
)
|
||||||
)
|
except Exception as e:
|
||||||
except Exception as e:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
if token:
|
if token:
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
await client.connect(form_data.url, auth=auth, headers=headers)
|
await client.connect(form_data.url, headers=headers)
|
||||||
specs = await client.list_tool_specs()
|
specs = await client.list_tool_specs()
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"specs": specs,
|
"specs": specs,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(f"Failed to create MCP client: {e}")
|
log.debug(f"Failed to create MCP client: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Failed to create MCP client",
|
detail=f"Failed to create MCP client",
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if client:
|
if client:
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
else: # openapi
|
else: # openapi
|
||||||
token = None
|
token = None
|
||||||
if form_data.auth_type == "bearer":
|
if form_data.auth_type == "bearer":
|
||||||
|
|
|
||||||
|
|
@ -262,15 +262,15 @@ async def update_folder_is_expanded_by_id(
|
||||||
async def delete_folder_by_id(
|
async def delete_folder_by_id(
|
||||||
request: Request, id: str, user=Depends(get_verified_user)
|
request: Request, id: str, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
chat_delete_permission = has_permission(
|
if Chats.count_chats_by_folder_id_and_user_id(id, user.id):
|
||||||
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
|
chat_delete_permission = has_permission(
|
||||||
)
|
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
|
||||||
|
|
||||||
if user.role != "admin" and not chat_delete_permission:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
|
||||||
)
|
)
|
||||||
|
if user.role != "admin" and not chat_delete_permission:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||||
if folder:
|
if folder:
|
||||||
|
|
|
||||||
|
|
@ -431,8 +431,10 @@ async def update_function_valves_by_id(
|
||||||
try:
|
try:
|
||||||
form_data = {k: v for k, v in form_data.items() if v is not None}
|
form_data = {k: v for k, v in form_data.items() if v is not None}
|
||||||
valves = Valves(**form_data)
|
valves = Valves(**form_data)
|
||||||
Functions.update_function_valves_by_id(id, valves.model_dump())
|
|
||||||
return valves.model_dump()
|
valves_dict = valves.model_dump(exclude_unset=True)
|
||||||
|
Functions.update_function_valves_by_id(id, valves_dict)
|
||||||
|
return valves_dict
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Error updating function values by id {id}: {e}")
|
log.exception(f"Error updating function values by id {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -514,10 +516,11 @@ async def update_function_user_valves_by_id(
|
||||||
try:
|
try:
|
||||||
form_data = {k: v for k, v in form_data.items() if v is not None}
|
form_data = {k: v for k, v in form_data.items() if v is not None}
|
||||||
user_valves = UserValves(**form_data)
|
user_valves = UserValves(**form_data)
|
||||||
|
user_valves_dict = user_valves.model_dump(exclude_unset=True)
|
||||||
Functions.update_user_valves_by_id_and_user_id(
|
Functions.update_user_valves_by_id_and_user_id(
|
||||||
id, user.id, user_valves.model_dump()
|
id, user.id, user_valves_dict
|
||||||
)
|
)
|
||||||
return user_valves.model_dump()
|
return user_valves_dict
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Error updating function user valves by id {id}: {e}")
|
log.exception(f"Error updating function user valves by id {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
|
|
@ -514,6 +514,7 @@ async def image_generations(
|
||||||
size = form_data.size
|
size = form_data.size
|
||||||
|
|
||||||
width, height = tuple(map(int, size.split("x")))
|
width, height = tuple(map(int, size.split("x")))
|
||||||
|
model = get_image_model(request)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
try:
|
try:
|
||||||
|
|
@ -531,11 +532,7 @@ async def image_generations(
|
||||||
headers["X-OpenWebUI-User-Role"] = user.role
|
headers["X-OpenWebUI-User-Role"] = user.role
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"model": (
|
"model": model,
|
||||||
request.app.state.config.IMAGE_GENERATION_MODEL
|
|
||||||
if request.app.state.config.IMAGE_GENERATION_MODEL != ""
|
|
||||||
else "dall-e-2"
|
|
||||||
),
|
|
||||||
"prompt": form_data.prompt,
|
"prompt": form_data.prompt,
|
||||||
"n": form_data.n,
|
"n": form_data.n,
|
||||||
"size": (
|
"size": (
|
||||||
|
|
@ -584,7 +581,6 @@ async def image_generations(
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
headers["x-goog-api-key"] = request.app.state.config.IMAGES_GEMINI_API_KEY
|
headers["x-goog-api-key"] = request.app.state.config.IMAGES_GEMINI_API_KEY
|
||||||
|
|
||||||
model = get_image_model(request)
|
|
||||||
data = {
|
data = {
|
||||||
"instances": {"prompt": form_data.prompt},
|
"instances": {"prompt": form_data.prompt},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
|
@ -640,7 +636,7 @@ async def image_generations(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
res = await comfyui_generate_image(
|
res = await comfyui_generate_image(
|
||||||
request.app.state.config.IMAGE_GENERATION_MODEL,
|
model,
|
||||||
form_data,
|
form_data,
|
||||||
user.id,
|
user.id,
|
||||||
request.app.state.config.COMFYUI_BASE_URL,
|
request.app.state.config.COMFYUI_BASE_URL,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ from open_webui.retrieval.loaders.youtube import YoutubeLoader
|
||||||
# Web search engines
|
# Web search engines
|
||||||
from open_webui.retrieval.web.main import SearchResult
|
from open_webui.retrieval.web.main import SearchResult
|
||||||
from open_webui.retrieval.web.utils import get_web_loader
|
from open_webui.retrieval.web.utils import get_web_loader
|
||||||
|
from open_webui.retrieval.web.ollama import search_ollama_cloud
|
||||||
from open_webui.retrieval.web.brave import search_brave
|
from open_webui.retrieval.web.brave import search_brave
|
||||||
from open_webui.retrieval.web.kagi import search_kagi
|
from open_webui.retrieval.web.kagi import search_kagi
|
||||||
from open_webui.retrieval.web.mojeek import search_mojeek
|
from open_webui.retrieval.web.mojeek import search_mojeek
|
||||||
|
|
@ -469,6 +470,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
||||||
"WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
"WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
|
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
|
||||||
"BYPASS_WEB_SEARCH_WEB_LOADER": request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER,
|
"BYPASS_WEB_SEARCH_WEB_LOADER": request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER,
|
||||||
|
"OLLAMA_CLOUD_WEB_SEARCH_API_KEY": request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY,
|
||||||
"SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL,
|
"SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL,
|
||||||
"YACY_QUERY_URL": request.app.state.config.YACY_QUERY_URL,
|
"YACY_QUERY_URL": request.app.state.config.YACY_QUERY_URL,
|
||||||
"YACY_USERNAME": request.app.state.config.YACY_USERNAME,
|
"YACY_USERNAME": request.app.state.config.YACY_USERNAME,
|
||||||
|
|
@ -525,6 +527,7 @@ class WebConfig(BaseModel):
|
||||||
WEB_SEARCH_DOMAIN_FILTER_LIST: Optional[List[str]] = []
|
WEB_SEARCH_DOMAIN_FILTER_LIST: Optional[List[str]] = []
|
||||||
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
|
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
|
||||||
BYPASS_WEB_SEARCH_WEB_LOADER: Optional[bool] = None
|
BYPASS_WEB_SEARCH_WEB_LOADER: Optional[bool] = None
|
||||||
|
OLLAMA_CLOUD_WEB_SEARCH_API_KEY: Optional[str] = None
|
||||||
SEARXNG_QUERY_URL: Optional[str] = None
|
SEARXNG_QUERY_URL: Optional[str] = None
|
||||||
YACY_QUERY_URL: Optional[str] = None
|
YACY_QUERY_URL: Optional[str] = None
|
||||||
YACY_USERNAME: Optional[str] = None
|
YACY_USERNAME: Optional[str] = None
|
||||||
|
|
@ -988,6 +991,9 @@ async def update_rag_config(
|
||||||
request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = (
|
request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = (
|
||||||
form_data.web.BYPASS_WEB_SEARCH_WEB_LOADER
|
form_data.web.BYPASS_WEB_SEARCH_WEB_LOADER
|
||||||
)
|
)
|
||||||
|
request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY = (
|
||||||
|
form_data.web.OLLAMA_CLOUD_WEB_SEARCH_API_KEY
|
||||||
|
)
|
||||||
request.app.state.config.SEARXNG_QUERY_URL = form_data.web.SEARXNG_QUERY_URL
|
request.app.state.config.SEARXNG_QUERY_URL = form_data.web.SEARXNG_QUERY_URL
|
||||||
request.app.state.config.YACY_QUERY_URL = form_data.web.YACY_QUERY_URL
|
request.app.state.config.YACY_QUERY_URL = form_data.web.YACY_QUERY_URL
|
||||||
request.app.state.config.YACY_USERNAME = form_data.web.YACY_USERNAME
|
request.app.state.config.YACY_USERNAME = form_data.web.YACY_USERNAME
|
||||||
|
|
@ -1139,6 +1145,7 @@ async def update_rag_config(
|
||||||
"WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
"WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
|
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
|
||||||
"BYPASS_WEB_SEARCH_WEB_LOADER": request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER,
|
"BYPASS_WEB_SEARCH_WEB_LOADER": request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER,
|
||||||
|
"OLLAMA_CLOUD_WEB_SEARCH_API_KEY": request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY,
|
||||||
"SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL,
|
"SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL,
|
||||||
"YACY_QUERY_URL": request.app.state.config.YACY_QUERY_URL,
|
"YACY_QUERY_URL": request.app.state.config.YACY_QUERY_URL,
|
||||||
"YACY_USERNAME": request.app.state.config.YACY_USERNAME,
|
"YACY_USERNAME": request.app.state.config.YACY_USERNAME,
|
||||||
|
|
@ -1407,59 +1414,35 @@ def process_file(
|
||||||
form_data: ProcessFileForm,
|
form_data: ProcessFileForm,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
try:
|
if user.role == "admin":
|
||||||
file = Files.get_file_by_id(form_data.file_id)
|
file = Files.get_file_by_id(form_data.file_id)
|
||||||
|
else:
|
||||||
|
file = Files.get_file_by_id_and_user_id(form_data.file_id, user.id)
|
||||||
|
|
||||||
collection_name = form_data.collection_name
|
if file:
|
||||||
|
try:
|
||||||
|
|
||||||
if collection_name is None:
|
collection_name = form_data.collection_name
|
||||||
collection_name = f"file-{file.id}"
|
|
||||||
|
|
||||||
if form_data.content:
|
if collection_name is None:
|
||||||
# Update the content in the file
|
collection_name = f"file-{file.id}"
|
||||||
# Usage: /files/{file_id}/data/content/update, /files/ (audio file upload pipeline)
|
|
||||||
|
|
||||||
try:
|
if form_data.content:
|
||||||
# /files/{file_id}/data/content/update
|
# Update the content in the file
|
||||||
VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}")
|
# Usage: /files/{file_id}/data/content/update, /files/ (audio file upload pipeline)
|
||||||
except:
|
|
||||||
# Audio file upload pipeline
|
|
||||||
pass
|
|
||||||
|
|
||||||
docs = [
|
try:
|
||||||
Document(
|
# /files/{file_id}/data/content/update
|
||||||
page_content=form_data.content.replace("<br/>", "\n"),
|
VECTOR_DB_CLIENT.delete_collection(
|
||||||
metadata={
|
collection_name=f"file-{file.id}"
|
||||||
**file.meta,
|
|
||||||
"name": file.filename,
|
|
||||||
"created_by": file.user_id,
|
|
||||||
"file_id": file.id,
|
|
||||||
"source": file.filename,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
text_content = form_data.content
|
|
||||||
elif form_data.collection_name:
|
|
||||||
# Check if the file has already been processed and save the content
|
|
||||||
# Usage: /knowledge/{id}/file/add, /knowledge/{id}/file/update
|
|
||||||
|
|
||||||
result = VECTOR_DB_CLIENT.query(
|
|
||||||
collection_name=f"file-{file.id}", filter={"file_id": file.id}
|
|
||||||
)
|
|
||||||
|
|
||||||
if result is not None and len(result.ids[0]) > 0:
|
|
||||||
docs = [
|
|
||||||
Document(
|
|
||||||
page_content=result.documents[0][idx],
|
|
||||||
metadata=result.metadatas[0][idx],
|
|
||||||
)
|
)
|
||||||
for idx, id in enumerate(result.ids[0])
|
except:
|
||||||
]
|
# Audio file upload pipeline
|
||||||
else:
|
pass
|
||||||
|
|
||||||
docs = [
|
docs = [
|
||||||
Document(
|
Document(
|
||||||
page_content=file.data.get("content", ""),
|
page_content=form_data.content.replace("<br/>", "\n"),
|
||||||
metadata={
|
metadata={
|
||||||
**file.meta,
|
**file.meta,
|
||||||
"name": file.filename,
|
"name": file.filename,
|
||||||
|
|
@ -1470,149 +1453,190 @@ def process_file(
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
text_content = file.data.get("content", "")
|
text_content = form_data.content
|
||||||
else:
|
elif form_data.collection_name:
|
||||||
# Process the file and save the content
|
# Check if the file has already been processed and save the content
|
||||||
# Usage: /files/
|
# Usage: /knowledge/{id}/file/add, /knowledge/{id}/file/update
|
||||||
file_path = file.path
|
|
||||||
if file_path:
|
result = VECTOR_DB_CLIENT.query(
|
||||||
file_path = Storage.get_file(file_path)
|
collection_name=f"file-{file.id}", filter={"file_id": file.id}
|
||||||
loader = Loader(
|
|
||||||
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
|
||||||
DATALAB_MARKER_API_KEY=request.app.state.config.DATALAB_MARKER_API_KEY,
|
|
||||||
DATALAB_MARKER_API_BASE_URL=request.app.state.config.DATALAB_MARKER_API_BASE_URL,
|
|
||||||
DATALAB_MARKER_ADDITIONAL_CONFIG=request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG,
|
|
||||||
DATALAB_MARKER_SKIP_CACHE=request.app.state.config.DATALAB_MARKER_SKIP_CACHE,
|
|
||||||
DATALAB_MARKER_FORCE_OCR=request.app.state.config.DATALAB_MARKER_FORCE_OCR,
|
|
||||||
DATALAB_MARKER_PAGINATE=request.app.state.config.DATALAB_MARKER_PAGINATE,
|
|
||||||
DATALAB_MARKER_STRIP_EXISTING_OCR=request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR,
|
|
||||||
DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION=request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION,
|
|
||||||
DATALAB_MARKER_FORMAT_LINES=request.app.state.config.DATALAB_MARKER_FORMAT_LINES,
|
|
||||||
DATALAB_MARKER_USE_LLM=request.app.state.config.DATALAB_MARKER_USE_LLM,
|
|
||||||
DATALAB_MARKER_OUTPUT_FORMAT=request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT,
|
|
||||||
EXTERNAL_DOCUMENT_LOADER_URL=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL,
|
|
||||||
EXTERNAL_DOCUMENT_LOADER_API_KEY=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
|
||||||
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
|
|
||||||
DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL,
|
|
||||||
DOCLING_PARAMS={
|
|
||||||
"do_ocr": request.app.state.config.DOCLING_DO_OCR,
|
|
||||||
"force_ocr": request.app.state.config.DOCLING_FORCE_OCR,
|
|
||||||
"ocr_engine": request.app.state.config.DOCLING_OCR_ENGINE,
|
|
||||||
"ocr_lang": request.app.state.config.DOCLING_OCR_LANG,
|
|
||||||
"pdf_backend": request.app.state.config.DOCLING_PDF_BACKEND,
|
|
||||||
"table_mode": request.app.state.config.DOCLING_TABLE_MODE,
|
|
||||||
"pipeline": request.app.state.config.DOCLING_PIPELINE,
|
|
||||||
"do_picture_description": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
|
|
||||||
"picture_description_mode": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
|
|
||||||
"picture_description_local": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
|
||||||
"picture_description_api": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
|
|
||||||
},
|
|
||||||
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
|
|
||||||
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
|
||||||
DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
|
||||||
MISTRAL_OCR_API_KEY=request.app.state.config.MISTRAL_OCR_API_KEY,
|
|
||||||
)
|
|
||||||
docs = loader.load(
|
|
||||||
file.filename, file.meta.get("content_type"), file_path
|
|
||||||
)
|
)
|
||||||
|
|
||||||
docs = [
|
if result is not None and len(result.ids[0]) > 0:
|
||||||
Document(
|
docs = [
|
||||||
page_content=doc.page_content,
|
Document(
|
||||||
metadata={
|
page_content=result.documents[0][idx],
|
||||||
**doc.metadata,
|
metadata=result.metadatas[0][idx],
|
||||||
"name": file.filename,
|
)
|
||||||
"created_by": file.user_id,
|
for idx, id in enumerate(result.ids[0])
|
||||||
"file_id": file.id,
|
]
|
||||||
"source": file.filename,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
for doc in docs
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
docs = [
|
|
||||||
Document(
|
|
||||||
page_content=file.data.get("content", ""),
|
|
||||||
metadata={
|
|
||||||
**file.meta,
|
|
||||||
"name": file.filename,
|
|
||||||
"created_by": file.user_id,
|
|
||||||
"file_id": file.id,
|
|
||||||
"source": file.filename,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]
|
|
||||||
text_content = " ".join([doc.page_content for doc in docs])
|
|
||||||
|
|
||||||
log.debug(f"text_content: {text_content}")
|
|
||||||
Files.update_file_data_by_id(
|
|
||||||
file.id,
|
|
||||||
{"content": text_content},
|
|
||||||
)
|
|
||||||
hash = calculate_sha256_string(text_content)
|
|
||||||
Files.update_file_hash_by_id(file.id, hash)
|
|
||||||
|
|
||||||
if request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL:
|
|
||||||
Files.update_file_data_by_id(file.id, {"status": "completed"})
|
|
||||||
return {
|
|
||||||
"status": True,
|
|
||||||
"collection_name": None,
|
|
||||||
"filename": file.filename,
|
|
||||||
"content": text_content,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
result = save_docs_to_vector_db(
|
|
||||||
request,
|
|
||||||
docs=docs,
|
|
||||||
collection_name=collection_name,
|
|
||||||
metadata={
|
|
||||||
"file_id": file.id,
|
|
||||||
"name": file.filename,
|
|
||||||
"hash": hash,
|
|
||||||
},
|
|
||||||
add=(True if form_data.collection_name else False),
|
|
||||||
user=user,
|
|
||||||
)
|
|
||||||
log.info(f"added {len(docs)} items to collection {collection_name}")
|
|
||||||
|
|
||||||
if result:
|
|
||||||
Files.update_file_metadata_by_id(
|
|
||||||
file.id,
|
|
||||||
{
|
|
||||||
"collection_name": collection_name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Files.update_file_data_by_id(
|
|
||||||
file.id,
|
|
||||||
{"status": "completed"},
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": True,
|
|
||||||
"collection_name": collection_name,
|
|
||||||
"filename": file.filename,
|
|
||||||
"content": text_content,
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
raise Exception("Error saving document to vector database")
|
docs = [
|
||||||
except Exception as e:
|
Document(
|
||||||
raise e
|
page_content=file.data.get("content", ""),
|
||||||
|
metadata={
|
||||||
|
**file.meta,
|
||||||
|
"name": file.filename,
|
||||||
|
"created_by": file.user_id,
|
||||||
|
"file_id": file.id,
|
||||||
|
"source": file.filename,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
except Exception as e:
|
text_content = file.data.get("content", "")
|
||||||
log.exception(e)
|
else:
|
||||||
if "No pandoc was found" in str(e):
|
# Process the file and save the content
|
||||||
raise HTTPException(
|
# Usage: /files/
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
file_path = file.path
|
||||||
detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
|
if file_path:
|
||||||
|
file_path = Storage.get_file(file_path)
|
||||||
|
loader = Loader(
|
||||||
|
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||||
|
DATALAB_MARKER_API_KEY=request.app.state.config.DATALAB_MARKER_API_KEY,
|
||||||
|
DATALAB_MARKER_API_BASE_URL=request.app.state.config.DATALAB_MARKER_API_BASE_URL,
|
||||||
|
DATALAB_MARKER_ADDITIONAL_CONFIG=request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG,
|
||||||
|
DATALAB_MARKER_SKIP_CACHE=request.app.state.config.DATALAB_MARKER_SKIP_CACHE,
|
||||||
|
DATALAB_MARKER_FORCE_OCR=request.app.state.config.DATALAB_MARKER_FORCE_OCR,
|
||||||
|
DATALAB_MARKER_PAGINATE=request.app.state.config.DATALAB_MARKER_PAGINATE,
|
||||||
|
DATALAB_MARKER_STRIP_EXISTING_OCR=request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR,
|
||||||
|
DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION=request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION,
|
||||||
|
DATALAB_MARKER_FORMAT_LINES=request.app.state.config.DATALAB_MARKER_FORMAT_LINES,
|
||||||
|
DATALAB_MARKER_USE_LLM=request.app.state.config.DATALAB_MARKER_USE_LLM,
|
||||||
|
DATALAB_MARKER_OUTPUT_FORMAT=request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT,
|
||||||
|
EXTERNAL_DOCUMENT_LOADER_URL=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL,
|
||||||
|
EXTERNAL_DOCUMENT_LOADER_API_KEY=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
||||||
|
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
|
||||||
|
DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL,
|
||||||
|
DOCLING_PARAMS={
|
||||||
|
"do_ocr": request.app.state.config.DOCLING_DO_OCR,
|
||||||
|
"force_ocr": request.app.state.config.DOCLING_FORCE_OCR,
|
||||||
|
"ocr_engine": request.app.state.config.DOCLING_OCR_ENGINE,
|
||||||
|
"ocr_lang": request.app.state.config.DOCLING_OCR_LANG,
|
||||||
|
"pdf_backend": request.app.state.config.DOCLING_PDF_BACKEND,
|
||||||
|
"table_mode": request.app.state.config.DOCLING_TABLE_MODE,
|
||||||
|
"pipeline": request.app.state.config.DOCLING_PIPELINE,
|
||||||
|
"do_picture_description": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
|
||||||
|
"picture_description_mode": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
|
||||||
|
"picture_description_local": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
||||||
|
"picture_description_api": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
|
||||||
|
},
|
||||||
|
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
|
||||||
|
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||||
|
DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
||||||
|
MISTRAL_OCR_API_KEY=request.app.state.config.MISTRAL_OCR_API_KEY,
|
||||||
|
)
|
||||||
|
docs = loader.load(
|
||||||
|
file.filename, file.meta.get("content_type"), file_path
|
||||||
|
)
|
||||||
|
|
||||||
|
docs = [
|
||||||
|
Document(
|
||||||
|
page_content=doc.page_content,
|
||||||
|
metadata={
|
||||||
|
**doc.metadata,
|
||||||
|
"name": file.filename,
|
||||||
|
"created_by": file.user_id,
|
||||||
|
"file_id": file.id,
|
||||||
|
"source": file.filename,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for doc in docs
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
docs = [
|
||||||
|
Document(
|
||||||
|
page_content=file.data.get("content", ""),
|
||||||
|
metadata={
|
||||||
|
**file.meta,
|
||||||
|
"name": file.filename,
|
||||||
|
"created_by": file.user_id,
|
||||||
|
"file_id": file.id,
|
||||||
|
"source": file.filename,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
text_content = " ".join([doc.page_content for doc in docs])
|
||||||
|
|
||||||
|
log.debug(f"text_content: {text_content}")
|
||||||
|
Files.update_file_data_by_id(
|
||||||
|
file.id,
|
||||||
|
{"content": text_content},
|
||||||
)
|
)
|
||||||
else:
|
hash = calculate_sha256_string(text_content)
|
||||||
raise HTTPException(
|
Files.update_file_hash_by_id(file.id, hash)
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e),
|
if request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL:
|
||||||
|
Files.update_file_data_by_id(file.id, {"status": "completed"})
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"collection_name": None,
|
||||||
|
"filename": file.filename,
|
||||||
|
"content": text_content,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
result = save_docs_to_vector_db(
|
||||||
|
request,
|
||||||
|
docs=docs,
|
||||||
|
collection_name=collection_name,
|
||||||
|
metadata={
|
||||||
|
"file_id": file.id,
|
||||||
|
"name": file.filename,
|
||||||
|
"hash": hash,
|
||||||
|
},
|
||||||
|
add=(True if form_data.collection_name else False),
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
log.info(f"added {len(docs)} items to collection {collection_name}")
|
||||||
|
|
||||||
|
if result:
|
||||||
|
Files.update_file_metadata_by_id(
|
||||||
|
file.id,
|
||||||
|
{
|
||||||
|
"collection_name": collection_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Files.update_file_data_by_id(
|
||||||
|
file.id,
|
||||||
|
{"status": "completed"},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"collection_name": collection_name,
|
||||||
|
"filename": file.filename,
|
||||||
|
"content": text_content,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise Exception("Error saving document to vector database")
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
Files.update_file_data_by_id(
|
||||||
|
file.id,
|
||||||
|
{"status": "failed"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "No pandoc was found" in str(e):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProcessTextForm(BaseModel):
|
class ProcessTextForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
|
@ -1769,7 +1793,15 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: add playwright to search the web
|
# TODO: add playwright to search the web
|
||||||
if engine == "searxng":
|
if engine == "ollama_cloud":
|
||||||
|
return search_ollama_cloud(
|
||||||
|
"https://ollama.com",
|
||||||
|
request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY,
|
||||||
|
query,
|
||||||
|
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
||||||
|
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
|
)
|
||||||
|
elif engine == "searxng":
|
||||||
if request.app.state.config.SEARXNG_QUERY_URL:
|
if request.app.state.config.SEARXNG_QUERY_URL:
|
||||||
return search_searxng(
|
return search_searxng(
|
||||||
request.app.state.config.SEARXNG_QUERY_URL,
|
request.app.state.config.SEARXNG_QUERY_URL,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from pydantic import BaseModel, HttpUrl
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.models.oauth_sessions import OAuthSessions
|
||||||
from open_webui.models.tools import (
|
from open_webui.models.tools import (
|
||||||
ToolForm,
|
ToolForm,
|
||||||
ToolModel,
|
ToolModel,
|
||||||
|
|
@ -41,7 +42,15 @@ 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.get_tools()
|
tools = [
|
||||||
|
ToolUserResponse(
|
||||||
|
**{
|
||||||
|
**tool.model_dump(),
|
||||||
|
"has_user_valves": "class UserValves(BaseModel):" in tool.content,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
|
@ -72,6 +81,20 @@ async def get_tools(request: Request, user=Depends(get_verified_user)):
|
||||||
# MCP Tool Servers
|
# MCP Tool Servers
|
||||||
for server in request.app.state.config.TOOL_SERVER_CONNECTIONS:
|
for server in request.app.state.config.TOOL_SERVER_CONNECTIONS:
|
||||||
if server.get("type", "openapi") == "mcp":
|
if server.get("type", "openapi") == "mcp":
|
||||||
|
server_id = server.get("info", {}).get("id")
|
||||||
|
auth_type = server.get("auth_type", "none")
|
||||||
|
|
||||||
|
session_token = None
|
||||||
|
if auth_type == "oauth_2.1":
|
||||||
|
splits = server_id.split(":")
|
||||||
|
server_id = splits[-1] if len(splits) > 1 else server_id
|
||||||
|
|
||||||
|
session_token = (
|
||||||
|
await request.app.state.oauth_client_manager.get_oauth_token(
|
||||||
|
user.id, f"mcp:{server_id}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
tools.append(
|
tools.append(
|
||||||
ToolUserResponse(
|
ToolUserResponse(
|
||||||
**{
|
**{
|
||||||
|
|
@ -88,6 +111,13 @@ async def get_tools(request: Request, user=Depends(get_verified_user)):
|
||||||
),
|
),
|
||||||
"updated_at": int(time.time()),
|
"updated_at": int(time.time()),
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"authenticated": session_token is not None,
|
||||||
|
}
|
||||||
|
if auth_type == "oauth_2.1"
|
||||||
|
else {}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -486,8 +516,9 @@ async def update_tools_valves_by_id(
|
||||||
try:
|
try:
|
||||||
form_data = {k: v for k, v in form_data.items() if v is not None}
|
form_data = {k: v for k, v in form_data.items() if v is not None}
|
||||||
valves = Valves(**form_data)
|
valves = Valves(**form_data)
|
||||||
Tools.update_tool_valves_by_id(id, valves.model_dump())
|
valves_dict = valves.model_dump(exclude_unset=True)
|
||||||
return valves.model_dump()
|
Tools.update_tool_valves_by_id(id, valves_dict)
|
||||||
|
return valves_dict
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Failed to update tool valves by id {id}: {e}")
|
log.exception(f"Failed to update tool valves by id {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -562,10 +593,11 @@ async def update_tools_user_valves_by_id(
|
||||||
try:
|
try:
|
||||||
form_data = {k: v for k, v in form_data.items() if v is not None}
|
form_data = {k: v for k, v in form_data.items() if v is not None}
|
||||||
user_valves = UserValves(**form_data)
|
user_valves = UserValves(**form_data)
|
||||||
|
user_valves_dict = user_valves.model_dump(exclude_unset=True)
|
||||||
Tools.update_user_valves_by_id_and_user_id(
|
Tools.update_user_valves_by_id_and_user_id(
|
||||||
id, user.id, user_valves.model_dump()
|
id, user.id, user_valves_dict
|
||||||
)
|
)
|
||||||
return user_valves.model_dump()
|
return user_valves_dict
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Failed to update user valves by id {id}: {e}")
|
log.exception(f"Failed to update user valves by id {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,13 @@ def has_access(
|
||||||
type: str = "write",
|
type: str = "write",
|
||||||
access_control: Optional[dict] = None,
|
access_control: Optional[dict] = None,
|
||||||
user_group_ids: Optional[Set[str]] = None,
|
user_group_ids: Optional[Set[str]] = None,
|
||||||
|
strict: bool = True,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if access_control is None:
|
if access_control is None:
|
||||||
return type == "read"
|
if strict:
|
||||||
|
return type == "read"
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
if user_group_ids is None:
|
if user_group_ids is None:
|
||||||
user_groups = Groups.get_groups_by_member_id(user_id)
|
user_groups = Groups.get_groups_by_member_id(user_id)
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,9 @@ class MCPClient:
|
||||||
self.session: Optional[ClientSession] = None
|
self.session: Optional[ClientSession] = None
|
||||||
self.exit_stack = AsyncExitStack()
|
self.exit_stack = AsyncExitStack()
|
||||||
|
|
||||||
async def connect(
|
async def connect(self, url: str, headers: Optional[dict] = None):
|
||||||
self, url: str, headers: Optional[dict] = None, auth: Optional[any] = None
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
self._streams_context = streamablehttp_client(
|
self._streams_context = streamablehttp_client(url, headers=headers)
|
||||||
url, headers=headers, auth=auth
|
|
||||||
)
|
|
||||||
|
|
||||||
transport = await self.exit_stack.enter_async_context(self._streams_context)
|
transport = await self.exit_stack.enter_async_context(self._streams_context)
|
||||||
read_stream, write_stream, _ = transport
|
read_stream, write_stream, _ = transport
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ from fastapi.responses import HTMLResponse
|
||||||
from starlette.responses import Response, StreamingResponse, JSONResponse
|
from starlette.responses import Response, StreamingResponse, JSONResponse
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.models.oauth_sessions import OAuthSessions
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
from open_webui.models.folders import Folders
|
from open_webui.models.folders import Folders
|
||||||
from open_webui.models.users import Users
|
from open_webui.models.users import Users
|
||||||
|
|
@ -1047,6 +1048,22 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
headers["Authorization"] = (
|
headers["Authorization"] = (
|
||||||
f"Bearer {oauth_token.get('access_token', '')}"
|
f"Bearer {oauth_token.get('access_token', '')}"
|
||||||
)
|
)
|
||||||
|
elif auth_type == "oauth_2.1":
|
||||||
|
try:
|
||||||
|
splits = server_id.split(":")
|
||||||
|
server_id = splits[-1] if len(splits) > 1 else server_id
|
||||||
|
|
||||||
|
oauth_token = await request.app.state.oauth_client_manager.get_oauth_token(
|
||||||
|
user.id, f"mcp:{server_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if oauth_token:
|
||||||
|
headers["Authorization"] = (
|
||||||
|
f"Bearer {oauth_token.get('access_token', '')}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth token: {e}")
|
||||||
|
oauth_token = None
|
||||||
|
|
||||||
mcp_client = MCPClient()
|
mcp_client = MCPClient()
|
||||||
await mcp_client.connect(
|
await mcp_client.connect(
|
||||||
|
|
@ -1171,26 +1188,15 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
raise Exception("No user message found")
|
raise Exception("No user message found")
|
||||||
|
|
||||||
if context_string != "":
|
if context_string != "":
|
||||||
# Workaround for Ollama 2.0+ system prompt issue
|
form_data["messages"] = add_or_update_user_message(
|
||||||
# TODO: replace with add_or_update_system_message
|
rag_template(
|
||||||
if model.get("owned_by") == "ollama":
|
request.app.state.config.RAG_TEMPLATE,
|
||||||
form_data["messages"] = prepend_to_first_user_message_content(
|
context_string,
|
||||||
rag_template(
|
prompt,
|
||||||
request.app.state.config.RAG_TEMPLATE,
|
),
|
||||||
context_string,
|
form_data["messages"],
|
||||||
prompt,
|
append=False,
|
||||||
),
|
)
|
||||||
form_data["messages"],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
form_data["messages"] = add_or_update_system_message(
|
|
||||||
rag_template(
|
|
||||||
request.app.state.config.RAG_TEMPLATE,
|
|
||||||
context_string,
|
|
||||||
prompt,
|
|
||||||
),
|
|
||||||
form_data["messages"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# If there are citations, add them to the data_items
|
# If there are citations, add them to the data_items
|
||||||
sources = [
|
sources = [
|
||||||
|
|
|
||||||
|
|
@ -120,19 +120,20 @@ def pop_system_message(messages: list[dict]) -> tuple[Optional[dict], list[dict]
|
||||||
return get_system_message(messages), remove_system_message(messages)
|
return get_system_message(messages), remove_system_message(messages)
|
||||||
|
|
||||||
|
|
||||||
def prepend_to_first_user_message_content(
|
def update_message_content(message: dict, content: str, append: bool = True) -> dict:
|
||||||
content: str, messages: list[dict]
|
if isinstance(message["content"], list):
|
||||||
) -> list[dict]:
|
for item in message["content"]:
|
||||||
for message in messages:
|
if item["type"] == "text":
|
||||||
if message["role"] == "user":
|
if append:
|
||||||
if isinstance(message["content"], list):
|
item["text"] = f"{item['text']}\n{content}"
|
||||||
for item in message["content"]:
|
else:
|
||||||
if item["type"] == "text":
|
item["text"] = f"{content}\n{item['text']}"
|
||||||
item["text"] = f"{content}\n{item['text']}"
|
else:
|
||||||
else:
|
if append:
|
||||||
message["content"] = f"{content}\n{message['content']}"
|
message["content"] = f"{message['content']}\n{content}"
|
||||||
break
|
else:
|
||||||
return messages
|
message["content"] = f"{content}\n{message['content']}"
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
def add_or_update_system_message(
|
def add_or_update_system_message(
|
||||||
|
|
@ -148,10 +149,7 @@ def add_or_update_system_message(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if messages and messages[0].get("role") == "system":
|
if messages and messages[0].get("role") == "system":
|
||||||
if append:
|
messages[0] = update_message_content(messages[0], content, append)
|
||||||
messages[0]["content"] = f"{messages[0]['content']}\n{content}"
|
|
||||||
else:
|
|
||||||
messages[0]["content"] = f"{content}\n{messages[0]['content']}"
|
|
||||||
else:
|
else:
|
||||||
# Insert at the beginning
|
# Insert at the beginning
|
||||||
messages.insert(0, {"role": "system", "content": content})
|
messages.insert(0, {"role": "system", "content": content})
|
||||||
|
|
@ -159,7 +157,7 @@ def add_or_update_system_message(
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
def add_or_update_user_message(content: str, messages: list[dict]):
|
def add_or_update_user_message(content: str, messages: list[dict], append: bool = True):
|
||||||
"""
|
"""
|
||||||
Adds a new user message at the end of the messages list
|
Adds a new user message at the end of the messages list
|
||||||
or updates the existing user message at the end.
|
or updates the existing user message at the end.
|
||||||
|
|
@ -170,7 +168,7 @@ def add_or_update_user_message(content: str, messages: list[dict]):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if messages and messages[-1].get("role") == "user":
|
if messages and messages[-1].get("role") == "user":
|
||||||
messages[-1]["content"] = f"{messages[-1]['content']}\n{content}"
|
messages[-1] = update_message_content(messages[-1], content, append)
|
||||||
else:
|
else:
|
||||||
# Insert at the end
|
# Insert at the end
|
||||||
messages.append({"role": "user", "content": content})
|
messages.append({"role": "user", "content": content})
|
||||||
|
|
@ -178,6 +176,16 @@ def add_or_update_user_message(content: str, messages: list[dict]):
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def prepend_to_first_user_message_content(
|
||||||
|
content: str, messages: list[dict]
|
||||||
|
) -> list[dict]:
|
||||||
|
for message in messages:
|
||||||
|
if message["role"] == "user":
|
||||||
|
message = update_message_content(message, content, append=False)
|
||||||
|
break
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
def append_or_update_assistant_message(content: str, messages: list[dict]):
|
def append_or_update_assistant_message(content: str, messages: list[dict]):
|
||||||
"""
|
"""
|
||||||
Adds a new assistant message at the end of the messages list
|
Adds a new assistant message at the end of the messages list
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import base64
|
import base64
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import sys
|
import sys
|
||||||
|
import urllib
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
@ -9,6 +11,9 @@ from datetime import datetime, timedelta
|
||||||
import re
|
import re
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import time
|
import time
|
||||||
|
import secrets
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from authlib.integrations.starlette_client import OAuth
|
from authlib.integrations.starlette_client import OAuth
|
||||||
|
|
@ -18,6 +23,7 @@ from fastapi import (
|
||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
from open_webui.models.auths import Auths
|
from open_webui.models.auths import Auths
|
||||||
|
|
@ -56,11 +62,27 @@ from open_webui.env import (
|
||||||
WEBUI_AUTH_COOKIE_SAME_SITE,
|
WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||||
WEBUI_AUTH_COOKIE_SECURE,
|
WEBUI_AUTH_COOKIE_SECURE,
|
||||||
ENABLE_OAUTH_ID_TOKEN_COOKIE,
|
ENABLE_OAUTH_ID_TOKEN_COOKIE,
|
||||||
|
OAUTH_CLIENT_INFO_ENCRYPTION_KEY,
|
||||||
)
|
)
|
||||||
from open_webui.utils.misc import parse_duration
|
from open_webui.utils.misc import parse_duration
|
||||||
from open_webui.utils.auth import get_password_hash, create_token
|
from open_webui.utils.auth import get_password_hash, create_token
|
||||||
from open_webui.utils.webhook import post_webhook
|
from open_webui.utils.webhook import post_webhook
|
||||||
|
|
||||||
|
from mcp.shared.auth import (
|
||||||
|
OAuthClientMetadata,
|
||||||
|
OAuthMetadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthClientInformationFull(OAuthClientMetadata):
|
||||||
|
issuer: Optional[str] = None # URL of the OAuth server that issued this client
|
||||||
|
|
||||||
|
client_id: str
|
||||||
|
client_secret: str | None = None
|
||||||
|
client_id_issued_at: int | None = None
|
||||||
|
client_secret_expires_at: int | None = None
|
||||||
|
|
||||||
|
|
||||||
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
|
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
|
||||||
|
|
||||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||||
|
|
@ -89,6 +111,42 @@ auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||||
auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN
|
auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN
|
||||||
|
|
||||||
|
|
||||||
|
FERNET = None
|
||||||
|
|
||||||
|
if len(OAUTH_CLIENT_INFO_ENCRYPTION_KEY) != 44:
|
||||||
|
key_bytes = hashlib.sha256(OAUTH_CLIENT_INFO_ENCRYPTION_KEY.encode()).digest()
|
||||||
|
OAUTH_CLIENT_INFO_ENCRYPTION_KEY = base64.urlsafe_b64encode(key_bytes)
|
||||||
|
else:
|
||||||
|
OAUTH_CLIENT_INFO_ENCRYPTION_KEY = OAUTH_CLIENT_INFO_ENCRYPTION_KEY.encode()
|
||||||
|
|
||||||
|
try:
|
||||||
|
FERNET = Fernet(OAUTH_CLIENT_INFO_ENCRYPTION_KEY)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error initializing Fernet with provided key: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_data(data) -> str:
|
||||||
|
"""Encrypt data for storage"""
|
||||||
|
try:
|
||||||
|
data_json = json.dumps(data)
|
||||||
|
encrypted = FERNET.encrypt(data_json.encode()).decode()
|
||||||
|
return encrypted
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error encrypting data: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_data(data: str):
|
||||||
|
"""Decrypt data from storage"""
|
||||||
|
try:
|
||||||
|
decrypted = FERNET.decrypt(data.encode()).decode()
|
||||||
|
return json.loads(decrypted)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error decrypting data: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def is_in_blocked_groups(group_name: str, groups: list) -> bool:
|
def is_in_blocked_groups(group_name: str, groups: list) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if a group name matches any blocked pattern.
|
Check if a group name matches any blocked pattern.
|
||||||
|
|
@ -133,6 +191,412 @@ def is_in_blocked_groups(group_name: str, groups: list) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_parsed_and_base_url(server_url) -> tuple[urllib.parse.ParseResult, str]:
|
||||||
|
parsed = urllib.parse.urlparse(server_url)
|
||||||
|
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
||||||
|
return parsed, base_url
|
||||||
|
|
||||||
|
|
||||||
|
def get_discovery_urls(server_url) -> list[str]:
|
||||||
|
urls = []
|
||||||
|
parsed, base_url = get_parsed_and_base_url(server_url)
|
||||||
|
|
||||||
|
urls.append(
|
||||||
|
urllib.parse.urljoin(base_url, "/.well-known/oauth-authorization-server")
|
||||||
|
)
|
||||||
|
urls.append(urllib.parse.urljoin(base_url, "/.well-known/openid-configuration"))
|
||||||
|
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Some OAuth providers require Initial Access Tokens (IATs) for dynamic client registration.
|
||||||
|
# This is not currently supported.
|
||||||
|
async def get_oauth_client_info_with_dynamic_client_registration(
|
||||||
|
request,
|
||||||
|
client_id: str,
|
||||||
|
oauth_server_url: str,
|
||||||
|
oauth_server_key: Optional[str] = None,
|
||||||
|
) -> OAuthClientInformationFull:
|
||||||
|
try:
|
||||||
|
oauth_server_metadata = None
|
||||||
|
oauth_server_metadata_url = None
|
||||||
|
|
||||||
|
redirect_base_url = (
|
||||||
|
str(request.app.state.config.WEBUI_URL or request.base_url)
|
||||||
|
).rstrip("/")
|
||||||
|
|
||||||
|
oauth_client_metadata = OAuthClientMetadata(
|
||||||
|
client_name="Open WebUI",
|
||||||
|
redirect_uris=[f"{redirect_base_url}/oauth/clients/{client_id}/callback"],
|
||||||
|
grant_types=["authorization_code", "refresh_token"],
|
||||||
|
response_types=["code"],
|
||||||
|
token_endpoint_auth_method="client_secret_post",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attempt to fetch OAuth server metadata to get registration endpoint & scopes
|
||||||
|
discovery_urls = get_discovery_urls(oauth_server_url)
|
||||||
|
for url in discovery_urls:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(
|
||||||
|
url, ssl=AIOHTTP_CLIENT_SESSION_SSL
|
||||||
|
) as oauth_server_metadata_response:
|
||||||
|
if oauth_server_metadata_response.status == 200:
|
||||||
|
try:
|
||||||
|
oauth_server_metadata = OAuthMetadata.model_validate(
|
||||||
|
await oauth_server_metadata_response.json()
|
||||||
|
)
|
||||||
|
oauth_server_metadata_url = url
|
||||||
|
if (
|
||||||
|
oauth_client_metadata.scope is None
|
||||||
|
and oauth_server_metadata.scopes_supported is not None
|
||||||
|
):
|
||||||
|
oauth_client_metadata.scope = " ".join(
|
||||||
|
oauth_server_metadata.scopes_supported
|
||||||
|
)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error parsing OAuth metadata from {url}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
registration_url = None
|
||||||
|
if oauth_server_metadata and oauth_server_metadata.registration_endpoint:
|
||||||
|
registration_url = str(oauth_server_metadata.registration_endpoint)
|
||||||
|
else:
|
||||||
|
_, base_url = get_parsed_and_base_url(oauth_server_url)
|
||||||
|
registration_url = urllib.parse.urljoin(base_url, "/register")
|
||||||
|
|
||||||
|
registration_data = oauth_client_metadata.model_dump(
|
||||||
|
exclude_none=True,
|
||||||
|
mode="json",
|
||||||
|
by_alias=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Perform dynamic client registration and return client info
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
registration_url, json=registration_data, ssl=AIOHTTP_CLIENT_SESSION_SSL
|
||||||
|
) as oauth_client_registration_response:
|
||||||
|
try:
|
||||||
|
registration_response_json = (
|
||||||
|
await oauth_client_registration_response.json()
|
||||||
|
)
|
||||||
|
oauth_client_info = OAuthClientInformationFull.model_validate(
|
||||||
|
{
|
||||||
|
**registration_response_json,
|
||||||
|
**{"issuer": oauth_server_metadata_url},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
log.info(
|
||||||
|
f"Dynamic client registration successful at {registration_url}, client_id: {oauth_client_info.client_id}"
|
||||||
|
)
|
||||||
|
return oauth_client_info
|
||||||
|
except Exception as e:
|
||||||
|
error_text = None
|
||||||
|
try:
|
||||||
|
error_text = await oauth_client_registration_response.text()
|
||||||
|
log.error(
|
||||||
|
f"Dynamic client registration failed at {registration_url}: {oauth_client_registration_response.status} - {error_text}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log.error(f"Error parsing client registration response: {e}")
|
||||||
|
raise Exception(
|
||||||
|
f"Dynamic client registration failed: {error_text}"
|
||||||
|
if error_text
|
||||||
|
else "Error parsing client registration response"
|
||||||
|
)
|
||||||
|
raise Exception("Dynamic client registration failed")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Exception during dynamic client registration: {e}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthClientManager:
|
||||||
|
def __init__(self, app):
|
||||||
|
self.oauth = OAuth()
|
||||||
|
self.app = app
|
||||||
|
self.clients = {}
|
||||||
|
|
||||||
|
def add_client(self, client_id, oauth_client_info: OAuthClientInformationFull):
|
||||||
|
self.clients[client_id] = {
|
||||||
|
"client": self.oauth.register(
|
||||||
|
name=client_id,
|
||||||
|
client_id=oauth_client_info.client_id,
|
||||||
|
client_secret=oauth_client_info.client_secret,
|
||||||
|
client_kwargs=(
|
||||||
|
{"scope": oauth_client_info.scope}
|
||||||
|
if oauth_client_info.scope
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
server_metadata_url=(
|
||||||
|
oauth_client_info.issuer if oauth_client_info.issuer else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"client_info": oauth_client_info,
|
||||||
|
}
|
||||||
|
return self.clients[client_id]
|
||||||
|
|
||||||
|
def remove_client(self, client_id):
|
||||||
|
if client_id in self.clients:
|
||||||
|
del self.clients[client_id]
|
||||||
|
log.info(f"Removed OAuth client {client_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_client(self, client_id):
|
||||||
|
client = self.clients.get(client_id)
|
||||||
|
return client["client"] if client else None
|
||||||
|
|
||||||
|
def get_client_info(self, client_id):
|
||||||
|
client = self.clients.get(client_id)
|
||||||
|
return client["client_info"] if client else None
|
||||||
|
|
||||||
|
def get_server_metadata_url(self, client_id):
|
||||||
|
if client_id in self.clients:
|
||||||
|
client = self.clients[client_id]
|
||||||
|
return (
|
||||||
|
client.server_metadata_url
|
||||||
|
if hasattr(client, "server_metadata_url")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_oauth_token(
|
||||||
|
self, user_id: str, client_id: str, force_refresh: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get a valid OAuth token for the user, automatically refreshing if needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user ID
|
||||||
|
client_id: The OAuth client ID (provider)
|
||||||
|
force_refresh: Force token refresh even if current token appears valid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: OAuth token data with access_token, or None if no valid token available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the OAuth session
|
||||||
|
session = OAuthSessions.get_session_by_provider_and_user_id(
|
||||||
|
client_id, user_id
|
||||||
|
)
|
||||||
|
if not session:
|
||||||
|
log.warning(
|
||||||
|
f"No OAuth session found for user {user_id}, client_id {client_id}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if force_refresh or datetime.now() + timedelta(
|
||||||
|
minutes=5
|
||||||
|
) >= datetime.fromtimestamp(session.expires_at):
|
||||||
|
log.debug(
|
||||||
|
f"Token refresh needed for user {user_id}, client_id {session.provider}"
|
||||||
|
)
|
||||||
|
refreshed_token = await self._refresh_token(session)
|
||||||
|
if refreshed_token:
|
||||||
|
return refreshed_token
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
f"Token refresh failed for user {user_id}, client_id {session.provider}, deleting session {session.id}"
|
||||||
|
)
|
||||||
|
OAuthSessions.delete_session_by_id(session.id)
|
||||||
|
return None
|
||||||
|
return session.token
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth token for user {user_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _refresh_token(self, session) -> dict:
|
||||||
|
"""
|
||||||
|
Refresh an OAuth token if needed, with concurrency protection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: The OAuth session object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Refreshed token data, or None if refresh failed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Perform the actual refresh
|
||||||
|
refreshed_token = await self._perform_token_refresh(session)
|
||||||
|
|
||||||
|
if refreshed_token:
|
||||||
|
# Update the session with new token data
|
||||||
|
session = OAuthSessions.update_session_by_id(
|
||||||
|
session.id, refreshed_token
|
||||||
|
)
|
||||||
|
log.info(f"Successfully refreshed token for session {session.id}")
|
||||||
|
return session.token
|
||||||
|
else:
|
||||||
|
log.error(f"Failed to refresh token for session {session.id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error refreshing token for session {session.id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _perform_token_refresh(self, session) -> dict:
|
||||||
|
"""
|
||||||
|
Perform the actual OAuth token refresh.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: The OAuth session object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: New token data, or None if refresh failed
|
||||||
|
"""
|
||||||
|
client_id = session.provider
|
||||||
|
token_data = session.token
|
||||||
|
|
||||||
|
if not token_data.get("refresh_token"):
|
||||||
|
log.warning(f"No refresh token available for session {session.id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = self.get_client(client_id)
|
||||||
|
if not client:
|
||||||
|
log.error(f"No OAuth client found for provider {client_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
token_endpoint = None
|
||||||
|
async with aiohttp.ClientSession(trust_env=True) as session_http:
|
||||||
|
async with session_http.get(
|
||||||
|
self.get_server_metadata_url(client_id)
|
||||||
|
) as r:
|
||||||
|
if r.status == 200:
|
||||||
|
openid_data = await r.json()
|
||||||
|
token_endpoint = openid_data.get("token_endpoint")
|
||||||
|
else:
|
||||||
|
log.error(
|
||||||
|
f"Failed to fetch OpenID configuration for client_id {client_id}"
|
||||||
|
)
|
||||||
|
if not token_endpoint:
|
||||||
|
log.error(f"No token endpoint found for client_id {client_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Prepare refresh request
|
||||||
|
refresh_data = {
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": token_data["refresh_token"],
|
||||||
|
"client_id": client.client_id,
|
||||||
|
}
|
||||||
|
if hasattr(client, "client_secret") and client.client_secret:
|
||||||
|
refresh_data["client_secret"] = client.client_secret
|
||||||
|
|
||||||
|
# Make refresh request
|
||||||
|
async with aiohttp.ClientSession(trust_env=True) as session_http:
|
||||||
|
async with session_http.post(
|
||||||
|
token_endpoint,
|
||||||
|
data=refresh_data,
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
|
) as r:
|
||||||
|
if r.status == 200:
|
||||||
|
new_token_data = await r.json()
|
||||||
|
|
||||||
|
# Merge with existing token data (preserve refresh_token if not provided)
|
||||||
|
if "refresh_token" not in new_token_data:
|
||||||
|
new_token_data["refresh_token"] = token_data[
|
||||||
|
"refresh_token"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add timestamp for tracking
|
||||||
|
new_token_data["issued_at"] = datetime.now().timestamp()
|
||||||
|
|
||||||
|
# Calculate expires_at if we have expires_in
|
||||||
|
if (
|
||||||
|
"expires_in" in new_token_data
|
||||||
|
and "expires_at" not in new_token_data
|
||||||
|
):
|
||||||
|
new_token_data["expires_at"] = int(
|
||||||
|
datetime.now().timestamp()
|
||||||
|
+ new_token_data["expires_in"]
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug(f"Token refresh successful for client_id {client_id}")
|
||||||
|
return new_token_data
|
||||||
|
else:
|
||||||
|
error_text = await r.text()
|
||||||
|
log.error(
|
||||||
|
f"Token refresh failed for client_id {client_id}: {r.status} - {error_text}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Exception during token refresh for client_id {client_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def handle_authorize(self, request, client_id: str) -> RedirectResponse:
|
||||||
|
client = self.get_client(client_id)
|
||||||
|
if client is None:
|
||||||
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
client_info = self.get_client_info(client_id)
|
||||||
|
if client_info is None:
|
||||||
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
redirect_uri = (
|
||||||
|
client_info.redirect_uris[0] if client_info.redirect_uris else None
|
||||||
|
)
|
||||||
|
return await client.authorize_redirect(request, str(redirect_uri))
|
||||||
|
|
||||||
|
async def handle_callback(self, request, client_id: str, user_id: str, response):
|
||||||
|
client = self.get_client(client_id)
|
||||||
|
if client is None:
|
||||||
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
error_message = None
|
||||||
|
try:
|
||||||
|
token = await client.authorize_access_token(request)
|
||||||
|
if token:
|
||||||
|
try:
|
||||||
|
# Add timestamp for tracking
|
||||||
|
token["issued_at"] = datetime.now().timestamp()
|
||||||
|
|
||||||
|
# Calculate expires_at if we have expires_in
|
||||||
|
if "expires_in" in token and "expires_at" not in token:
|
||||||
|
token["expires_at"] = (
|
||||||
|
datetime.now().timestamp() + token["expires_in"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up any existing sessions for this user/client_id first
|
||||||
|
sessions = OAuthSessions.get_sessions_by_user_id(user_id)
|
||||||
|
for session in sessions:
|
||||||
|
if session.provider == client_id:
|
||||||
|
OAuthSessions.delete_session_by_id(session.id)
|
||||||
|
|
||||||
|
session = OAuthSessions.create_session(
|
||||||
|
user_id=user_id,
|
||||||
|
provider=client_id,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
log.info(
|
||||||
|
f"Stored OAuth session server-side for user {user_id}, client_id {client_id}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
error_message = "Failed to store OAuth session server-side"
|
||||||
|
log.error(f"Failed to store OAuth session server-side: {e}")
|
||||||
|
else:
|
||||||
|
error_message = "Failed to obtain OAuth token"
|
||||||
|
log.warning(error_message)
|
||||||
|
except Exception as e:
|
||||||
|
error_message = "OAuth callback error"
|
||||||
|
log.warning(f"OAuth callback error: {e}")
|
||||||
|
|
||||||
|
redirect_url = (
|
||||||
|
str(request.app.state.config.WEBUI_URL or request.base_url)
|
||||||
|
).rstrip("/")
|
||||||
|
|
||||||
|
if error_message:
|
||||||
|
log.debug(error_message)
|
||||||
|
redirect_url = f"{redirect_url}/?error={error_message}"
|
||||||
|
return RedirectResponse(url=redirect_url, headers=response.headers)
|
||||||
|
|
||||||
|
response = RedirectResponse(url=redirect_url, headers=response.headers)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class OAuthManager:
|
class OAuthManager:
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.oauth = OAuth()
|
self.oauth = OAuth()
|
||||||
|
|
@ -191,8 +655,10 @@ class OAuthManager:
|
||||||
return refreshed_token
|
return refreshed_token
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(
|
||||||
f"Token refresh failed for user {user_id}, provider {session.provider}"
|
f"Token refresh failed for user {user_id}, provider {session.provider}, deleting session {session.id}"
|
||||||
)
|
)
|
||||||
|
OAuthSessions.delete_session_by_id(session.id)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
return session.token
|
return session.token
|
||||||
|
|
||||||
|
|
@ -252,9 +718,10 @@ class OAuthManager:
|
||||||
log.error(f"No OAuth client found for provider {provider}")
|
log.error(f"No OAuth client found for provider {provider}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
server_metadata_url = self.get_server_metadata_url(provider)
|
||||||
token_endpoint = None
|
token_endpoint = None
|
||||||
async with aiohttp.ClientSession(trust_env=True) as session_http:
|
async with aiohttp.ClientSession(trust_env=True) as session_http:
|
||||||
async with session_http.get(client.gserver_metadata_url) as r:
|
async with session_http.get(server_metadata_url) as r:
|
||||||
if r.status == 200:
|
if r.status == 200:
|
||||||
openid_data = await r.json()
|
openid_data = await r.json()
|
||||||
token_endpoint = openid_data.get("token_endpoint")
|
token_endpoint = openid_data.get("token_endpoint")
|
||||||
|
|
@ -301,7 +768,7 @@ class OAuthManager:
|
||||||
"expires_in" in new_token_data
|
"expires_in" in new_token_data
|
||||||
and "expires_at" not in new_token_data
|
and "expires_at" not in new_token_data
|
||||||
):
|
):
|
||||||
new_token_data["expires_at"] = (
|
new_token_data["expires_at"] = int(
|
||||||
datetime.now().timestamp()
|
datetime.now().timestamp()
|
||||||
+ new_token_data["expires_in"]
|
+ new_token_data["expires_in"]
|
||||||
)
|
)
|
||||||
|
|
@ -574,7 +1041,7 @@ class OAuthManager:
|
||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
# If the provider has a custom redirect URL, use that, otherwise automatically generate one
|
# If the provider has a custom redirect URL, use that, otherwise automatically generate one
|
||||||
redirect_uri = OAUTH_PROVIDERS[provider].get("redirect_uri") or request.url_for(
|
redirect_uri = OAUTH_PROVIDERS[provider].get("redirect_uri") or request.url_for(
|
||||||
"oauth_callback", provider=provider
|
"oauth_login_callback", provider=provider
|
||||||
)
|
)
|
||||||
client = self.get_client(provider)
|
client = self.get_client(provider)
|
||||||
if client is None:
|
if client is None:
|
||||||
|
|
@ -791,9 +1258,9 @@ class OAuthManager:
|
||||||
else ERROR_MESSAGES.DEFAULT("Error during OAuth process")
|
else ERROR_MESSAGES.DEFAULT("Error during OAuth process")
|
||||||
)
|
)
|
||||||
|
|
||||||
redirect_base_url = str(request.app.state.config.WEBUI_URL or request.base_url)
|
redirect_base_url = (
|
||||||
if redirect_base_url.endswith("/"):
|
str(request.app.state.config.WEBUI_URL or request.base_url)
|
||||||
redirect_base_url = redirect_base_url[:-1]
|
).rstrip("/")
|
||||||
redirect_url = f"{redirect_base_url}/auth"
|
redirect_url = f"{redirect_base_url}/auth"
|
||||||
|
|
||||||
if error_message:
|
if error_message:
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,14 @@ python-jose==3.4.0
|
||||||
passlib[bcrypt]==1.7.4
|
passlib[bcrypt]==1.7.4
|
||||||
cryptography
|
cryptography
|
||||||
|
|
||||||
requests==2.32.4
|
requests==2.32.5
|
||||||
aiohttp==3.12.15
|
aiohttp==3.12.15
|
||||||
async-timeout
|
async-timeout
|
||||||
aiocache
|
aiocache
|
||||||
aiofiles
|
aiofiles
|
||||||
starlette-compress==1.6.0
|
starlette-compress==1.6.0
|
||||||
httpx[socks,http2,zstd,cli,brotli]==0.28.1
|
httpx[socks,http2,zstd,cli,brotli]==0.28.1
|
||||||
|
starsessions[redis]==2.2.1
|
||||||
|
|
||||||
sqlalchemy==2.0.38
|
sqlalchemy==2.0.38
|
||||||
alembic==1.14.0
|
alembic==1.14.0
|
||||||
|
|
@ -43,13 +44,13 @@ asgiref==3.8.1
|
||||||
# AI libraries
|
# AI libraries
|
||||||
openai
|
openai
|
||||||
anthropic
|
anthropic
|
||||||
google-genai==1.32.0
|
google-genai==1.38.0
|
||||||
google-generativeai==0.8.5
|
google-generativeai==0.8.5
|
||||||
tiktoken
|
tiktoken
|
||||||
mcp==1.14.1
|
mcp==1.14.1
|
||||||
|
|
||||||
langchain==0.3.26
|
langchain==0.3.27
|
||||||
langchain-community==0.3.27
|
langchain-community==0.3.29
|
||||||
|
|
||||||
fake-useragent==2.2.0
|
fake-useragent==2.2.0
|
||||||
chromadb==1.0.20
|
chromadb==1.0.20
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ dependencies = [
|
||||||
"PyJWT[crypto]==2.10.1",
|
"PyJWT[crypto]==2.10.1",
|
||||||
"authlib==1.6.3",
|
"authlib==1.6.3",
|
||||||
|
|
||||||
"requests==2.32.4",
|
"requests==2.32.5",
|
||||||
"aiohttp==3.12.15",
|
"aiohttp==3.12.15",
|
||||||
"async-timeout",
|
"async-timeout",
|
||||||
"aiocache",
|
"aiocache",
|
||||||
|
|
@ -51,11 +51,11 @@ dependencies = [
|
||||||
|
|
||||||
"openai",
|
"openai",
|
||||||
"anthropic",
|
"anthropic",
|
||||||
"google-genai==1.32.0",
|
"google-genai==1.38.0",
|
||||||
"google-generativeai==0.8.5",
|
"google-generativeai==0.8.5",
|
||||||
|
|
||||||
"langchain==0.3.26",
|
"langchain==0.3.27",
|
||||||
"langchain-community==0.3.27",
|
"langchain-community==0.3.29",
|
||||||
|
|
||||||
"fake-useragent==2.2.0",
|
"fake-useragent==2.2.0",
|
||||||
"chromadb==1.0.20",
|
"chromadb==1.0.20",
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,11 @@ export const importChat = async (
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getChatList = async (token: string = '', page: number | null = null) => {
|
export const getChatList = async (
|
||||||
|
token: string = '',
|
||||||
|
page: number | null = null,
|
||||||
|
include_folders: boolean = false
|
||||||
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
|
@ -85,6 +89,10 @@ export const getChatList = async (token: string = '', page: number | null = null
|
||||||
searchParams.append('page', `${page}`);
|
searchParams.append('page', `${page}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (include_folders) {
|
||||||
|
searchParams.append('include_folders', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import type { Banner } from '$lib/types';
|
import type { Banner } from '$lib/types';
|
||||||
|
|
||||||
export const importConfig = async (token: string, config) => {
|
export const importConfig = async (token: string, config) => {
|
||||||
|
|
@ -202,6 +202,52 @@ export const verifyToolServerConnection = async (token: string, connection: obje
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RegisterOAuthClientForm = {
|
||||||
|
url: string;
|
||||||
|
client_id: string;
|
||||||
|
client_name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerOAuthClient = async (
|
||||||
|
token: string,
|
||||||
|
formData: RegisterOAuthClientForm,
|
||||||
|
type: null | string = null
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const searchParams = type ? `?type=${type}` : '';
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/oauth/clients/register${searchParams}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...formData
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOAuthClientAuthorizationUrl = (clientId: string, type: null | string = null) => {
|
||||||
|
const oauthClientId = type ? `${type}:${clientId}` : clientId;
|
||||||
|
return `${WEBUI_BASE_URL}/oauth/clients/${oauthClientId}/authorize`;
|
||||||
|
};
|
||||||
|
|
||||||
export const getCodeExecutionConfig = async (token: string) => {
|
export const getCodeExecutionConfig = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import Tags from './common/Tags.svelte';
|
import Tags from './common/Tags.svelte';
|
||||||
import { getToolServerData } from '$lib/apis';
|
import { getToolServerData } from '$lib/apis';
|
||||||
import { verifyToolServerConnection } from '$lib/apis/configs';
|
import { verifyToolServerConnection, registerOAuthClient } from '$lib/apis/configs';
|
||||||
import AccessControl from './workspace/common/AccessControl.svelte';
|
import AccessControl from './workspace/common/AccessControl.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
@ -41,10 +41,47 @@
|
||||||
let name = '';
|
let name = '';
|
||||||
let description = '';
|
let description = '';
|
||||||
|
|
||||||
let enable = true;
|
let oauthClientInfo = null;
|
||||||
|
|
||||||
|
let enable = true;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
|
const registerOAuthClientHandler = async () => {
|
||||||
|
if (url === '') {
|
||||||
|
toast.error($i18n.t('Please enter a valid URL'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id === '') {
|
||||||
|
toast.error($i18n.t('Please enter a valid ID'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await registerOAuthClient(
|
||||||
|
localStorage.token,
|
||||||
|
{
|
||||||
|
url: url,
|
||||||
|
client_id: id
|
||||||
|
},
|
||||||
|
'mcp'
|
||||||
|
).catch((err) => {
|
||||||
|
toast.error($i18n.t('Registration failed'));
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.warning(
|
||||||
|
$i18n.t(
|
||||||
|
'Please save the connection to persist the OAuth client information and do not change the ID'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
toast.success($i18n.t('Registration successful'));
|
||||||
|
|
||||||
|
console.debug('Registration successful', res);
|
||||||
|
oauthClientInfo = res?.oauth_client_info ?? null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const verifyHandler = async () => {
|
const verifyHandler = async () => {
|
||||||
if (url === '') {
|
if (url === '') {
|
||||||
toast.error($i18n.t('Please enter a valid URL'));
|
toast.error($i18n.t('Please enter a valid URL'));
|
||||||
|
|
@ -106,6 +143,12 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'mcp' && auth_type === 'oauth_2.1' && !oauthClientInfo) {
|
||||||
|
toast.error($i18n.t('Please register the OAuth client'));
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const connection = {
|
const connection = {
|
||||||
url,
|
url,
|
||||||
path,
|
path,
|
||||||
|
|
@ -119,7 +162,8 @@
|
||||||
info: {
|
info: {
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
description: description
|
description: description,
|
||||||
|
...(oauthClientInfo ? { oauth_client_info: oauthClientInfo } : {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -139,6 +183,7 @@
|
||||||
id = '';
|
id = '';
|
||||||
name = '';
|
name = '';
|
||||||
description = '';
|
description = '';
|
||||||
|
oauthClientInfo = null;
|
||||||
|
|
||||||
enable = true;
|
enable = true;
|
||||||
accessControl = null;
|
accessControl = null;
|
||||||
|
|
@ -156,6 +201,7 @@
|
||||||
id = connection.info?.id ?? '';
|
id = connection.info?.id ?? '';
|
||||||
name = connection.info?.name ?? '';
|
name = connection.info?.name ?? '';
|
||||||
description = connection.info?.description ?? '';
|
description = connection.info?.description ?? '';
|
||||||
|
oauthClientInfo = connection.info?.oauth_client_info ?? null;
|
||||||
|
|
||||||
enable = connection.config?.enable ?? true;
|
enable = connection.config?.enable ?? true;
|
||||||
accessControl = connection.config?.access_control ?? null;
|
accessControl = connection.config?.access_control ?? null;
|
||||||
|
|
@ -227,25 +273,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if type === 'mcp'}
|
|
||||||
<div
|
|
||||||
class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-2xl text-xs px-4 py-3 mb-2"
|
|
||||||
>
|
|
||||||
<span class="font-medium">
|
|
||||||
{$i18n.t('Warning')}:
|
|
||||||
</span>
|
|
||||||
{$i18n.t(
|
|
||||||
'MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.'
|
|
||||||
)}
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="font-medium underline"
|
|
||||||
href="https://docs.openwebui.com/features/mcp"
|
|
||||||
target="_blank">{$i18n.t('Read more →')}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class="flex justify-between mb-0.5">
|
<div class="flex justify-between mb-0.5">
|
||||||
|
|
@ -333,11 +360,52 @@
|
||||||
|
|
||||||
<div class="flex gap-2 mt-2">
|
<div class="flex gap-2 mt-2">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<label
|
<div class="flex justify-between items-center">
|
||||||
for="select-bearer-or-session"
|
<div class="flex gap-2 items-center">
|
||||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
<div
|
||||||
>{$i18n.t('Auth')}</label
|
for="select-bearer-or-session"
|
||||||
>
|
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('Auth')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if auth_type === 'oauth_2.1'}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex flex-col justify-end items-center shrink-0">
|
||||||
|
<Tooltip
|
||||||
|
content={oauthClientInfo
|
||||||
|
? $i18n.t('Register Again')
|
||||||
|
: $i18n.t('Register Client')}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class=" text-xs underline dark:text-gray-500 dark:hover:text-gray-200 text-gray-700 hover:text-gray-900 transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
registerOAuthClientHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Register Client')}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !oauthClientInfo}
|
||||||
|
<div
|
||||||
|
class="text-xs font-medium px-1.5 rounded-md bg-yellow-500/20 text-yellow-700 dark:text-yellow-200"
|
||||||
|
>
|
||||||
|
{$i18n.t('Not Registered')}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="text-xs font-medium px-1.5 rounded-md bg-green-500/20 text-green-700 dark:text-green-200"
|
||||||
|
>
|
||||||
|
{$i18n.t('Registered')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="flex-shrink-0 self-start">
|
<div class="flex-shrink-0 self-start">
|
||||||
|
|
@ -353,6 +421,9 @@
|
||||||
|
|
||||||
{#if !direct}
|
{#if !direct}
|
||||||
<option value="system_oauth">{$i18n.t('OAuth')}</option>
|
<option value="system_oauth">{$i18n.t('OAuth')}</option>
|
||||||
|
{#if type === 'mcp'}
|
||||||
|
<option value="oauth_2.1">{$i18n.t('OAuth 2.1')}</option>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -382,6 +453,12 @@
|
||||||
>
|
>
|
||||||
{$i18n.t('Forwards system user OAuth access token to authenticate')}
|
{$i18n.t('Forwards system user OAuth access token to authenticate')}
|
||||||
</div>
|
</div>
|
||||||
|
{:else if auth_type === 'oauth_2.1'}
|
||||||
|
<div
|
||||||
|
class={`flex items-center text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('Uses OAuth 2.1 Dynamic Client Registration')}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -470,6 +547,25 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if type === 'mcp'}
|
||||||
|
<div
|
||||||
|
class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-2xl text-xs px-4 py-3 mb-2"
|
||||||
|
>
|
||||||
|
<span class="font-medium">
|
||||||
|
{$i18n.t('Warning')}:
|
||||||
|
</span>
|
||||||
|
{$i18n.t(
|
||||||
|
'MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.'
|
||||||
|
)}
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="font-medium underline"
|
||||||
|
href="https://docs.openwebui.com/features/mcp"
|
||||||
|
target="_blank">{$i18n.t('Read more →')}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
||||||
{#if edit}
|
{#if edit}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full">
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full">
|
||||||
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||||
<tr class=" border-b-2 border-gray-100 dark:border-gray-800">
|
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
||||||
|
|
|
||||||
|
|
@ -389,7 +389,7 @@
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||||
<tr class=" border-b-2 border-gray-100 dark:border-gray-800">
|
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
@ -366,22 +367,20 @@ class Pipe:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
||||||
{#await import('$lib/components/common/CodeEditor.svelte') then { default: CodeEditor }}
|
<CodeEditor
|
||||||
<CodeEditor
|
bind:this={codeEditor}
|
||||||
bind:this={codeEditor}
|
value={content}
|
||||||
value={content}
|
lang="python"
|
||||||
lang="python"
|
{boilerplate}
|
||||||
{boilerplate}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
_content = e;
|
||||||
_content = e;
|
}}
|
||||||
}}
|
onSave={async () => {
|
||||||
onSave={async () => {
|
if (formElement) {
|
||||||
if (formElement) {
|
formElement.requestSubmit();
|
||||||
formElement.requestSubmit();
|
}
|
||||||
}
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
{/await}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pb-3 flex justify-between">
|
<div class="pb-3 flex justify-between">
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
let webSearchEngines = [
|
let webSearchEngines = [
|
||||||
|
'ollama_cloud',
|
||||||
'searxng',
|
'searxng',
|
||||||
'yacy',
|
'yacy',
|
||||||
'google_pse',
|
'google_pse',
|
||||||
|
|
@ -130,7 +131,24 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if webConfig.WEB_SEARCH_ENGINE !== ''}
|
{#if webConfig.WEB_SEARCH_ENGINE !== ''}
|
||||||
{#if webConfig.WEB_SEARCH_ENGINE === 'searxng'}
|
{#if webConfig.WEB_SEARCH_ENGINE === 'ollama_cloud'}
|
||||||
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
|
<div>
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Ollama Cloud API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<SensitiveInput
|
||||||
|
placeholder={$i18n.t('Enter Ollama Cloud API Key')}
|
||||||
|
bind:value={webConfig.OLLAMA_CLOUD_WEB_SEARCH_API_KEY}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if webConfig.WEB_SEARCH_ENGINE === 'searxng'}
|
||||||
<div class="mb-2.5 flex w-full flex-col">
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div>
|
<div>
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
import Drawer from '../common/Drawer.svelte';
|
import Drawer from '../common/Drawer.svelte';
|
||||||
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
||||||
import Thread from './Thread.svelte';
|
import Thread from './Thread.svelte';
|
||||||
|
import i18n from '$lib/i18n';
|
||||||
|
|
||||||
export let id = '';
|
export let id = '';
|
||||||
|
|
||||||
|
|
@ -252,6 +253,10 @@
|
||||||
{typingUsers}
|
{typingUsers}
|
||||||
userSuggestions={true}
|
userSuggestions={true}
|
||||||
channelSuggestions={true}
|
channelSuggestions={true}
|
||||||
|
disabled={!channel?.write_access}
|
||||||
|
placeholder={!channel?.write_access
|
||||||
|
? $i18n.t('You do not have permission to send messages in this channel.')
|
||||||
|
: $i18n.t('Type here...')}
|
||||||
{onChange}
|
{onChange}
|
||||||
onSubmit={submitHandler}
|
onSubmit={submitHandler}
|
||||||
{scrollToBottom}
|
{scrollToBottom}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
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';
|
||||||
|
|
||||||
export let placeholder = $i18n.t('Send a Message');
|
export let placeholder = $i18n.t('Type here...');
|
||||||
|
|
||||||
export let id = null;
|
export let id = null;
|
||||||
export let chatInputElement;
|
export let chatInputElement;
|
||||||
|
|
@ -53,6 +53,7 @@
|
||||||
export let scrollEnd = true;
|
export let scrollEnd = true;
|
||||||
export let scrollToBottom: Function = () => {};
|
export let scrollToBottom: Function = () => {};
|
||||||
|
|
||||||
|
export let disabled = false;
|
||||||
export let acceptFiles = true;
|
export let acceptFiles = true;
|
||||||
export let showFormattingToolbar = true;
|
export let showFormattingToolbar = true;
|
||||||
|
|
||||||
|
|
@ -731,7 +732,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="">
|
<div
|
||||||
|
class="{disabled ? 'opacity-50 pointer-events-none cursor-not-allowed' : ''} relative z-20"
|
||||||
|
>
|
||||||
{#if recording}
|
{#if recording}
|
||||||
<VoiceRecording
|
<VoiceRecording
|
||||||
bind:recording
|
bind:recording
|
||||||
|
|
@ -837,6 +840,8 @@
|
||||||
bind:this={chatInputElement}
|
bind:this={chatInputElement}
|
||||||
json={true}
|
json={true}
|
||||||
messageInput={true}
|
messageInput={true}
|
||||||
|
editable={!disabled}
|
||||||
|
{placeholder}
|
||||||
richText={$settings?.richTextInput ?? true}
|
richText={$settings?.richTextInput ?? true}
|
||||||
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
|
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
|
||||||
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@
|
||||||
<Message
|
<Message
|
||||||
{message}
|
{message}
|
||||||
{thread}
|
{thread}
|
||||||
|
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}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
export let message;
|
export let message;
|
||||||
export let showUserProfile = true;
|
export let showUserProfile = true;
|
||||||
export let thread = false;
|
export let thread = false;
|
||||||
|
export let disabled = false;
|
||||||
|
|
||||||
export let onDelete: Function = () => {};
|
export let onDelete: Function = () => {};
|
||||||
export let onEdit: Function = () => {};
|
export let onEdit: Function = () => {};
|
||||||
|
|
@ -68,7 +69,7 @@
|
||||||
? '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"
|
||||||
>
|
>
|
||||||
{#if !edit}
|
{#if !edit && !disabled}
|
||||||
<div
|
<div
|
||||||
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
|
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
import Messages from './Messages.svelte';
|
import Messages from './Messages.svelte';
|
||||||
import { onDestroy, onMount, tick, getContext } from 'svelte';
|
import { onDestroy, onMount, tick, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import Spinner from '../common/Spinner.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -175,32 +176,42 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" max-h-full w-full overflow-y-auto" bind:this={messagesContainerElement}>
|
<div class=" max-h-full w-full overflow-y-auto" bind:this={messagesContainerElement}>
|
||||||
<Messages
|
{#if messages !== null}
|
||||||
id={threadId}
|
<Messages
|
||||||
{channel}
|
id={threadId}
|
||||||
{messages}
|
{channel}
|
||||||
{top}
|
{messages}
|
||||||
thread={true}
|
{top}
|
||||||
onLoad={async () => {
|
thread={true}
|
||||||
const newMessages = await getChannelThreadMessages(
|
onLoad={async () => {
|
||||||
localStorage.token,
|
const newMessages = await getChannelThreadMessages(
|
||||||
channel.id,
|
localStorage.token,
|
||||||
threadId,
|
channel.id,
|
||||||
messages.length
|
threadId,
|
||||||
);
|
messages.length
|
||||||
|
);
|
||||||
|
|
||||||
messages = [...messages, ...newMessages];
|
messages = [...messages, ...newMessages];
|
||||||
|
|
||||||
if (newMessages.length < 50) {
|
if (newMessages.length < 50) {
|
||||||
top = true;
|
top = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="w-full flex justify-center pt-5 pb-10">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class=" pb-[1rem] px-2.5 w-full">
|
<div class=" pb-[1rem] px-2.5 w-full">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
id={threadId}
|
id={threadId}
|
||||||
|
disabled={!channel?.write_access}
|
||||||
|
placeholder={!channel?.write_access
|
||||||
|
? $i18n.t('You do not have permission to send messages in this thread.')
|
||||||
|
: $i18n.t('Reply to thread...')}
|
||||||
typingUsersClassName="from-gray-50 dark:from-gray-850"
|
typingUsersClassName="from-gray-50 dark:from-gray-850"
|
||||||
{typingUsers}
|
{typingUsers}
|
||||||
userSuggestions={true}
|
userSuggestions={true}
|
||||||
|
|
|
||||||
|
|
@ -1415,6 +1415,9 @@
|
||||||
console.error('OneDrive Error:', error);
|
console.error('OneDrive Error:', error);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onUpload={async (e) => {
|
||||||
|
dispatch('upload', e);
|
||||||
|
}}
|
||||||
onClose={async () => {
|
onClose={async () => {
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
import { settings } from '$lib/stores';
|
||||||
|
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import { isValidHttpUrl, isYoutubeUrl } from '$lib/utils';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let onSubmit: (e) => void;
|
||||||
|
|
||||||
|
let url = '';
|
||||||
|
|
||||||
|
const submitHandler = () => {
|
||||||
|
if (isValidHttpUrl(url)) {
|
||||||
|
onSubmit({
|
||||||
|
type: isYoutubeUrl(url) ? 'youtube' : 'web',
|
||||||
|
data: url
|
||||||
|
});
|
||||||
|
|
||||||
|
show = false;
|
||||||
|
url = '';
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t('Please enter a valid URL.'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:show size="sm">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="flex justify-between items-center dark:text-gray-100 px-5 pt-4 pb-1.5">
|
||||||
|
<h1 class="text-lg font-medium self-center font-primary">
|
||||||
|
{$i18n.t('Attach Webpage')}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
aria-label={$i18n.t('Close modal')}
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-5 pb-4">
|
||||||
|
<form
|
||||||
|
on:submit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex justify-between mb-0.5">
|
||||||
|
<label
|
||||||
|
for="webpage-url"
|
||||||
|
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>{$i18n.t('Webpage URL')}</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="webpage-url"
|
||||||
|
class={`w-full flex-1 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"
|
||||||
|
bind:value={url}
|
||||||
|
placeholder={'https://example.com'}
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 pt-3 bg-gray-50 dark:bg-gray-900/50">
|
||||||
|
<button
|
||||||
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-800 text-white dark:bg-white dark:text-black dark:hover:bg-gray-200 transition rounded-full"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{$i18n.t('Add')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||||
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
|
import { removeLastWordFromString, isValidHttpUrl, isYoutubeUrl } from '$lib/utils';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
|
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
|
||||||
import Database from '$lib/components/icons/Database.svelte';
|
import Database from '$lib/components/icons/Database.svelte';
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
: items),
|
: items),
|
||||||
|
|
||||||
...(query.startsWith('http')
|
...(query.startsWith('http')
|
||||||
? query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')
|
? isYoutubeUrl(query)
|
||||||
? [{ type: 'youtube', name: query, description: query }]
|
? [{ type: 'youtube', name: query, description: query }]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
|
@ -228,7 +228,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')}
|
{#if isYoutubeUrl(query)}
|
||||||
<button
|
<button
|
||||||
class="px-2 py-1 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button"
|
class="px-2 py-1 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@
|
||||||
import Chats from './InputMenu/Chats.svelte';
|
import Chats from './InputMenu/Chats.svelte';
|
||||||
import Notes from './InputMenu/Notes.svelte';
|
import Notes from './InputMenu/Notes.svelte';
|
||||||
import Knowledge from './InputMenu/Knowledge.svelte';
|
import Knowledge from './InputMenu/Knowledge.svelte';
|
||||||
|
import AttachWebpageModal from './AttachWebpageModal.svelte';
|
||||||
|
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -39,11 +41,14 @@
|
||||||
export let uploadGoogleDriveHandler: Function;
|
export let uploadGoogleDriveHandler: Function;
|
||||||
export let uploadOneDriveHandler: Function;
|
export let uploadOneDriveHandler: Function;
|
||||||
|
|
||||||
|
export let onUpload: Function;
|
||||||
export let onClose: Function;
|
export let onClose: Function;
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
let tab = '';
|
let tab = '';
|
||||||
|
|
||||||
|
let showAttachWebpageModal = false;
|
||||||
|
|
||||||
let fileUploadEnabled = true;
|
let fileUploadEnabled = true;
|
||||||
$: fileUploadEnabled =
|
$: fileUploadEnabled =
|
||||||
fileUploadCapableModels.length === selectedModels.length &&
|
fileUploadCapableModels.length === selectedModels.length &&
|
||||||
|
|
@ -78,6 +83,13 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<AttachWebpageModal
|
||||||
|
bind:show={showAttachWebpageModal}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
onUpload(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Hidden file input used to open the camera on mobile -->
|
<!-- Hidden file input used to open the camera on mobile -->
|
||||||
<input
|
<input
|
||||||
id="camera-input"
|
id="camera-input"
|
||||||
|
|
@ -166,6 +178,16 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
|
on:click={() => {
|
||||||
|
showAttachWebpageModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GlobeAlt />
|
||||||
|
<div class="line-clamp-1">{$i18n.t('Attach Webpage')}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
{#if $config?.features?.enable_notes ?? false}
|
{#if $config?.features?.enable_notes ?? false}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={fileUploadCapableModels.length !== selectedModels.length
|
content={fileUploadCapableModels.length !== selectedModels.length
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
const getItemsPage = async () => {
|
const getItemsPage = async () => {
|
||||||
itemsLoading = true;
|
itemsLoading = true;
|
||||||
let res = await getChatList(localStorage.token, page).catch(() => {
|
let res = await getChatList(localStorage.token, page, true).catch(() => {
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@
|
||||||
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');
|
||||||
|
|
||||||
|
|
@ -43,6 +46,11 @@
|
||||||
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) {
|
||||||
|
|
@ -64,7 +72,8 @@
|
||||||
a[tool.id] = {
|
a[tool.id] = {
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
description: tool.meta.description,
|
description: tool.meta.description,
|
||||||
enabled: selectedToolIds.includes(tool.id)
|
enabled: selectedToolIds.includes(tool.id),
|
||||||
|
...tool
|
||||||
};
|
};
|
||||||
return a;
|
return a;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
@ -87,6 +96,16 @@
|
||||||
};
|
};
|
||||||
</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) => {
|
||||||
|
|
@ -304,11 +323,25 @@
|
||||||
|
|
||||||
{#each Object.keys(tools) as toolId}
|
{#each Object.keys(tools) as toolId}
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
class="relative flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={(e) => {
|
||||||
tools[toolId].enabled = !tools[toolId].enabled;
|
if (!(tools[toolId]?.authenticated ?? true)) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
let parts = toolId.split(':');
|
||||||
|
let serverId = parts?.at(-1) ?? toolId;
|
||||||
|
|
||||||
|
const authUrl = getOAuthClientAuthorizationUrl(serverId, 'mcp');
|
||||||
|
window.open(authUrl, '_blank', 'noopener');
|
||||||
|
} else {
|
||||||
|
tools[toolId].enabled = !tools[toolId].enabled;
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{#if !(tools[toolId]?.authenticated ?? true)}
|
||||||
|
<!-- make it slighly darker and not clickable -->
|
||||||
|
<div class="absolute inset-0 opacity-50 rounded-xl cursor-not-allowed z-10" />
|
||||||
|
{/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">
|
||||||
<Tooltip content={tools[toolId]?.name ?? ''} placement="top">
|
<Tooltip content={tools[toolId]?.name ?? ''} placement="top">
|
||||||
|
|
@ -322,6 +355,44 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if tools[toolId]?.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();
|
||||||
|
selectedValvesType = 'tool';
|
||||||
|
selectedValvesItemId = toolId;
|
||||||
|
showValvesModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class=" shrink-0">
|
<div class=" shrink-0">
|
||||||
<Switch
|
<Switch
|
||||||
state={tools[toolId].enabled}
|
state={tools[toolId].enabled}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
import { getContext, onMount, tick, onDestroy } from 'svelte';
|
import { getContext, onMount, tick, onDestroy } from 'svelte';
|
||||||
|
import { config } from '$lib/stores';
|
||||||
|
|
||||||
|
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
||||||
|
import { executeCode } from '$lib/apis/utils';
|
||||||
import { copyToClipboard, renderMermaidDiagram } from '$lib/utils';
|
import { copyToClipboard, renderMermaidDiagram } from '$lib/utils';
|
||||||
|
|
||||||
import 'highlight.js/styles/github-dark.min.css';
|
import 'highlight.js/styles/github-dark.min.css';
|
||||||
|
|
||||||
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||||
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
|
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
|
||||||
import { config } from '$lib/stores';
|
|
||||||
import { executeCode } from '$lib/apis/utils';
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte';
|
import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte';
|
||||||
import CommandLine from '$lib/components/icons/CommandLine.svelte';
|
import CommandLine from '$lib/components/icons/CommandLine.svelte';
|
||||||
|
|
@ -480,19 +482,17 @@
|
||||||
|
|
||||||
{#if !collapsed}
|
{#if !collapsed}
|
||||||
{#if edit}
|
{#if edit}
|
||||||
{#await import('$lib/components/common/CodeEditor.svelte') then { default: CodeEditor }}
|
<CodeEditor
|
||||||
<CodeEditor
|
value={code}
|
||||||
value={code}
|
{id}
|
||||||
{id}
|
{lang}
|
||||||
{lang}
|
onSave={() => {
|
||||||
onSave={() => {
|
saveCode();
|
||||||
saveCode();
|
}}
|
||||||
}}
|
onChange={(value) => {
|
||||||
onChange={(value) => {
|
_code = value;
|
||||||
_code = value;
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
{/await}
|
|
||||||
{:else}
|
{:else}
|
||||||
<pre
|
<pre
|
||||||
class=" hljs p-4 px-5 overflow-x-auto"
|
class=" hljs p-4 px-5 overflow-x-auto"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount, getContext } from 'svelte';
|
||||||
import panzoom, { type PanZoom } from 'panzoom';
|
import panzoom, { type PanZoom } from 'panzoom';
|
||||||
|
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
export let src = '';
|
export let src = '';
|
||||||
export let alt = '';
|
export let alt = '';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let mounted = false;
|
let mounted = false;
|
||||||
|
|
||||||
let previewElement = null;
|
let previewElement = null;
|
||||||
|
|
@ -100,9 +102,10 @@
|
||||||
|
|
||||||
const mimeType = blob.type || 'image/png';
|
const mimeType = blob.type || 'image/png';
|
||||||
// create file name based on the MIME type, alt should be a valid file name with extension
|
// create file name based on the MIME type, alt should be a valid file name with extension
|
||||||
const fileName = alt
|
const fileName = `${$i18n
|
||||||
? `${alt.replaceAll('.', '')}.${mimeType.split('/')[1]}`
|
.t('Generated Image')
|
||||||
: 'download.png';
|
.toLowerCase()
|
||||||
|
.replace(/ /g, '_')}.${mimeType.split('/')[1]}`;
|
||||||
|
|
||||||
// Use FileSaver to save the blob
|
// Use FileSaver to save the blob
|
||||||
saveAs(blob, fileName);
|
saveAs(blob, fileName);
|
||||||
|
|
@ -119,9 +122,10 @@
|
||||||
const blobWithType = new Blob([blob], { type: mimeType });
|
const blobWithType = new Blob([blob], { type: mimeType });
|
||||||
|
|
||||||
// create file name based on the MIME type, alt should be a valid file name with extension
|
// create file name based on the MIME type, alt should be a valid file name with extension
|
||||||
const fileName = alt
|
const fileName = `${$i18n
|
||||||
? `${alt.replaceAll('.', '')}.${mimeType.split('/')[1]}`
|
.t('Generated Image')
|
||||||
: 'download.png';
|
.toLowerCase()
|
||||||
|
.replace(/ /g, '_')}.${mimeType.split('/')[1]}`;
|
||||||
|
|
||||||
// Use FileSaver to save the blob
|
// Use FileSaver to save the blob
|
||||||
saveAs(blobWithType, fileName);
|
saveAs(blobWithType, fileName);
|
||||||
|
|
@ -146,9 +150,10 @@
|
||||||
const blobWithType = new Blob([blob], { type: mimeType });
|
const blobWithType = new Blob([blob], { type: mimeType });
|
||||||
|
|
||||||
// create file name based on the MIME type, alt should be a valid file name with extension
|
// create file name based on the MIME type, alt should be a valid file name with extension
|
||||||
const fileName = alt
|
const fileName = `${$i18n
|
||||||
? `${alt.replaceAll('.', '')}.${mimeType.split('/')[1]}`
|
.t('Generated Image')
|
||||||
: 'download.png';
|
.toLowerCase()
|
||||||
|
.replace(/ /g, '_')}.${mimeType.split('/')[1]}`;
|
||||||
|
|
||||||
// Use FileSaver to save the blob
|
// Use FileSaver to save the blob
|
||||||
saveAs(blobWithType, fileName);
|
saveAs(blobWithType, fileName);
|
||||||
|
|
|
||||||
|
|
@ -149,10 +149,15 @@
|
||||||
export let onChange = (e) => {};
|
export let onChange = (e) => {};
|
||||||
|
|
||||||
// create a lowlight instance with all languages loaded
|
// create a lowlight instance with all languages loaded
|
||||||
const lowlight = createLowlight(hljs.listLanguages().reduce((obj, lang) => {
|
const lowlight = createLowlight(
|
||||||
obj[lang] = () => hljs.getLanguage(lang);
|
hljs.listLanguages().reduce(
|
||||||
return obj;
|
(obj, lang) => {
|
||||||
}, {} as Record<string, any>));
|
obj[lang] = () => hljs.getLanguage(lang);
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{} as Record<string, any>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
export let editor: Editor | null = null;
|
export let editor: Editor | null = null;
|
||||||
|
|
||||||
|
|
@ -163,7 +168,7 @@
|
||||||
export let documentId = '';
|
export let documentId = '';
|
||||||
|
|
||||||
export let className = 'input-prose';
|
export let className = 'input-prose';
|
||||||
export let placeholder = 'Type here...';
|
export let placeholder = $i18n.t('Type here...');
|
||||||
let _placeholder = placeholder;
|
let _placeholder = placeholder;
|
||||||
|
|
||||||
$: if (placeholder !== _placeholder) {
|
$: if (placeholder !== _placeholder) {
|
||||||
|
|
@ -501,9 +506,14 @@
|
||||||
|
|
||||||
export const focus = () => {
|
export const focus = () => {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
editor.view.focus();
|
try {
|
||||||
// Scroll to the current selection
|
editor.view?.focus();
|
||||||
editor.view.dispatch(editor.view.state.tr.scrollIntoView());
|
// Scroll to the current selection
|
||||||
|
editor.view?.dispatch(editor.view.state.tr.scrollIntoView());
|
||||||
|
} catch (e) {
|
||||||
|
// sometimes focusing throws an error, ignore
|
||||||
|
console.warn('Error focusing editor', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -679,7 +689,7 @@
|
||||||
link: link
|
link: link
|
||||||
}),
|
}),
|
||||||
...(dragHandle ? [ListItemDragHandle] : []),
|
...(dragHandle ? [ListItemDragHandle] : []),
|
||||||
Placeholder.configure({ placeholder: () => _placeholder }),
|
Placeholder.configure({ placeholder: () => _placeholder, showOnlyWhenEditable: false }),
|
||||||
SelectionDecoration,
|
SelectionDecoration,
|
||||||
|
|
||||||
...(richText
|
...(richText
|
||||||
|
|
@ -1113,4 +1123,9 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div bind:this={element} class="relative w-full min-w-full h-full min-h-fit {className}" />
|
<div
|
||||||
|
bind:this={element}
|
||||||
|
class="relative w-full min-w-full h-full min-h-fit {className} {!editable
|
||||||
|
? 'cursor-not-allowed'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
import { getAdminDetails } from '$lib/apis/auths';
|
import { getAdminDetails } from '$lib/apis/auths';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
import { config } from '$lib/stores';
|
import { config } from '$lib/stores';
|
||||||
|
|
@ -38,7 +41,11 @@
|
||||||
style="white-space: pre-wrap;"
|
style="white-space: pre-wrap;"
|
||||||
>
|
>
|
||||||
{#if ($config?.ui?.pending_user_overlay_content ?? '').trim() !== ''}
|
{#if ($config?.ui?.pending_user_overlay_content ?? '').trim() !== ''}
|
||||||
{$config.ui.pending_user_overlay_content}
|
{@html marked.parse(
|
||||||
|
DOMPurify.sanitize(
|
||||||
|
($config?.ui?.pending_user_overlay_content ?? '').replace(/\n/g, '<br>')
|
||||||
|
)
|
||||||
|
)}
|
||||||
{:else}
|
{:else}
|
||||||
{$i18n.t('Your account status is currently pending activation.')}{'\n'}{$i18n.t(
|
{$i18n.t('Your account status is currently pending activation.')}{'\n'}{$i18n.t(
|
||||||
'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.'
|
'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.'
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,28 @@
|
||||||
import calendar from 'dayjs/plugin/calendar';
|
import calendar from 'dayjs/plugin/calendar';
|
||||||
import Loader from '../common/Loader.svelte';
|
import Loader from '../common/Loader.svelte';
|
||||||
import { createMessagesList } from '$lib/utils';
|
import { createMessagesList } from '$lib/utils';
|
||||||
import { user } from '$lib/stores';
|
import { config, user } from '$lib/stores';
|
||||||
import Messages from '../chat/Messages.svelte';
|
import Messages from '../chat/Messages.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import PencilSquare from '../icons/PencilSquare.svelte';
|
||||||
|
import Note from '../icons/Note.svelte';
|
||||||
dayjs.extend(calendar);
|
dayjs.extend(calendar);
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let onClose = () => {};
|
export let onClose = () => {};
|
||||||
|
|
||||||
|
let actions = [
|
||||||
|
{
|
||||||
|
label: 'Start a new conversation',
|
||||||
|
onClick: async () => {
|
||||||
|
await goto(`/${query ? `?q=${query}` : ''}`);
|
||||||
|
show = false;
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
icon: PencilSquare
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
let query = '';
|
let query = '';
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
|
||||||
|
|
@ -55,7 +69,13 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatId = chatList[selectedIdx].id;
|
const selectedChatIdx = selectedIdx - actions.length;
|
||||||
|
if (selectedChatIdx < 0) {
|
||||||
|
selectedChat = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatId = chatList[selectedChatIdx].id;
|
||||||
|
|
||||||
const chat = await getChatById(localStorage.token, chatId).catch(async (error) => {
|
const chat = await getChatById(localStorage.token, chatId).catch(async (error) => {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -162,13 +182,13 @@
|
||||||
if (e.code === 'Escape') {
|
if (e.code === 'Escape') {
|
||||||
show = false;
|
show = false;
|
||||||
onClose();
|
onClose();
|
||||||
} else if (e.code === 'Enter' && (chatList ?? []).length > 0) {
|
} else if (e.code === 'Enter') {
|
||||||
const item = document.querySelector(`[data-arrow-selected="true"]`);
|
const item = document.querySelector(`[data-arrow-selected="true"]`);
|
||||||
if (item) {
|
if (item) {
|
||||||
item?.click();
|
item?.click();
|
||||||
|
show = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
show = false;
|
|
||||||
return;
|
return;
|
||||||
} else if (e.code === 'ArrowDown') {
|
} else if (e.code === 'ArrowDown') {
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
|
|
@ -182,7 +202,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1);
|
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1 + actions.length);
|
||||||
} else if (e.code === 'ArrowUp') {
|
} else if (e.code === 'ArrowUp') {
|
||||||
if (selectedIdx === 0) {
|
if (selectedIdx === 0) {
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
|
|
@ -205,6 +225,24 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
actions = [
|
||||||
|
...actions,
|
||||||
|
...(($config?.features?.enable_notes ?? false) &&
|
||||||
|
($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Create a new note',
|
||||||
|
onClick: async () => {
|
||||||
|
await goto(`/notes${query ? `?content=${query}` : ''}`);
|
||||||
|
show = false;
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
icon: Note
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
];
|
||||||
|
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -240,7 +278,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
return;
|
return;
|
||||||
} else if (e.code === 'ArrowDown') {
|
} else if (e.code === 'ArrowDown') {
|
||||||
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1);
|
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1 + actions.length);
|
||||||
} else if (e.code === 'ArrowUp') {
|
} else if (e.code === 'ArrowUp') {
|
||||||
selectedIdx = Math.max(selectedIdx - 1, 0);
|
selectedIdx = Math.max(selectedIdx - 1, 0);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -257,11 +295,43 @@
|
||||||
|
|
||||||
<div class="flex px-4 pb-1">
|
<div class="flex px-4 pb-1">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col overflow-y-auto h-96 md:h-[40rem] max-h-full scrollbar-hidden w-full flex-1"
|
class="flex flex-col overflow-y-auto h-96 md:h-[40rem] max-h-full scrollbar-hidden w-full flex-1 pr-2"
|
||||||
>
|
>
|
||||||
|
<div class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium pb-2 px-2">
|
||||||
|
{$i18n.t('Actions')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each actions as action, idx (action.label)}
|
||||||
|
<button
|
||||||
|
class=" w-full flex items-center rounded-xl text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
|
||||||
|
idx
|
||||||
|
? 'bg-gray-50 dark:bg-gray-850'
|
||||||
|
: ''}"
|
||||||
|
data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
|
||||||
|
dragabble="false"
|
||||||
|
on:mouseenter={() => {
|
||||||
|
selectedIdx = idx;
|
||||||
|
}}
|
||||||
|
on:click={async () => {
|
||||||
|
await action.onClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="pr-2">
|
||||||
|
<svelte:component this={action.icon} />
|
||||||
|
</div>
|
||||||
|
<div class=" flex-1 text-left">
|
||||||
|
<div class="text-ellipsis line-clamp-1 w-full">
|
||||||
|
{$i18n.t(action.label)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
|
||||||
{#if chatList}
|
{#if chatList}
|
||||||
|
<hr class="border-gray-50 dark:border-gray-850 my-3" />
|
||||||
|
|
||||||
{#if chatList.length === 0}
|
{#if chatList.length === 0}
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5">
|
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 py-4">
|
||||||
{$i18n.t('No results found')}
|
{$i18n.t('No results found')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -296,15 +366,15 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
|
class=" w-full flex justify-between items-center rounded-xl text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
|
||||||
idx
|
idx + actions.length
|
||||||
? 'bg-gray-50 dark:bg-gray-850'
|
? 'bg-gray-50 dark:bg-gray-850'
|
||||||
: ''}"
|
: ''}"
|
||||||
href="/c/{chat.id}"
|
href="/c/{chat.id}"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
|
data-arrow-selected={selectedIdx === idx + actions.length ? 'true' : undefined}
|
||||||
on:mouseenter={() => {
|
on:mouseenter={() => {
|
||||||
selectedIdx = idx;
|
selectedIdx = idx + actions.length;
|
||||||
}}
|
}}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await goto(`/c/${chat.id}`);
|
await goto(`/c/${chat.id}`);
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@
|
||||||
|
|
||||||
<div class="my-2 -mx-2">
|
<div class="my-2 -mx-2">
|
||||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
|
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
|
||||||
<AccessControl bind:accessControl />
|
<AccessControl bind:accessControl accessRoles={['read', 'write']} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -339,10 +339,6 @@
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (open) {
|
|
||||||
isExpandedUpdateDebounceHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
const renameHandler = async () => {
|
const renameHandler = async () => {
|
||||||
console.log('Edit');
|
console.log('Edit');
|
||||||
await tick();
|
await tick();
|
||||||
|
|
@ -471,6 +467,7 @@
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
open = !open;
|
open = !open;
|
||||||
|
isExpandedUpdateDebounceHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if folders[folderId]?.meta?.icon}
|
{#if folders[folderId]?.meta?.icon}
|
||||||
|
|
|
||||||
|
|
@ -872,7 +872,8 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedModelId) {
|
if (!selectedModelId) {
|
||||||
selectedModelId = $models.at(0)?.id || '';
|
selectedModelId =
|
||||||
|
$models.filter((model) => !(model?.info?.meta?.hidden ?? false)).at(0)?.id || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropzoneElement = document.getElementById('note-editor');
|
const dropzoneElement = document.getElementById('note-editor');
|
||||||
|
|
|
||||||
|
|
@ -423,7 +423,7 @@ Based on the user's instruction, update and enhance the existing notes or select
|
||||||
class=" bg-transparent rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-full text-right pr-5"
|
class=" bg-transparent rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-full text-right pr-5"
|
||||||
bind:value={selectedModelId}
|
bind:value={selectedModelId}
|
||||||
>
|
>
|
||||||
{#each $models as model}
|
{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
|
||||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
|
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
|
||||||
>{model.name}</option
|
>{model.name}</option
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
|
@ -26,6 +28,7 @@
|
||||||
// Assuming $i18n.languages is an array of language codes
|
// Assuming $i18n.languages is an array of language codes
|
||||||
$: loadLocale($i18n.languages);
|
$: loadLocale($i18n.languages);
|
||||||
|
|
||||||
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount, getContext, onDestroy } from 'svelte';
|
import { onMount, getContext, onDestroy } from 'svelte';
|
||||||
import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
|
import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
|
||||||
|
|
@ -42,7 +45,6 @@
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import NoteMenu from './Notes/NoteMenu.svelte';
|
import NoteMenu from './Notes/NoteMenu.svelte';
|
||||||
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
||||||
import { marked } from 'marked';
|
|
||||||
import XMark from '../icons/XMark.svelte';
|
import XMark from '../icons/XMark.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
@ -97,7 +99,7 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createNoteHandler = async () => {
|
const createNoteHandler = async (content?: string) => {
|
||||||
// $i18n.t('New Note'),
|
// $i18n.t('New Note'),
|
||||||
const res = await createNewNote(localStorage.token, {
|
const res = await createNewNote(localStorage.token, {
|
||||||
// YYYY-MM-DD
|
// YYYY-MM-DD
|
||||||
|
|
@ -105,8 +107,8 @@
|
||||||
data: {
|
data: {
|
||||||
content: {
|
content: {
|
||||||
json: null,
|
json: null,
|
||||||
html: '',
|
html: content ?? '',
|
||||||
md: ''
|
md: content ?? ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
meta: null,
|
meta: null,
|
||||||
|
|
@ -301,6 +303,14 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
if ($page.url.searchParams.get('content')) {
|
||||||
|
const content = $page.url.searchParams.get('content') ?? '';
|
||||||
|
if (content) {
|
||||||
|
createNoteHandler(content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await init();
|
await init();
|
||||||
loaded = true;
|
loaded = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { user } from '$lib/stores';
|
||||||
|
|
||||||
|
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
|
||||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||||
import { user } from '$lib/stores';
|
|
||||||
|
|
||||||
let formElement = null;
|
let formElement = null;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
@ -285,22 +286,20 @@ class Tools:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
||||||
{#await import('$lib/components/common/CodeEditor.svelte') then { default: CodeEditor }}
|
<CodeEditor
|
||||||
<CodeEditor
|
bind:this={codeEditor}
|
||||||
bind:this={codeEditor}
|
value={content}
|
||||||
value={content}
|
lang="python"
|
||||||
lang="python"
|
{boilerplate}
|
||||||
{boilerplate}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
_content = e;
|
||||||
_content = e;
|
}}
|
||||||
}}
|
onSave={async () => {
|
||||||
onSave={async () => {
|
if (formElement) {
|
||||||
if (formElement) {
|
formElement.requestSubmit();
|
||||||
formElement.requestSubmit();
|
}
|
||||||
}
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
{/await}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pb-3 flex justify-between">
|
<div class="pb-3 flex justify-between">
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,20 @@
|
||||||
updateFunctionValvesById
|
updateFunctionValvesById
|
||||||
} from '$lib/apis/functions';
|
} from '$lib/apis/functions';
|
||||||
import { getToolValvesById, getToolValvesSpecById, updateToolValvesById } from '$lib/apis/tools';
|
import { getToolValvesById, getToolValvesSpecById, updateToolValvesById } from '$lib/apis/tools';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getUserValvesSpecById as getToolUserValvesSpecById,
|
||||||
|
getUserValvesById as getToolUserValvesById,
|
||||||
|
updateUserValvesById as updateToolUserValvesById,
|
||||||
|
getTools
|
||||||
|
} from '$lib/apis/tools';
|
||||||
|
import {
|
||||||
|
getUserValvesSpecById as getFunctionUserValvesSpecById,
|
||||||
|
getUserValvesById as getFunctionUserValvesById,
|
||||||
|
updateUserValvesById as updateFunctionUserValvesById,
|
||||||
|
getFunctions
|
||||||
|
} from '$lib/apis/functions';
|
||||||
|
|
||||||
import Spinner from '../../common/Spinner.svelte';
|
import Spinner from '../../common/Spinner.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import Valves from '$lib/components/common/Valves.svelte';
|
import Valves from '$lib/components/common/Valves.svelte';
|
||||||
|
|
@ -23,6 +37,7 @@
|
||||||
|
|
||||||
export let type = 'tool';
|
export let type = 'tool';
|
||||||
export let id = null;
|
export let id = null;
|
||||||
|
export let userValves = false;
|
||||||
|
|
||||||
let saving = false;
|
let saving = false;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
@ -43,14 +58,28 @@
|
||||||
|
|
||||||
let res = null;
|
let res = null;
|
||||||
|
|
||||||
if (type === 'tool') {
|
if (userValves) {
|
||||||
res = await updateToolValvesById(localStorage.token, id, valves).catch((error) => {
|
if (type === 'tool') {
|
||||||
toast.error(`${error}`);
|
res = await updateToolUserValvesById(localStorage.token, id, valves).catch((error) => {
|
||||||
});
|
toast.error(`${error}`);
|
||||||
} else if (type === 'function') {
|
});
|
||||||
res = await updateFunctionValvesById(localStorage.token, id, valves).catch((error) => {
|
} else if (type === 'function') {
|
||||||
toast.error(`${error}`);
|
res = await updateFunctionUserValvesById(localStorage.token, id, valves).catch(
|
||||||
});
|
(error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (type === 'tool') {
|
||||||
|
res = await updateToolValvesById(localStorage.token, id, valves).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
});
|
||||||
|
} else if (type === 'function') {
|
||||||
|
res = await updateFunctionValvesById(localStorage.token, id, valves).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
|
|
@ -67,28 +96,43 @@
|
||||||
valves = {};
|
valves = {};
|
||||||
valvesSpec = null;
|
valvesSpec = null;
|
||||||
|
|
||||||
if (type === 'tool') {
|
try {
|
||||||
valves = await getToolValvesById(localStorage.token, id);
|
if (userValves) {
|
||||||
valvesSpec = await getToolValvesSpecById(localStorage.token, id);
|
if (type === 'tool') {
|
||||||
} else if (type === 'function') {
|
valves = await getToolUserValvesById(localStorage.token, id);
|
||||||
valves = await getFunctionValvesById(localStorage.token, id);
|
valvesSpec = await getToolUserValvesSpecById(localStorage.token, id);
|
||||||
valvesSpec = await getFunctionValvesSpecById(localStorage.token, id);
|
} else if (type === 'function') {
|
||||||
}
|
valves = await getFunctionUserValvesById(localStorage.token, id);
|
||||||
|
valvesSpec = await getFunctionUserValvesSpecById(localStorage.token, id);
|
||||||
if (!valves) {
|
}
|
||||||
valves = {};
|
} else {
|
||||||
}
|
if (type === 'tool') {
|
||||||
|
valves = await getToolValvesById(localStorage.token, id);
|
||||||
if (valvesSpec) {
|
valvesSpec = await getToolValvesSpecById(localStorage.token, id);
|
||||||
// Convert array to string
|
} else if (type === 'function') {
|
||||||
for (const property in valvesSpec.properties) {
|
valves = await getFunctionValvesById(localStorage.token, id);
|
||||||
if (valvesSpec.properties[property]?.type === 'array') {
|
valvesSpec = await getFunctionValvesSpecById(localStorage.token, id);
|
||||||
valves[property] = (valves[property] ?? []).join(',');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
loading = false;
|
if (!valves) {
|
||||||
|
valves = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valvesSpec) {
|
||||||
|
// Convert array to string
|
||||||
|
for (const property in valvesSpec.properties) {
|
||||||
|
if (valvesSpec.properties[property]?.type === 'array') {
|
||||||
|
valves[property] = (valves[property] ?? []).join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(`Error fetching valves`);
|
||||||
|
show = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (show) {
|
$: if (show) {
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@
|
||||||
"{{COUNT}} extracted lines": "{{COUNT}} línies extretes",
|
"{{COUNT}} extracted lines": "{{COUNT}} línies extretes",
|
||||||
"{{COUNT}} hidden lines": "{{COUNT}} línies ocultes",
|
"{{COUNT}} hidden lines": "{{COUNT}} línies ocultes",
|
||||||
"{{COUNT}} Replies": "{{COUNT}} respostes",
|
"{{COUNT}} Replies": "{{COUNT}} respostes",
|
||||||
"{{COUNT}} Sources": "",
|
"{{COUNT}} Sources": "{{COUNT}} fonts",
|
||||||
"{{COUNT}} words": "{{COUNT}} paraules",
|
"{{COUNT}} words": "{{COUNT}} paraules",
|
||||||
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "",
|
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "{{LOCALIZED_DATE}} a les {{LOCALIZED_TIME}}",
|
||||||
"{{model}} download has been canceled": "La descàrrega del model {{model}} s'ha cancel·lat",
|
"{{model}} download has been canceled": "La descàrrega del model {{model}} s'ha cancel·lat",
|
||||||
"{{user}}'s Chats": "Els xats de {{user}}",
|
"{{user}}'s Chats": "Els xats de {{user}}",
|
||||||
"{{webUIName}} Backend Required": "El Backend de {{webUIName}} és necessari",
|
"{{webUIName}} Backend Required": "El Backend de {{webUIName}} és necessari",
|
||||||
"*Prompt node ID(s) are required for image generation": "*Els identificadors de nodes d'indicacions són necessaris per a la generació d'imatges",
|
"*Prompt node ID(s) are required for image generation": "*Els identificadors de nodes d'indicacions són necessaris per a la generació d'imatges",
|
||||||
"1 Source": "",
|
"1 Source": "1 font",
|
||||||
"A new version (v{{LATEST_VERSION}}) is now available.": "Hi ha una nova versió disponible (v{{LATEST_VERSION}}).",
|
"A new version (v{{LATEST_VERSION}}) is now available.": "Hi ha una nova versió disponible (v{{LATEST_VERSION}}).",
|
||||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Un model de tasca s'utilitza quan es realitzen tasques com ara generar títols per a xats i consultes de cerca per a la web",
|
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Un model de tasca s'utilitza quan es realitzen tasques com ara generar títols per a xats i consultes de cerca per a la web",
|
||||||
"a user": "un usuari",
|
"a user": "un usuari",
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
"Accessible to all users": "Accessible a tots els usuaris",
|
"Accessible to all users": "Accessible a tots els usuaris",
|
||||||
"Account": "Compte",
|
"Account": "Compte",
|
||||||
"Account Activation Pending": "Activació del compte pendent",
|
"Account Activation Pending": "Activació del compte pendent",
|
||||||
"accurate": "",
|
"accurate": "precís",
|
||||||
"Accurate information": "Informació precisa",
|
"Accurate information": "Informació precisa",
|
||||||
"Action": "Acció",
|
"Action": "Acció",
|
||||||
"Action not found": "Acció no trobada",
|
"Action not found": "Acció no trobada",
|
||||||
|
|
@ -147,8 +147,8 @@
|
||||||
"Ask a question": "Fer una pregunta",
|
"Ask a question": "Fer una pregunta",
|
||||||
"Assistant": "Assistent",
|
"Assistant": "Assistent",
|
||||||
"Attach file from knowledge": "Associar arxiu del coneixement",
|
"Attach file from knowledge": "Associar arxiu del coneixement",
|
||||||
"Attach Knowledge": "",
|
"Attach Knowledge": "Afegir coneixement",
|
||||||
"Attach Notes": "",
|
"Attach Notes": "Afegir notes",
|
||||||
"Attention to detail": "Atenció al detall",
|
"Attention to detail": "Atenció al detall",
|
||||||
"Attribute for Mail": "Atribut per al Correu",
|
"Attribute for Mail": "Atribut per al Correu",
|
||||||
"Attribute for Username": "Atribut per al Nom d'usuari",
|
"Attribute for Username": "Atribut per al Nom d'usuari",
|
||||||
|
|
@ -212,7 +212,7 @@
|
||||||
"Capture Audio": "Capturar àudio",
|
"Capture Audio": "Capturar àudio",
|
||||||
"Certificate Path": "Camí del certificat",
|
"Certificate Path": "Camí del certificat",
|
||||||
"Change Password": "Canviar la contrasenya",
|
"Change Password": "Canviar la contrasenya",
|
||||||
"Channel": "",
|
"Channel": "Canal",
|
||||||
"Channel deleted successfully": "Canal suprimit correctament",
|
"Channel deleted successfully": "Canal suprimit correctament",
|
||||||
"Channel Name": "Nom del canal",
|
"Channel Name": "Nom del canal",
|
||||||
"Channel updated successfully": "Canal actualitzat correctament",
|
"Channel updated successfully": "Canal actualitzat correctament",
|
||||||
|
|
@ -244,7 +244,7 @@
|
||||||
"Clear memory": "Esborrar la memòria",
|
"Clear memory": "Esborrar la memòria",
|
||||||
"Clear Memory": "Esborrar la memòria",
|
"Clear Memory": "Esborrar la memòria",
|
||||||
"click here": "prem aquí",
|
"click here": "prem aquí",
|
||||||
"Click here for filter guides.": "Clica aquí per filtrar les guies.",
|
"Click here for filter guides.": "Clica aquí per l'ajuda dels filtres.",
|
||||||
"Click here for help.": "Clica aquí per obtenir ajuda.",
|
"Click here for help.": "Clica aquí per obtenir ajuda.",
|
||||||
"Click here to": "Clic aquí per",
|
"Click here to": "Clic aquí per",
|
||||||
"Click here to download user import template file.": "Fes clic aquí per descarregar l'arxiu de plantilla d'importació d'usuaris",
|
"Click here to download user import template file.": "Fes clic aquí per descarregar l'arxiu de plantilla d'importació d'usuaris",
|
||||||
|
|
@ -358,7 +358,7 @@
|
||||||
"Custom Parameter Value": "Valor del paràmetre personalitzat",
|
"Custom Parameter Value": "Valor del paràmetre personalitzat",
|
||||||
"Danger Zone": "Zona de perill",
|
"Danger Zone": "Zona de perill",
|
||||||
"Dark": "Fosc",
|
"Dark": "Fosc",
|
||||||
"Data Controls": "",
|
"Data Controls": "Controls de dades",
|
||||||
"Database": "Base de dades",
|
"Database": "Base de dades",
|
||||||
"Datalab Marker API": "API de Datalab Marker",
|
"Datalab Marker API": "API de Datalab Marker",
|
||||||
"Datalab Marker API Key required.": "API de Datalab Marker requereix clau.",
|
"Datalab Marker API Key required.": "API de Datalab Marker requereix clau.",
|
||||||
|
|
@ -370,8 +370,8 @@
|
||||||
"Default (SentenceTransformers)": "Per defecte (SentenceTransformers)",
|
"Default (SentenceTransformers)": "Per defecte (SentenceTransformers)",
|
||||||
"Default action buttons will be used.": "S'utilitzaran els botons d'acció per defecte",
|
"Default action buttons will be used.": "S'utilitzaran els botons d'acció per defecte",
|
||||||
"Default description enabled": "Descripcions per defecte habilitades",
|
"Default description enabled": "Descripcions per defecte habilitades",
|
||||||
"Default Features": "",
|
"Default Features": "Característiques per defecte",
|
||||||
"Default Filters": "",
|
"Default Filters": "Filres per defecte",
|
||||||
"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model's built-in tool-calling capabilities, but requires the model to inherently support this feature.": "El mode predeterminat funciona amb una gamma més àmplia de models cridant a les eines una vegada abans de l'execució. El mode natiu aprofita les capacitats de crida d'eines integrades del model, però requereix que el model admeti aquesta funció de manera inherent.",
|
"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model's built-in tool-calling capabilities, but requires the model to inherently support this feature.": "El mode predeterminat funciona amb una gamma més àmplia de models cridant a les eines una vegada abans de l'execució. El mode natiu aprofita les capacitats de crida d'eines integrades del model, però requereix que el model admeti aquesta funció de manera inherent.",
|
||||||
"Default Model": "Model per defecte",
|
"Default Model": "Model per defecte",
|
||||||
"Default model updated": "Model per defecte actualitzat",
|
"Default model updated": "Model per defecte actualitzat",
|
||||||
|
|
@ -433,11 +433,11 @@
|
||||||
"Display Multi-model Responses in Tabs": "Mostrar respostes multi-model a les pestanyes",
|
"Display Multi-model Responses in Tabs": "Mostrar respostes multi-model a les pestanyes",
|
||||||
"Display the username instead of You in the Chat": "Mostrar el nom d'usuari en lloc de 'Tu' al xat",
|
"Display the username instead of You in the Chat": "Mostrar el nom d'usuari en lloc de 'Tu' al xat",
|
||||||
"Displays citations in the response": "Mostra les referències a la resposta",
|
"Displays citations in the response": "Mostra les referències a la resposta",
|
||||||
"Displays status updates (e.g., web search progress) in the response": "",
|
"Displays status updates (e.g., web search progress) in the response": "Mostra actualitzacions d'estat (per exemple, progrés de la cerca web) a la resposta",
|
||||||
"Dive into knowledge": "Aprofundir en el coneixement",
|
"Dive into knowledge": "Aprofundir en el coneixement",
|
||||||
"dlparse_v1": "",
|
"dlparse_v1": "dlparse_v1",
|
||||||
"dlparse_v2": "",
|
"dlparse_v2": "dlparse_v2",
|
||||||
"dlparse_v4": "",
|
"dlparse_v4": "dlparse_v4",
|
||||||
"Do not install functions from sources you do not fully trust.": "No instal·lis funcions de fonts en què no confiïs plenament.",
|
"Do not install functions from sources you do not fully trust.": "No instal·lis funcions de fonts en què no confiïs plenament.",
|
||||||
"Do not install tools from sources you do not fully trust.": "No instal·lis eines de fonts en què no confiïs plenament.",
|
"Do not install tools from sources you do not fully trust.": "No instal·lis eines de fonts en què no confiïs plenament.",
|
||||||
"Docling": "Docling",
|
"Docling": "Docling",
|
||||||
|
|
@ -487,7 +487,7 @@
|
||||||
"Edit Memory": "Editar la memòria",
|
"Edit Memory": "Editar la memòria",
|
||||||
"Edit User": "Editar l'usuari",
|
"Edit User": "Editar l'usuari",
|
||||||
"Edit User Group": "Editar el grup d'usuaris",
|
"Edit User Group": "Editar el grup d'usuaris",
|
||||||
"edited": "",
|
"edited": "editat",
|
||||||
"Edited": "Editat",
|
"Edited": "Editat",
|
||||||
"Editing": "Editant",
|
"Editing": "Editant",
|
||||||
"Eject": "Expulsar",
|
"Eject": "Expulsar",
|
||||||
|
|
@ -629,8 +629,8 @@
|
||||||
"Enter Your Password": "Introdueix la teva contrasenya",
|
"Enter Your Password": "Introdueix la teva contrasenya",
|
||||||
"Enter Your Role": "Introdueix el teu rol",
|
"Enter Your Role": "Introdueix el teu rol",
|
||||||
"Enter Your Username": "Introdueix el teu nom d'usuari",
|
"Enter Your Username": "Introdueix el teu nom d'usuari",
|
||||||
"Enter your webhook URL": "Entra la URL del webhook",
|
"Enter your webhook URL": "Introdueix la URL del webhook",
|
||||||
"Entra ID": "",
|
"Entra ID": "Introdueix l'ID",
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"ERROR": "ERROR",
|
"ERROR": "ERROR",
|
||||||
"Error accessing directory": "Error en accedir al directori",
|
"Error accessing directory": "Error en accedir al directori",
|
||||||
|
|
@ -674,7 +674,7 @@
|
||||||
"External": "Extern",
|
"External": "Extern",
|
||||||
"External Document Loader URL required.": "Fa falta la URL per a Document Loader",
|
"External Document Loader URL required.": "Fa falta la URL per a Document Loader",
|
||||||
"External Task Model": "Model de tasques extern",
|
"External Task Model": "Model de tasques extern",
|
||||||
"External Tools": "",
|
"External Tools": "Eines externes",
|
||||||
"External Web Loader API Key": "Clau API d'External Web Loader",
|
"External Web Loader API Key": "Clau API d'External Web Loader",
|
||||||
"External Web Loader URL": "URL d'External Web Loader",
|
"External Web Loader URL": "URL d'External Web Loader",
|
||||||
"External Web Search API Key": "Clau API d'External Web Search",
|
"External Web Search API Key": "Clau API d'External Web Search",
|
||||||
|
|
@ -698,7 +698,7 @@
|
||||||
"Failed to save models configuration": "No s'ha pogut desar la configuració dels models",
|
"Failed to save models configuration": "No s'ha pogut desar la configuració dels models",
|
||||||
"Failed to update settings": "No s'han pogut actualitzar les preferències",
|
"Failed to update settings": "No s'han pogut actualitzar les preferències",
|
||||||
"Failed to upload file.": "No s'ha pogut pujar l'arxiu.",
|
"Failed to upload file.": "No s'ha pogut pujar l'arxiu.",
|
||||||
"fast": "",
|
"fast": "ràpid",
|
||||||
"Features": "Característiques",
|
"Features": "Característiques",
|
||||||
"Features Permissions": "Permisos de les característiques",
|
"Features Permissions": "Permisos de les característiques",
|
||||||
"February": "Febrer",
|
"February": "Febrer",
|
||||||
|
|
@ -726,13 +726,13 @@
|
||||||
"Firecrawl API Key": "Clau API de Firecrawl",
|
"Firecrawl API Key": "Clau API de Firecrawl",
|
||||||
"Floating Quick Actions": "Accions ràpides flotants",
|
"Floating Quick Actions": "Accions ràpides flotants",
|
||||||
"Focus chat input": "Estableix el focus a l'entrada del xat",
|
"Focus chat input": "Estableix el focus a l'entrada del xat",
|
||||||
"Folder Background Image": "",
|
"Folder Background Image": "Imatge del fons de la carpeta",
|
||||||
"Folder deleted successfully": "Carpeta eliminada correctament",
|
"Folder deleted successfully": "Carpeta eliminada correctament",
|
||||||
"Folder Name": "Nom de la carpeta",
|
"Folder Name": "Nom de la carpeta",
|
||||||
"Folder name cannot be empty.": "El nom de la carpeta no pot ser buit.",
|
"Folder name cannot be empty.": "El nom de la carpeta no pot ser buit.",
|
||||||
"Folder name updated successfully": "Nom de la carpeta actualitzat correctament",
|
"Folder name updated successfully": "Nom de la carpeta actualitzat correctament",
|
||||||
"Folder updated successfully": "Carpeta actualitazda correctament",
|
"Folder updated successfully": "Carpeta actualitazda correctament",
|
||||||
"Folders": "",
|
"Folders": "Carpetes",
|
||||||
"Follow up": "Seguir",
|
"Follow up": "Seguir",
|
||||||
"Follow Up Generation": "Generació de seguiment",
|
"Follow Up Generation": "Generació de seguiment",
|
||||||
"Follow Up Generation Prompt": "Indicació per a la generació de seguiment",
|
"Follow Up Generation Prompt": "Indicació per a la generació de seguiment",
|
||||||
|
|
@ -746,7 +746,7 @@
|
||||||
"Format the lines in the output. Defaults to False. If set to True, the lines will be formatted to detect inline math and styles.": "Formata les línies a la sortida. Per defecte, és Fals. Si es defineix com a Cert, les línies es formataran per detectar matemàtiques i estils en línia.",
|
"Format the lines in the output. Defaults to False. If set to True, the lines will be formatted to detect inline math and styles.": "Formata les línies a la sortida. Per defecte, és Fals. Si es defineix com a Cert, les línies es formataran per detectar matemàtiques i estils en línia.",
|
||||||
"Format your variables using brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
|
"Format your variables using brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
|
||||||
"Formatting may be inconsistent from source.": "La formatació pot ser inconsistent amb l'origen",
|
"Formatting may be inconsistent from source.": "La formatació pot ser inconsistent amb l'origen",
|
||||||
"Forwards system user OAuth access token to authenticate": "",
|
"Forwards system user OAuth access token to authenticate": "Reenvia el testimoni d'accés OAuth de l'usuari del sistema per autenticar-se.",
|
||||||
"Forwards system user session credentials to authenticate": "Envia les credencials de l'usuari del sistema per autenticar",
|
"Forwards system user session credentials to authenticate": "Envia les credencials de l'usuari del sistema per autenticar",
|
||||||
"Full Context Mode": "Mode de context complert",
|
"Full Context Mode": "Mode de context complert",
|
||||||
"Function": "Funció",
|
"Function": "Funció",
|
||||||
|
|
@ -842,7 +842,7 @@
|
||||||
"Import Prompts": "Importar indicacions",
|
"Import Prompts": "Importar indicacions",
|
||||||
"Import Tools": "Importar eines",
|
"Import Tools": "Importar eines",
|
||||||
"Important Update": "Actualització important",
|
"Important Update": "Actualització important",
|
||||||
"In order to force OCR, performing OCR must be enabled.": "",
|
"In order to force OCR, performing OCR must be enabled.": "Per forçar l'OCR, cal activar l'OCR.",
|
||||||
"Include": "Incloure",
|
"Include": "Incloure",
|
||||||
"Include `--api-auth` flag when running stable-diffusion-webui": "Inclou `--api-auth` quan executis stable-diffusion-webui",
|
"Include `--api-auth` flag when running stable-diffusion-webui": "Inclou `--api-auth` quan executis stable-diffusion-webui",
|
||||||
"Include `--api` flag when running stable-diffusion-webui": "Inclou `--api` quan executis stable-diffusion-webui",
|
"Include `--api` flag when running stable-diffusion-webui": "Inclou `--api` quan executis stable-diffusion-webui",
|
||||||
|
|
@ -858,11 +858,11 @@
|
||||||
"Insert": "Inserir",
|
"Insert": "Inserir",
|
||||||
"Insert Follow-Up Prompt to Input": "Inserir un missatge de seguiment per a l'entrada",
|
"Insert Follow-Up Prompt to Input": "Inserir un missatge de seguiment per a l'entrada",
|
||||||
"Insert Prompt as Rich Text": "Inserir la indicació com a Text Ric",
|
"Insert Prompt as Rich Text": "Inserir la indicació com a Text Ric",
|
||||||
"Insert Suggestion Prompt to Input": "",
|
"Insert Suggestion Prompt to Input": "Insereix un suggeriment per introduir",
|
||||||
"Install from Github URL": "Instal·lar des de l'URL de Github",
|
"Install from Github URL": "Instal·lar des de l'URL de Github",
|
||||||
"Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu",
|
"Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu",
|
||||||
"Integration": "Integració",
|
"Integration": "Integració",
|
||||||
"Integrations": "",
|
"Integrations": "Integracions",
|
||||||
"Interface": "Interfície",
|
"Interface": "Interfície",
|
||||||
"Invalid file content": "Continguts del fitxer no vàlids",
|
"Invalid file content": "Continguts del fitxer no vàlids",
|
||||||
"Invalid file format.": "Format d'arxiu no vàlid.",
|
"Invalid file format.": "Format d'arxiu no vàlid.",
|
||||||
|
|
@ -921,7 +921,7 @@
|
||||||
"Leave empty to include all models or select specific models": "Deixa-ho en blanc per incloure tots els models o selecciona models específics",
|
"Leave empty to include all models or select specific models": "Deixa-ho en blanc per incloure tots els models o selecciona models específics",
|
||||||
"Leave empty to use the default prompt, or enter a custom prompt": "Deixa-ho en blanc per utilitzar la indicació predeterminada o introdueix una indicació personalitzada",
|
"Leave empty to use the default prompt, or enter a custom prompt": "Deixa-ho en blanc per utilitzar la indicació predeterminada o introdueix una indicació personalitzada",
|
||||||
"Leave model field empty to use the default model.": "Deixa el camp de model buit per utilitzar el model per defecte.",
|
"Leave model field empty to use the default model.": "Deixa el camp de model buit per utilitzar el model per defecte.",
|
||||||
"Legacy": "",
|
"Legacy": "Llegat",
|
||||||
"lexical": "lèxic",
|
"lexical": "lèxic",
|
||||||
"License": "Llicència",
|
"License": "Llicència",
|
||||||
"Lift List": "Aixecar la llista",
|
"Lift List": "Aixecar la llista",
|
||||||
|
|
@ -1027,7 +1027,7 @@
|
||||||
"New Tool": "Nova eina",
|
"New Tool": "Nova eina",
|
||||||
"new-channel": "nou-canal",
|
"new-channel": "nou-canal",
|
||||||
"Next message": "Missatge següent",
|
"Next message": "Missatge següent",
|
||||||
"No authentication": "",
|
"No authentication": "Sense autenticació",
|
||||||
"No chats found": "No s'han trobat xats",
|
"No chats found": "No s'han trobat xats",
|
||||||
"No chats found for this user.": "No s'han trobat xats per a aquest usuari.",
|
"No chats found for this user.": "No s'han trobat xats per a aquest usuari.",
|
||||||
"No chats found.": "No s'ha trobat xats.",
|
"No chats found.": "No s'ha trobat xats.",
|
||||||
|
|
@ -1048,12 +1048,12 @@
|
||||||
"No models found": "No s'han trobat models",
|
"No models found": "No s'han trobat models",
|
||||||
"No models selected": "No s'ha seleccionat cap model",
|
"No models selected": "No s'ha seleccionat cap model",
|
||||||
"No Notes": "No hi ha notes",
|
"No Notes": "No hi ha notes",
|
||||||
"No notes found": "",
|
"No notes found": "No s'han trobat notes",
|
||||||
"No results": "No s'han trobat resultats",
|
"No results": "No s'han trobat resultats",
|
||||||
"No results found": "No s'han trobat resultats",
|
"No results found": "No s'han trobat resultats",
|
||||||
"No search query generated": "No s'ha generat cap consulta",
|
"No search query generated": "No s'ha generat cap consulta",
|
||||||
"No source available": "Sense font disponible",
|
"No source available": "Sense font disponible",
|
||||||
"No sources found": "",
|
"No sources found": "No s'han trobat fonts",
|
||||||
"No suggestion prompts": "Cap prompt suggerit",
|
"No suggestion prompts": "Cap prompt suggerit",
|
||||||
"No users were found.": "No s'han trobat usuaris",
|
"No users were found.": "No s'han trobat usuaris",
|
||||||
"No valves": "No hi ha valves",
|
"No valves": "No hi ha valves",
|
||||||
|
|
@ -1062,7 +1062,7 @@
|
||||||
"None": "Cap",
|
"None": "Cap",
|
||||||
"Not factually correct": "No és clarament correcte",
|
"Not factually correct": "No és clarament correcte",
|
||||||
"Not helpful": "No ajuda",
|
"Not helpful": "No ajuda",
|
||||||
"Note": "",
|
"Note": "Nota",
|
||||||
"Note deleted successfully": "La nota s'ha eliminat correctament",
|
"Note deleted successfully": "La nota s'ha eliminat correctament",
|
||||||
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si s'estableix una puntuació mínima, la cerca només retornarà documents amb una puntuació major o igual a la puntuació mínima.",
|
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si s'estableix una puntuació mínima, la cerca només retornarà documents amb una puntuació major o igual a la puntuació mínima.",
|
||||||
"Notes": "Notes",
|
"Notes": "Notes",
|
||||||
|
|
@ -1070,7 +1070,7 @@
|
||||||
"Notification Webhook": "Webhook de la notificació",
|
"Notification Webhook": "Webhook de la notificació",
|
||||||
"Notifications": "Notificacions",
|
"Notifications": "Notificacions",
|
||||||
"November": "Novembre",
|
"November": "Novembre",
|
||||||
"OAuth": "",
|
"OAuth": "OAuth",
|
||||||
"OAuth ID": "ID OAuth",
|
"OAuth ID": "ID OAuth",
|
||||||
"October": "Octubre",
|
"October": "Octubre",
|
||||||
"Off": "Desactivat",
|
"Off": "Desactivat",
|
||||||
|
|
@ -1095,10 +1095,10 @@
|
||||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ui! Estàs utilitzant un mètode no suportat (només frontend). Si us plau, serveix la WebUI des del backend.",
|
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ui! Estàs utilitzant un mètode no suportat (només frontend). Si us plau, serveix la WebUI des del backend.",
|
||||||
"Open file": "Obrir arxiu",
|
"Open file": "Obrir arxiu",
|
||||||
"Open in full screen": "Obrir en pantalla complerta",
|
"Open in full screen": "Obrir en pantalla complerta",
|
||||||
"Open link": "",
|
"Open link": "Obrir l'enllaç",
|
||||||
"Open modal to configure connection": "Obre el modal per configurar la connexió",
|
"Open modal to configure connection": "Obre el modal per configurar la connexió",
|
||||||
"Open Modal To Manage Floating Quick Actions": "Obre el model per configurar les Accions ràpides flotants",
|
"Open Modal To Manage Floating Quick Actions": "Obre el model per configurar les Accions ràpides flotants",
|
||||||
"Open Modal To Manage Image Compression": "",
|
"Open Modal To Manage Image Compression": "Obrir un modal per gestionar la compressió d'imatges",
|
||||||
"Open new chat": "Obre un xat nou",
|
"Open new chat": "Obre un xat nou",
|
||||||
"Open Sidebar": "Obre la barra lateral",
|
"Open Sidebar": "Obre la barra lateral",
|
||||||
"Open User Profile Menu": "Obre el menú de perfil d'usuari",
|
"Open User Profile Menu": "Obre el menú de perfil d'usuari",
|
||||||
|
|
@ -1129,14 +1129,14 @@
|
||||||
"Password": "Contrasenya",
|
"Password": "Contrasenya",
|
||||||
"Passwords do not match.": "Les contrasenyes no coincideixen",
|
"Passwords do not match.": "Les contrasenyes no coincideixen",
|
||||||
"Paste Large Text as File": "Enganxa un text llarg com a fitxer",
|
"Paste Large Text as File": "Enganxa un text llarg com a fitxer",
|
||||||
"PDF Backend": "",
|
"PDF Backend": "Backend de PDF",
|
||||||
"PDF document (.pdf)": "Document PDF (.pdf)",
|
"PDF document (.pdf)": "Document PDF (.pdf)",
|
||||||
"PDF Extract Images (OCR)": "Extreu imatges del PDF (OCR)",
|
"PDF Extract Images (OCR)": "Extreu imatges del PDF (OCR)",
|
||||||
"pending": "pendent",
|
"pending": "pendent",
|
||||||
"Pending": "Pendent",
|
"Pending": "Pendent",
|
||||||
"Pending User Overlay Content": "Contingut de la finestra d'usuari pendent",
|
"Pending User Overlay Content": "Contingut de la finestra d'usuari pendent",
|
||||||
"Pending User Overlay Title": "Títol de la finestra d'usuari pendent",
|
"Pending User Overlay Title": "Títol de la finestra d'usuari pendent",
|
||||||
"Perform OCR": "",
|
"Perform OCR": "Fer OCR",
|
||||||
"Permission denied when accessing media devices": "Permís denegat en accedir a dispositius multimèdia",
|
"Permission denied when accessing media devices": "Permís denegat en accedir a dispositius multimèdia",
|
||||||
"Permission denied when accessing microphone": "Permís denegat en accedir al micròfon",
|
"Permission denied when accessing microphone": "Permís denegat en accedir al micròfon",
|
||||||
"Permission denied when accessing microphone: {{error}}": "Permís denegat en accedir al micròfon: {{error}}",
|
"Permission denied when accessing microphone: {{error}}": "Permís denegat en accedir al micròfon: {{error}}",
|
||||||
|
|
@ -1152,7 +1152,7 @@
|
||||||
"Pinned": "Fixat",
|
"Pinned": "Fixat",
|
||||||
"Pioneer insights": "Perspectives pioneres",
|
"Pioneer insights": "Perspectives pioneres",
|
||||||
"Pipe": "Canonada",
|
"Pipe": "Canonada",
|
||||||
"Pipeline": "",
|
"Pipeline": "Canonada",
|
||||||
"Pipeline deleted successfully": "Pipeline eliminada correctament",
|
"Pipeline deleted successfully": "Pipeline eliminada correctament",
|
||||||
"Pipeline downloaded successfully": "Pipeline descarregada correctament",
|
"Pipeline downloaded successfully": "Pipeline descarregada correctament",
|
||||||
"Pipelines": "Pipelines",
|
"Pipelines": "Pipelines",
|
||||||
|
|
@ -1197,13 +1197,13 @@
|
||||||
"Prompts": "Indicacions",
|
"Prompts": "Indicacions",
|
||||||
"Prompts Access": "Accés a les indicacions",
|
"Prompts Access": "Accés a les indicacions",
|
||||||
"Prompts Public Sharing": "Compartició pública de indicacions",
|
"Prompts Public Sharing": "Compartició pública de indicacions",
|
||||||
"Provider Type": "",
|
"Provider Type": "Tipus de proveïdor",
|
||||||
"Public": "Públic",
|
"Public": "Públic",
|
||||||
"Pull \"{{searchValue}}\" from Ollama.com": "Obtenir \"{{searchValue}}\" de Ollama.com",
|
"Pull \"{{searchValue}}\" from Ollama.com": "Obtenir \"{{searchValue}}\" de Ollama.com",
|
||||||
"Pull a model from Ollama.com": "Obtenir un model d'Ollama.com",
|
"Pull a model from Ollama.com": "Obtenir un model d'Ollama.com",
|
||||||
"pypdfium2": "",
|
"pypdfium2": "pypdfium2",
|
||||||
"Query Generation Prompt": "Indicació per a generació de consulta",
|
"Query Generation Prompt": "Indicació per a generació de consulta",
|
||||||
"Querying": "",
|
"Querying": "Consultes",
|
||||||
"Quick Actions": "Accions ràpides",
|
"Quick Actions": "Accions ràpides",
|
||||||
"RAG Template": "Plantilla RAG",
|
"RAG Template": "Plantilla RAG",
|
||||||
"Rating": "Valoració",
|
"Rating": "Valoració",
|
||||||
|
|
@ -1218,7 +1218,7 @@
|
||||||
"Redirecting you to Open WebUI Community": "Redirigint-te a la comunitat OpenWebUI",
|
"Redirecting you to Open WebUI Community": "Redirigint-te a la comunitat OpenWebUI",
|
||||||
"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.": "Redueix la probabilitat de generar ximpleries. Un valor més alt (p. ex. 100) donarà respostes més diverses, mentre que un valor més baix (p. ex. 10) serà més conservador.",
|
"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.": "Redueix la probabilitat de generar ximpleries. Un valor més alt (p. ex. 100) donarà respostes més diverses, mentre que un valor més baix (p. ex. 10) serà més conservador.",
|
||||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Fes referència a tu mateix com a \"Usuari\" (p. ex., \"L'usuari està aprenent espanyol\")",
|
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Fes referència a tu mateix com a \"Usuari\" (p. ex., \"L'usuari està aprenent espanyol\")",
|
||||||
"Reference Chats": "",
|
"Reference Chats": "Xats de referència",
|
||||||
"Refused when it shouldn't have": "Refusat quan no hauria d'haver estat",
|
"Refused when it shouldn't have": "Refusat quan no hauria d'haver estat",
|
||||||
"Regenerate": "Regenerar",
|
"Regenerate": "Regenerar",
|
||||||
"Regenerate Menu": "Regenerar el menú",
|
"Regenerate Menu": "Regenerar el menú",
|
||||||
|
|
@ -1239,7 +1239,7 @@
|
||||||
"Rename": "Canviar el nom",
|
"Rename": "Canviar el nom",
|
||||||
"Reorder Models": "Reordenar els models",
|
"Reorder Models": "Reordenar els models",
|
||||||
"Reply in Thread": "Respondre al fil",
|
"Reply in Thread": "Respondre al fil",
|
||||||
"required": "",
|
"required": "necessari",
|
||||||
"Reranking Engine": "Motor de valoració",
|
"Reranking Engine": "Motor de valoració",
|
||||||
"Reranking Model": "Model de reavaluació",
|
"Reranking Model": "Model de reavaluació",
|
||||||
"Reset": "Restableix",
|
"Reset": "Restableix",
|
||||||
|
|
@ -1256,11 +1256,11 @@
|
||||||
"RESULT": "Resultat",
|
"RESULT": "Resultat",
|
||||||
"Retrieval": "Retrieval",
|
"Retrieval": "Retrieval",
|
||||||
"Retrieval Query Generation": "Generació de consultes Retrieval",
|
"Retrieval Query Generation": "Generació de consultes Retrieval",
|
||||||
"Retrieved {{count}} sources": "",
|
"Retrieved {{count}} sources": "S'han obtingut {{count}} fonts",
|
||||||
"Retrieved {{count}} sources_one": "",
|
"Retrieved {{count}} sources_one": "S'han obtingut {{count}} sources_one",
|
||||||
"Retrieved {{count}} sources_many": "",
|
"Retrieved {{count}} sources_many": "S'han obtingut {{count}} sources_many",
|
||||||
"Retrieved {{count}} sources_other": "",
|
"Retrieved {{count}} sources_other": "S'han obtingut {{count}} sources_other",
|
||||||
"Retrieved 1 source": "",
|
"Retrieved 1 source": "S'ha obtingut una font",
|
||||||
"Rich Text Input for Chat": "Entrada de text ric per al xat",
|
"Rich Text Input for Chat": "Entrada de text ric per al xat",
|
||||||
"RK": "RK",
|
"RK": "RK",
|
||||||
"Role": "Rol",
|
"Role": "Rol",
|
||||||
|
|
@ -1304,7 +1304,7 @@
|
||||||
"SearchApi API Key": "Clau API de SearchApi",
|
"SearchApi API Key": "Clau API de SearchApi",
|
||||||
"SearchApi Engine": "Motor de SearchApi",
|
"SearchApi Engine": "Motor de SearchApi",
|
||||||
"Searched {{count}} sites": "S'han cercat {{count}} pàgines",
|
"Searched {{count}} sites": "S'han cercat {{count}} pàgines",
|
||||||
"Searching": "",
|
"Searching": "Cercant",
|
||||||
"Searching \"{{searchQuery}}\"": "Cercant \"{{searchQuery}}\"",
|
"Searching \"{{searchQuery}}\"": "Cercant \"{{searchQuery}}\"",
|
||||||
"Searching Knowledge for \"{{searchQuery}}\"": "Cercant \"{{searchQuery}}\" al coneixement",
|
"Searching Knowledge for \"{{searchQuery}}\"": "Cercant \"{{searchQuery}}\" al coneixement",
|
||||||
"Searching the web": "Cercant la web...",
|
"Searching the web": "Cercant la web...",
|
||||||
|
|
@ -1416,10 +1416,10 @@
|
||||||
"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
|
"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
|
||||||
"Speech-to-Text": "Àudio-a-Text",
|
"Speech-to-Text": "Àudio-a-Text",
|
||||||
"Speech-to-Text Engine": "Motor de veu a text",
|
"Speech-to-Text Engine": "Motor de veu a text",
|
||||||
"standard": "",
|
"standard": "estàndard",
|
||||||
"Start of the channel": "Inici del canal",
|
"Start of the channel": "Inici del canal",
|
||||||
"Start Tag": "Etiqueta d'inici",
|
"Start Tag": "Etiqueta d'inici",
|
||||||
"Status Updates": "",
|
"Status Updates": "Estat de les actualitzacions",
|
||||||
"STDOUT/STDERR": "STDOUT/STDERR",
|
"STDOUT/STDERR": "STDOUT/STDERR",
|
||||||
"Stop": "Atura",
|
"Stop": "Atura",
|
||||||
"Stop Generating": "Atura la generació",
|
"Stop Generating": "Atura la generació",
|
||||||
|
|
@ -1445,7 +1445,7 @@
|
||||||
"System": "Sistema",
|
"System": "Sistema",
|
||||||
"System Instructions": "Instruccions de sistema",
|
"System Instructions": "Instruccions de sistema",
|
||||||
"System Prompt": "Indicació del Sistema",
|
"System Prompt": "Indicació del Sistema",
|
||||||
"Table Mode": "",
|
"Table Mode": "Mode de taula",
|
||||||
"Tags": "Etiquetes",
|
"Tags": "Etiquetes",
|
||||||
"Tags Generation": "Generació d'etiquetes",
|
"Tags Generation": "Generació d'etiquetes",
|
||||||
"Tags Generation Prompt": "Indicació per a la generació d'etiquetes",
|
"Tags Generation Prompt": "Indicació per a la generació d'etiquetes",
|
||||||
|
|
@ -1530,7 +1530,7 @@
|
||||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "Per seleccionar kits d'eines aquí, afegeix-los primer a l'espai de treball \"Eines\".",
|
"To select toolkits here, add them to the \"Tools\" workspace first.": "Per seleccionar kits d'eines aquí, afegeix-los primer a l'espai de treball \"Eines\".",
|
||||||
"Toast notifications for new updates": "Notificacions Toast de noves actualitzacions",
|
"Toast notifications for new updates": "Notificacions Toast de noves actualitzacions",
|
||||||
"Today": "Avui",
|
"Today": "Avui",
|
||||||
"Today at {{LOCALIZED_TIME}}": "",
|
"Today at {{LOCALIZED_TIME}}": "Avui a les {{LOCALIZED_TIME}}",
|
||||||
"Toggle search": "Alternar cerca",
|
"Toggle search": "Alternar cerca",
|
||||||
"Toggle settings": "Alterna preferències",
|
"Toggle settings": "Alterna preferències",
|
||||||
"Toggle sidebar": "Alterna la barra lateral",
|
"Toggle sidebar": "Alterna la barra lateral",
|
||||||
|
|
@ -1568,7 +1568,7 @@
|
||||||
"Unarchive All Archived Chats": "Desarxivar tots els xats arxivats",
|
"Unarchive All Archived Chats": "Desarxivar tots els xats arxivats",
|
||||||
"Unarchive Chat": "Desarxivar xat",
|
"Unarchive Chat": "Desarxivar xat",
|
||||||
"Underline": "Subratllat",
|
"Underline": "Subratllat",
|
||||||
"Unknown": "",
|
"Unknown": "Desconegut",
|
||||||
"Unloads {{FROM_NOW}}": "Es descarrega {{FROM_NOW}}",
|
"Unloads {{FROM_NOW}}": "Es descarrega {{FROM_NOW}}",
|
||||||
"Unlock mysteries": "Desbloqueja els misteris",
|
"Unlock mysteries": "Desbloqueja els misteris",
|
||||||
"Unpin": "Alliberar",
|
"Unpin": "Alliberar",
|
||||||
|
|
@ -1610,7 +1610,7 @@
|
||||||
"User Webhooks": "Webhooks d'usuari",
|
"User Webhooks": "Webhooks d'usuari",
|
||||||
"Username": "Nom d'usuari",
|
"Username": "Nom d'usuari",
|
||||||
"Users": "Usuaris",
|
"Users": "Usuaris",
|
||||||
"Uses DefaultAzureCredential to authenticate": "",
|
"Uses DefaultAzureCredential to authenticate": "Utilitza DefaultAzureCredential per a l'autenticació",
|
||||||
"Using Entire Document": "Utilitzant tot el document",
|
"Using Entire Document": "Utilitzant tot el document",
|
||||||
"Using Focused Retrieval": "Utilitzant RAG",
|
"Using Focused Retrieval": "Utilitzant RAG",
|
||||||
"Using the default arena model with all models. Click the plus button to add custom models.": "S'utilitza el model d'Arena predeterminat amb tots els models. Clica el botó més per afegir models personalitzats.",
|
"Using the default arena model with all models. Click the plus button to add custom models.": "S'utilitza el model d'Arena predeterminat amb tots els models. Clica el botó més per afegir models personalitzats.",
|
||||||
|
|
@ -1628,7 +1628,7 @@
|
||||||
"View Result from **{{NAME}}**": "Veure el resultat de **{{NAME}}**",
|
"View Result from **{{NAME}}**": "Veure el resultat de **{{NAME}}**",
|
||||||
"Visibility": "Visibilitat",
|
"Visibility": "Visibilitat",
|
||||||
"Vision": "Visió",
|
"Vision": "Visió",
|
||||||
"vlm": "",
|
"vlm": "vlm",
|
||||||
"Voice": "Veu",
|
"Voice": "Veu",
|
||||||
"Voice Input": "Entrada de veu",
|
"Voice Input": "Entrada de veu",
|
||||||
"Voice mode": "Mode de veu",
|
"Voice mode": "Mode de veu",
|
||||||
|
|
@ -1673,7 +1673,7 @@
|
||||||
"Yacy Password": "Contrasenya de Yacy",
|
"Yacy Password": "Contrasenya de Yacy",
|
||||||
"Yacy Username": "Nom d'usuari de Yacy",
|
"Yacy Username": "Nom d'usuari de Yacy",
|
||||||
"Yesterday": "Ahir",
|
"Yesterday": "Ahir",
|
||||||
"Yesterday at {{LOCALIZED_TIME}}": "",
|
"Yesterday at {{LOCALIZED_TIME}}": "Ahir a les {{LOCALIZED_TIME}}",
|
||||||
"You": "Tu",
|
"You": "Tu",
|
||||||
"You are currently using a trial license. Please contact support to upgrade your license.": "Actualment esteu utilitzant una llicència de prova. Poseu-vos en contacte amb el servei d'assistència per actualitzar la vostra llicència.",
|
"You are currently using a trial license. Please contact support to upgrade your license.": "Actualment esteu utilitzant una llicència de prova. Poseu-vos en contacte amb el servei d'assistència per actualitzar la vostra llicència.",
|
||||||
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "Només pots xatejar amb un màxim de {{maxCount}} fitxers alhora.",
|
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "Només pots xatejar amb un màxim de {{maxCount}} fitxers alhora.",
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@
|
||||||
"Advanced Parameters": "",
|
"Advanced Parameters": "",
|
||||||
"Advanced Params": "",
|
"Advanced Params": "",
|
||||||
"AI": "",
|
"AI": "",
|
||||||
|
"Generated Image": "Generated Image",
|
||||||
"All": "",
|
"All": "",
|
||||||
"All Documents": "",
|
"All Documents": "",
|
||||||
"All models deleted successfully": "",
|
"All models deleted successfully": "",
|
||||||
|
|
|
||||||
|
|
@ -799,6 +799,15 @@ export const isValidHttpUrl = (string: string) => {
|
||||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isYoutubeUrl = (url: string) => {
|
||||||
|
return (
|
||||||
|
url.startsWith('https://www.youtube.com') ||
|
||||||
|
url.startsWith('https://youtu.be') ||
|
||||||
|
url.startsWith('https://youtube.com') ||
|
||||||
|
url.startsWith('https://m.youtube.com')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const removeEmojis = (str: string) => {
|
export const removeEmojis = (str: string) => {
|
||||||
// Regular expression to match emojis
|
// Regular expression to match emojis
|
||||||
const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g;
|
const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import Chat from '$lib/components/chat/Chat.svelte';
|
import Chat from '$lib/components/chat/Chat.svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if ($page.url.searchParams.get('error')) {
|
||||||
|
toast.error($page.url.searchParams.get('error') || 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Chat />
|
<Chat />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue