mirror of
https://git.mirrors.martin98.com/https://github.com/open-webui/open-webui
synced 2025-08-20 12:39:13 +08:00
Merge branch 'open-webui:main' into fix/oidc-500-error-name-field
This commit is contained in:
commit
9eaf01c323
@ -52,6 +52,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Cypress run
|
- name: Cypress run
|
||||||
uses: cypress-io/github-action@v6
|
uses: cypress-io/github-action@v6
|
||||||
|
env:
|
||||||
|
LIBGL_ALWAYS_SOFTWARE: 1
|
||||||
with:
|
with:
|
||||||
browser: chrome
|
browser: chrome
|
||||||
wait-on: 'http://localhost:3000'
|
wait-on: 'http://localhost:3000'
|
51
CHANGELOG.md
51
CHANGELOG.md
@ -5,6 +5,57 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.5.7] - 2025-01-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🌍 Enhanced Internationalization (i18n)**: Refined and expanded translations for greater global accessibility and a smoother experience for international users.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔗 Connection Model ID Resolution**: Resolved an issue preventing model IDs from registering in connections.
|
||||||
|
- **💡 Prefix ID for Ollama Connections**: Fixed a bug where prefix IDs in Ollama connections were non-functional.
|
||||||
|
- **🔧 Ollama Model Enable/Disable Functionality**: Addressed the issue of enable/disable toggles not working for Ollama base models.
|
||||||
|
- **🔒 RBAC Permissions for Tools and Models**: Corrected incorrect Role-Based Access Control (RBAC) permissions for tools and models, ensuring that users now only access features according to their assigned privileges, enhancing security and role clarity.
|
||||||
|
|
||||||
|
## [0.5.6] - 2025-01-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🧠 Effortful Reasoning Control for OpenAI Models**: Introduced the reasoning_effort parameter in chat controls for supported OpenAI models, enabling users to fine-tune how much cognitive effort a model dedicates to its responses, offering greater customization for complex queries and reasoning tasks.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔄 Chat Controls Loading UI Bug**: Resolved an issue where collapsible chat controls appeared as "loading," ensuring a smoother and more intuitive user experience for managing chat settings.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🔧 Updated Ollama Model Creation**: Revamped the Ollama model creation method to align with their new JSON payload format, ensuring seamless compatibility and more efficient model setup workflows.
|
||||||
|
|
||||||
|
## [0.5.5] - 2025-01-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🤔 Native 'Think' Tag Support**: Introduced the new 'think' tag support that visually displays how long the model is thinking, omitting the reasoning content itself until the next turn. Ideal for creating a more streamlined and focused interaction experience.
|
||||||
|
- **🖼️ Toggle Image Generation On/Off**: In the chat input menu, you can now easily toggle image generation before initiating chats, providing greater control and flexibility to suit your needs.
|
||||||
|
- **🔒 Chat Controls Permissions**: Admins can now disable chat controls access for users, offering tighter management and customization over user interactions.
|
||||||
|
- **🔍 Web Search & Image Generation Permissions**: Easily disable web search and image generation for specific users, improving workflow governance and security for certain environments.
|
||||||
|
- **🗂️ S3 and GCS Storage Provider Support**: Scaled deployments now benefit from expanded storage options with Amazon S3 and Google Cloud Storage seamlessly integrated as providers.
|
||||||
|
- **🎨 Enhanced Model Management**: Reintroduced the ability to download and delete models directly in the admin models settings page to minimize user confusion and aid efficient model management.
|
||||||
|
- **🔗 Improved Connection Handling**: Enhanced backend to smoothly handle multiple identical base URLs, allowing more flexible multi-instance configurations with fewer hiccups.
|
||||||
|
- **✨ General UI/UX Refinements**: Numerous tweaks across the WebUI make navigation and usability even more user-friendly and intuitive.
|
||||||
|
- **🌍 Translation Enhancements**: Various translation updates ensure smoother and more polished interactions for international users.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **⚡ MPS Functionality for Mac Users**: Fixed MPS support, ensuring smooth performance and compatibility for Mac users leveraging MPS.
|
||||||
|
- **📡 Ollama Connection Management**: Resolved the issue where deleting all Ollama connections prevented adding new ones.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **⚙️ General Stability Refac**: Backend refactoring delivers a more stable, robust platform.
|
||||||
|
- **🖥️ Desktop App Preparations**: Ongoing work to support the upcoming Open WebUI desktop app. Follow our progress and updates here: https://github.com/open-webui/desktop
|
||||||
|
|
||||||
## [0.5.4] - 2025-01-05
|
## [0.5.4] - 2025-01-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -5,12 +5,31 @@ from pathlib import Path
|
|||||||
|
|
||||||
import typer
|
import typer
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
from typing import Optional
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
KEY_FILE = Path.cwd() / ".webui_secret_key"
|
KEY_FILE = Path.cwd() / ".webui_secret_key"
|
||||||
|
|
||||||
|
|
||||||
|
def version_callback(value: bool):
|
||||||
|
if value:
|
||||||
|
from open_webui.env import VERSION
|
||||||
|
|
||||||
|
typer.echo(f"Open WebUI version: {VERSION}")
|
||||||
|
raise typer.Exit()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def main(
|
||||||
|
version: Annotated[
|
||||||
|
Optional[bool], typer.Option("--version", callback=version_callback)
|
||||||
|
] = None,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def serve(
|
def serve(
|
||||||
host: str = "0.0.0.0",
|
host: str = "0.0.0.0",
|
||||||
|
@ -9,22 +9,22 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import chromadb
|
import chromadb
|
||||||
import requests
|
import requests
|
||||||
import yaml
|
from pydantic import BaseModel
|
||||||
from open_webui.internal.db import Base, get_db
|
from sqlalchemy import JSON, Column, DateTime, Integer, func
|
||||||
|
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
OPEN_WEBUI_DIR,
|
|
||||||
DATA_DIR,
|
DATA_DIR,
|
||||||
|
DATABASE_URL,
|
||||||
ENV,
|
ENV,
|
||||||
FRONTEND_BUILD_DIR,
|
FRONTEND_BUILD_DIR,
|
||||||
|
OFFLINE_MODE,
|
||||||
|
OPEN_WEBUI_DIR,
|
||||||
WEBUI_AUTH,
|
WEBUI_AUTH,
|
||||||
WEBUI_FAVICON_URL,
|
WEBUI_FAVICON_URL,
|
||||||
WEBUI_NAME,
|
WEBUI_NAME,
|
||||||
log,
|
log,
|
||||||
DATABASE_URL,
|
|
||||||
OFFLINE_MODE,
|
|
||||||
)
|
)
|
||||||
from pydantic import BaseModel
|
from open_webui.internal.db import Base, get_db
|
||||||
from sqlalchemy import JSON, Column, DateTime, Integer, func
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointFilter(logging.Filter):
|
class EndpointFilter(logging.Filter):
|
||||||
@ -362,6 +362,30 @@ MICROSOFT_REDIRECT_URI = PersistentConfig(
|
|||||||
os.environ.get("MICROSOFT_REDIRECT_URI", ""),
|
os.environ.get("MICROSOFT_REDIRECT_URI", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GITHUB_CLIENT_ID = PersistentConfig(
|
||||||
|
"GITHUB_CLIENT_ID",
|
||||||
|
"oauth.github.client_id",
|
||||||
|
os.environ.get("GITHUB_CLIENT_ID", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
GITHUB_CLIENT_SECRET = PersistentConfig(
|
||||||
|
"GITHUB_CLIENT_SECRET",
|
||||||
|
"oauth.github.client_secret",
|
||||||
|
os.environ.get("GITHUB_CLIENT_SECRET", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
GITHUB_CLIENT_SCOPE = PersistentConfig(
|
||||||
|
"GITHUB_CLIENT_SCOPE",
|
||||||
|
"oauth.github.scope",
|
||||||
|
os.environ.get("GITHUB_CLIENT_SCOPE", "user:email"),
|
||||||
|
)
|
||||||
|
|
||||||
|
GITHUB_CLIENT_REDIRECT_URI = PersistentConfig(
|
||||||
|
"GITHUB_CLIENT_REDIRECT_URI",
|
||||||
|
"oauth.github.redirect_uri",
|
||||||
|
os.environ.get("GITHUB_CLIENT_REDIRECT_URI", ""),
|
||||||
|
)
|
||||||
|
|
||||||
OAUTH_CLIENT_ID = PersistentConfig(
|
OAUTH_CLIENT_ID = PersistentConfig(
|
||||||
"OAUTH_CLIENT_ID",
|
"OAUTH_CLIENT_ID",
|
||||||
"oauth.oidc.client_id",
|
"oauth.oidc.client_id",
|
||||||
@ -468,12 +492,20 @@ OAUTH_ALLOWED_DOMAINS = PersistentConfig(
|
|||||||
def load_oauth_providers():
|
def load_oauth_providers():
|
||||||
OAUTH_PROVIDERS.clear()
|
OAUTH_PROVIDERS.clear()
|
||||||
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
|
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
|
||||||
|
|
||||||
|
def google_oauth_register(client):
|
||||||
|
client.register(
|
||||||
|
name="google",
|
||||||
|
client_id=GOOGLE_CLIENT_ID.value,
|
||||||
|
client_secret=GOOGLE_CLIENT_SECRET.value,
|
||||||
|
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
||||||
|
client_kwargs={"scope": GOOGLE_OAUTH_SCOPE.value},
|
||||||
|
redirect_uri=GOOGLE_REDIRECT_URI.value,
|
||||||
|
)
|
||||||
|
|
||||||
OAUTH_PROVIDERS["google"] = {
|
OAUTH_PROVIDERS["google"] = {
|
||||||
"client_id": GOOGLE_CLIENT_ID.value,
|
|
||||||
"client_secret": GOOGLE_CLIENT_SECRET.value,
|
|
||||||
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
|
||||||
"scope": GOOGLE_OAUTH_SCOPE.value,
|
|
||||||
"redirect_uri": GOOGLE_REDIRECT_URI.value,
|
"redirect_uri": GOOGLE_REDIRECT_URI.value,
|
||||||
|
"register": google_oauth_register,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -481,12 +513,44 @@ def load_oauth_providers():
|
|||||||
and MICROSOFT_CLIENT_SECRET.value
|
and MICROSOFT_CLIENT_SECRET.value
|
||||||
and MICROSOFT_CLIENT_TENANT_ID.value
|
and MICROSOFT_CLIENT_TENANT_ID.value
|
||||||
):
|
):
|
||||||
|
|
||||||
|
def microsoft_oauth_register(client):
|
||||||
|
client.register(
|
||||||
|
name="microsoft",
|
||||||
|
client_id=MICROSOFT_CLIENT_ID.value,
|
||||||
|
client_secret=MICROSOFT_CLIENT_SECRET.value,
|
||||||
|
server_metadata_url=f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
|
||||||
|
client_kwargs={
|
||||||
|
"scope": MICROSOFT_OAUTH_SCOPE.value,
|
||||||
|
},
|
||||||
|
redirect_uri=MICROSOFT_REDIRECT_URI.value,
|
||||||
|
)
|
||||||
|
|
||||||
OAUTH_PROVIDERS["microsoft"] = {
|
OAUTH_PROVIDERS["microsoft"] = {
|
||||||
"client_id": MICROSOFT_CLIENT_ID.value,
|
|
||||||
"client_secret": MICROSOFT_CLIENT_SECRET.value,
|
|
||||||
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
|
|
||||||
"scope": MICROSOFT_OAUTH_SCOPE.value,
|
|
||||||
"redirect_uri": MICROSOFT_REDIRECT_URI.value,
|
"redirect_uri": MICROSOFT_REDIRECT_URI.value,
|
||||||
|
"picture_url": "https://graph.microsoft.com/v1.0/me/photo/$value",
|
||||||
|
"register": microsoft_oauth_register,
|
||||||
|
}
|
||||||
|
|
||||||
|
if GITHUB_CLIENT_ID.value and GITHUB_CLIENT_SECRET.value:
|
||||||
|
|
||||||
|
def github_oauth_register(client):
|
||||||
|
client.register(
|
||||||
|
name="github",
|
||||||
|
client_id=GITHUB_CLIENT_ID.value,
|
||||||
|
client_secret=GITHUB_CLIENT_SECRET.value,
|
||||||
|
access_token_url="https://github.com/login/oauth/access_token",
|
||||||
|
authorize_url="https://github.com/login/oauth/authorize",
|
||||||
|
api_base_url="https://api.github.com",
|
||||||
|
userinfo_endpoint="https://api.github.com/user",
|
||||||
|
client_kwargs={"scope": GITHUB_CLIENT_SCOPE.value},
|
||||||
|
redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
OAUTH_PROVIDERS["github"] = {
|
||||||
|
"redirect_uri": GITHUB_CLIENT_REDIRECT_URI.value,
|
||||||
|
"register": github_oauth_register,
|
||||||
|
"sub_claim": "id",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -494,13 +558,23 @@ def load_oauth_providers():
|
|||||||
and OAUTH_CLIENT_SECRET.value
|
and OAUTH_CLIENT_SECRET.value
|
||||||
and OPENID_PROVIDER_URL.value
|
and OPENID_PROVIDER_URL.value
|
||||||
):
|
):
|
||||||
|
|
||||||
|
def oidc_oauth_register(client):
|
||||||
|
client.register(
|
||||||
|
name="oidc",
|
||||||
|
client_id=OAUTH_CLIENT_ID.value,
|
||||||
|
client_secret=OAUTH_CLIENT_SECRET.value,
|
||||||
|
server_metadata_url=OPENID_PROVIDER_URL.value,
|
||||||
|
client_kwargs={
|
||||||
|
"scope": OAUTH_SCOPES.value,
|
||||||
|
},
|
||||||
|
redirect_uri=OPENID_REDIRECT_URI.value,
|
||||||
|
)
|
||||||
|
|
||||||
OAUTH_PROVIDERS["oidc"] = {
|
OAUTH_PROVIDERS["oidc"] = {
|
||||||
"client_id": OAUTH_CLIENT_ID.value,
|
|
||||||
"client_secret": OAUTH_CLIENT_SECRET.value,
|
|
||||||
"server_metadata_url": OPENID_PROVIDER_URL.value,
|
|
||||||
"scope": OAUTH_SCOPES.value,
|
|
||||||
"name": OAUTH_PROVIDER_NAME.value,
|
"name": OAUTH_PROVIDER_NAME.value,
|
||||||
"redirect_uri": OPENID_REDIRECT_URI.value,
|
"redirect_uri": OPENID_REDIRECT_URI.value,
|
||||||
|
"register": oidc_oauth_register,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -580,7 +654,7 @@ if CUSTOM_NAME:
|
|||||||
# STORAGE PROVIDER
|
# STORAGE PROVIDER
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
STORAGE_PROVIDER = os.environ.get("STORAGE_PROVIDER", "") # defaults to local, s3
|
STORAGE_PROVIDER = os.environ.get("STORAGE_PROVIDER", "local") # defaults to local, s3
|
||||||
|
|
||||||
S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None)
|
S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None)
|
||||||
S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None)
|
S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None)
|
||||||
@ -588,6 +662,11 @@ S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None)
|
|||||||
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
|
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
|
||||||
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
|
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
|
||||||
|
|
||||||
|
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None)
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get(
|
||||||
|
"GOOGLE_APPLICATION_CREDENTIALS_JSON", None
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# File Upload DIR
|
# File Upload DIR
|
||||||
####################################
|
####################################
|
||||||
@ -819,6 +898,10 @@ USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = (
|
|||||||
os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS", "False").lower() == "true"
|
os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS", "False").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
USER_PERMISSIONS_CHAT_CONTROLS = (
|
||||||
|
os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
USER_PERMISSIONS_CHAT_FILE_UPLOAD = (
|
USER_PERMISSIONS_CHAT_FILE_UPLOAD = (
|
||||||
os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
|
os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
|
||||||
)
|
)
|
||||||
@ -835,23 +918,39 @@ USER_PERMISSIONS_CHAT_TEMPORARY = (
|
|||||||
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
|
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
USER_PERMISSIONS_FEATURES_WEB_SEARCH = (
|
||||||
|
os.environ.get("USER_PERMISSIONS_FEATURES_WEB_SEARCH", "True").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = (
|
||||||
|
os.environ.get("USER_PERMISSIONS_FEATURES_IMAGE_GENERATION", "True").lower()
|
||||||
|
== "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_USER_PERMISSIONS = {
|
||||||
|
"workspace": {
|
||||||
|
"models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
|
||||||
|
"knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS,
|
||||||
|
"prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS,
|
||||||
|
"tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS,
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"controls": USER_PERMISSIONS_CHAT_CONTROLS,
|
||||||
|
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
|
||||||
|
"delete": USER_PERMISSIONS_CHAT_DELETE,
|
||||||
|
"edit": USER_PERMISSIONS_CHAT_EDIT,
|
||||||
|
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH,
|
||||||
|
"image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
USER_PERMISSIONS = PersistentConfig(
|
USER_PERMISSIONS = PersistentConfig(
|
||||||
"USER_PERMISSIONS",
|
"USER_PERMISSIONS",
|
||||||
"user.permissions",
|
"user.permissions",
|
||||||
{
|
DEFAULT_USER_PERMISSIONS,
|
||||||
"workspace": {
|
|
||||||
"models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
|
|
||||||
"knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS,
|
|
||||||
"prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS,
|
|
||||||
"tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS,
|
|
||||||
},
|
|
||||||
"chat": {
|
|
||||||
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
|
|
||||||
"delete": USER_PERMISSIONS_CHAT_DELETE,
|
|
||||||
"edit": USER_PERMISSIONS_CHAT_EDIT,
|
|
||||||
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ENABLE_CHANNELS = PersistentConfig(
|
ENABLE_CHANNELS = PersistentConfig(
|
||||||
@ -1034,6 +1133,32 @@ JSON format: { "tags": ["tag1", "tag2", "tag3"] }
|
|||||||
{{MESSAGES:END:6}}
|
{{MESSAGES:END:6}}
|
||||||
</chat_history>"""
|
</chat_history>"""
|
||||||
|
|
||||||
|
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||||
|
"IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE",
|
||||||
|
"task.image.prompt_template",
|
||||||
|
os.environ.get("IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||||
|
Generate a detailed prompt for am image generation task based on the given language and context. Describe the image as if you were explaining it to someone who cannot see it. Include relevant details, colors, shapes, and any other important elements.
|
||||||
|
|
||||||
|
### Guidelines:
|
||||||
|
- Be descriptive and detailed, focusing on the most important aspects of the image.
|
||||||
|
- Avoid making assumptions or adding information not present in the image.
|
||||||
|
- Use the chat's primary language; default to English if multilingual.
|
||||||
|
- If the image is too complex, focus on the most prominent elements.
|
||||||
|
|
||||||
|
### Output:
|
||||||
|
Strictly return in JSON format:
|
||||||
|
{
|
||||||
|
"prompt": "Your detailed description here."
|
||||||
|
}
|
||||||
|
|
||||||
|
### Chat History:
|
||||||
|
<chat_history>
|
||||||
|
{{MESSAGES:END:6}}
|
||||||
|
</chat_history>"""
|
||||||
|
|
||||||
ENABLE_TAGS_GENERATION = PersistentConfig(
|
ENABLE_TAGS_GENERATION = PersistentConfig(
|
||||||
"ENABLE_TAGS_GENERATION",
|
"ENABLE_TAGS_GENERATION",
|
||||||
"task.tags.enable",
|
"task.tags.enable",
|
||||||
@ -1193,6 +1318,7 @@ CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
|
|||||||
# Milvus
|
# Milvus
|
||||||
|
|
||||||
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
|
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
|
||||||
|
MILVUS_DB = os.environ.get("MILVUS_DB", "default")
|
||||||
|
|
||||||
# Qdrant
|
# Qdrant
|
||||||
QDRANT_URI = os.environ.get("QDRANT_URI", None)
|
QDRANT_URI = os.environ.get("QDRANT_URI", None)
|
||||||
@ -1602,6 +1728,13 @@ ENABLE_IMAGE_GENERATION = PersistentConfig(
|
|||||||
"image_generation.enable",
|
"image_generation.enable",
|
||||||
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
|
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ENABLE_IMAGE_PROMPT_GENERATION = PersistentConfig(
|
||||||
|
"ENABLE_IMAGE_PROMPT_GENERATION",
|
||||||
|
"image_generation.prompt.enable",
|
||||||
|
os.environ.get("ENABLE_IMAGE_PROMPT_GENERATION", "true").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
AUTOMATIC1111_BASE_URL = PersistentConfig(
|
AUTOMATIC1111_BASE_URL = PersistentConfig(
|
||||||
"AUTOMATIC1111_BASE_URL",
|
"AUTOMATIC1111_BASE_URL",
|
||||||
"image_generation.automatic1111.base_url",
|
"image_generation.automatic1111.base_url",
|
||||||
@ -1931,6 +2064,12 @@ LDAP_SERVER_PORT = PersistentConfig(
|
|||||||
int(os.environ.get("LDAP_SERVER_PORT", "389")),
|
int(os.environ.get("LDAP_SERVER_PORT", "389")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LDAP_ATTRIBUTE_FOR_MAIL = PersistentConfig(
|
||||||
|
"LDAP_ATTRIBUTE_FOR_MAIL",
|
||||||
|
"ldap.server.attribute_for_mail",
|
||||||
|
os.environ.get("LDAP_ATTRIBUTE_FOR_MAIL", "mail"),
|
||||||
|
)
|
||||||
|
|
||||||
LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig(
|
LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig(
|
||||||
"LDAP_ATTRIBUTE_FOR_USERNAME",
|
"LDAP_ATTRIBUTE_FOR_USERNAME",
|
||||||
"ldap.server.attribute_for_username",
|
"ldap.server.attribute_for_username",
|
||||||
|
@ -113,6 +113,7 @@ class TASKS(str, Enum):
|
|||||||
TAGS_GENERATION = "tags_generation"
|
TAGS_GENERATION = "tags_generation"
|
||||||
EMOJI_GENERATION = "emoji_generation"
|
EMOJI_GENERATION = "emoji_generation"
|
||||||
QUERY_GENERATION = "query_generation"
|
QUERY_GENERATION = "query_generation"
|
||||||
|
IMAGE_PROMPT_GENERATION = "image_prompt_generation"
|
||||||
AUTOCOMPLETE_GENERATION = "autocomplete_generation"
|
AUTOCOMPLETE_GENERATION = "autocomplete_generation"
|
||||||
FUNCTION_CALLING = "function_calling"
|
FUNCTION_CALLING = "function_calling"
|
||||||
MOA_RESPONSE_GENERATION = "moa_response_generation"
|
MOA_RESPONSE_GENERATION = "moa_response_generation"
|
||||||
|
@ -274,6 +274,8 @@ DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
|
|||||||
if "postgres://" in DATABASE_URL:
|
if "postgres://" in DATABASE_URL:
|
||||||
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
|
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
|
||||||
|
|
||||||
|
DATABASE_SCHEMA = os.environ.get("DATABASE_SCHEMA", None)
|
||||||
|
|
||||||
DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", 0)
|
DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", 0)
|
||||||
|
|
||||||
if DATABASE_POOL_SIZE == "":
|
if DATABASE_POOL_SIZE == "":
|
||||||
|
@ -7,6 +7,7 @@ from open_webui.internal.wrappers import register_connection
|
|||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
OPEN_WEBUI_DIR,
|
OPEN_WEBUI_DIR,
|
||||||
DATABASE_URL,
|
DATABASE_URL,
|
||||||
|
DATABASE_SCHEMA,
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
DATABASE_POOL_MAX_OVERFLOW,
|
DATABASE_POOL_MAX_OVERFLOW,
|
||||||
DATABASE_POOL_RECYCLE,
|
DATABASE_POOL_RECYCLE,
|
||||||
@ -14,7 +15,7 @@ from open_webui.env import (
|
|||||||
DATABASE_POOL_TIMEOUT,
|
DATABASE_POOL_TIMEOUT,
|
||||||
)
|
)
|
||||||
from peewee_migrate import Router
|
from peewee_migrate import Router
|
||||||
from sqlalchemy import Dialect, create_engine, types
|
from sqlalchemy import Dialect, create_engine, MetaData, types
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
from sqlalchemy.pool import QueuePool, NullPool
|
from sqlalchemy.pool import QueuePool, NullPool
|
||||||
@ -99,7 +100,8 @@ else:
|
|||||||
SessionLocal = sessionmaker(
|
SessionLocal = sessionmaker(
|
||||||
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
|
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
|
||||||
)
|
)
|
||||||
Base = declarative_base()
|
metadata_obj = MetaData(schema=DATABASE_SCHEMA)
|
||||||
|
Base = declarative_base(metadata=metadata_obj)
|
||||||
Session = scoped_session(SessionLocal)
|
Session = scoped_session(SessionLocal)
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ from open_webui.config import (
|
|||||||
COMFYUI_WORKFLOW,
|
COMFYUI_WORKFLOW,
|
||||||
COMFYUI_WORKFLOW_NODES,
|
COMFYUI_WORKFLOW_NODES,
|
||||||
ENABLE_IMAGE_GENERATION,
|
ENABLE_IMAGE_GENERATION,
|
||||||
|
ENABLE_IMAGE_PROMPT_GENERATION,
|
||||||
IMAGE_GENERATION_ENGINE,
|
IMAGE_GENERATION_ENGINE,
|
||||||
IMAGE_GENERATION_MODEL,
|
IMAGE_GENERATION_MODEL,
|
||||||
IMAGE_SIZE,
|
IMAGE_SIZE,
|
||||||
@ -225,6 +226,7 @@ from open_webui.config import (
|
|||||||
LDAP_SERVER_LABEL,
|
LDAP_SERVER_LABEL,
|
||||||
LDAP_SERVER_HOST,
|
LDAP_SERVER_HOST,
|
||||||
LDAP_SERVER_PORT,
|
LDAP_SERVER_PORT,
|
||||||
|
LDAP_ATTRIBUTE_FOR_MAIL,
|
||||||
LDAP_ATTRIBUTE_FOR_USERNAME,
|
LDAP_ATTRIBUTE_FOR_USERNAME,
|
||||||
LDAP_SEARCH_FILTERS,
|
LDAP_SEARCH_FILTERS,
|
||||||
LDAP_SEARCH_BASE,
|
LDAP_SEARCH_BASE,
|
||||||
@ -254,6 +256,7 @@ from open_webui.config import (
|
|||||||
ENABLE_AUTOCOMPLETE_GENERATION,
|
ENABLE_AUTOCOMPLETE_GENERATION,
|
||||||
TITLE_GENERATION_PROMPT_TEMPLATE,
|
TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
TAGS_GENERATION_PROMPT_TEMPLATE,
|
TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
|
||||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||||
QUERY_GENERATION_PROMPT_TEMPLATE,
|
QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
|
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
|
||||||
@ -437,6 +440,7 @@ app.state.config.ENABLE_LDAP = ENABLE_LDAP
|
|||||||
app.state.config.LDAP_SERVER_LABEL = LDAP_SERVER_LABEL
|
app.state.config.LDAP_SERVER_LABEL = LDAP_SERVER_LABEL
|
||||||
app.state.config.LDAP_SERVER_HOST = LDAP_SERVER_HOST
|
app.state.config.LDAP_SERVER_HOST = LDAP_SERVER_HOST
|
||||||
app.state.config.LDAP_SERVER_PORT = LDAP_SERVER_PORT
|
app.state.config.LDAP_SERVER_PORT = LDAP_SERVER_PORT
|
||||||
|
app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = LDAP_ATTRIBUTE_FOR_MAIL
|
||||||
app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = LDAP_ATTRIBUTE_FOR_USERNAME
|
app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = LDAP_ATTRIBUTE_FOR_USERNAME
|
||||||
app.state.config.LDAP_APP_DN = LDAP_APP_DN
|
app.state.config.LDAP_APP_DN = LDAP_APP_DN
|
||||||
app.state.config.LDAP_APP_PASSWORD = LDAP_APP_PASSWORD
|
app.state.config.LDAP_APP_PASSWORD = LDAP_APP_PASSWORD
|
||||||
@ -572,6 +576,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
|||||||
|
|
||||||
app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE
|
app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE
|
||||||
app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
|
app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
|
||||||
|
app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
|
||||||
|
|
||||||
app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
||||||
app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
||||||
@ -642,6 +647,10 @@ app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
|
|||||||
|
|
||||||
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
||||||
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
|
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
|
||||||
|
app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = (
|
||||||
|
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
|
||||||
|
)
|
||||||
|
|
||||||
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
||||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||||
)
|
)
|
||||||
|
@ -393,7 +393,7 @@ class ChatTable:
|
|||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> list[ChatModel]:
|
) -> list[ChatModel]:
|
||||||
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_archived:
|
if not include_archived:
|
||||||
query = query.filter_by(archived=False)
|
query = query.filter_by(archived=False)
|
||||||
|
|
||||||
|
@ -80,12 +80,11 @@ class GroupResponse(BaseModel):
|
|||||||
class GroupForm(BaseModel):
|
class GroupForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
|
permissions: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
class GroupUpdateForm(GroupForm):
|
class GroupUpdateForm(GroupForm):
|
||||||
permissions: Optional[dict] = None
|
|
||||||
user_ids: Optional[list[str]] = None
|
user_ids: Optional[list[str]] = None
|
||||||
admin_ids: Optional[list[str]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class GroupTable:
|
class GroupTable:
|
||||||
@ -95,7 +94,7 @@ class GroupTable:
|
|||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
group = GroupModel(
|
group = GroupModel(
|
||||||
**{
|
**{
|
||||||
**form_data.model_dump(),
|
**form_data.model_dump(exclude_none=True),
|
||||||
"id": str(uuid.uuid4()),
|
"id": str(uuid.uuid4()),
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
@ -189,5 +188,24 @@ class GroupTable:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def remove_user_from_all_groups(self, user_id: str) -> bool:
|
||||||
|
with get_db() as db:
|
||||||
|
try:
|
||||||
|
groups = self.get_groups_by_member_id(user_id)
|
||||||
|
|
||||||
|
for group in groups:
|
||||||
|
group.user_ids.remove(user_id)
|
||||||
|
db.query(Group).filter_by(id=group.id).update(
|
||||||
|
{
|
||||||
|
"user_ids": group.user_ids,
|
||||||
|
"updated_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
Groups = GroupTable()
|
Groups = GroupTable()
|
||||||
|
@ -2,7 +2,12 @@ import time
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.internal.db import Base, JSONField, get_db
|
from open_webui.internal.db import Base, JSONField, get_db
|
||||||
|
|
||||||
|
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
|
from open_webui.models.groups import Groups
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Column, String, Text
|
from sqlalchemy import BigInteger, Column, String, Text
|
||||||
|
|
||||||
@ -268,9 +273,11 @@ class UsersTable:
|
|||||||
|
|
||||||
def delete_user_by_id(self, id: str) -> bool:
|
def delete_user_by_id(self, id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
|
# Remove User from Groups
|
||||||
|
Groups.remove_user_from_all_groups(id)
|
||||||
|
|
||||||
# Delete User Chats
|
# Delete User Chats
|
||||||
result = Chats.delete_chats_by_user_id(id)
|
result = Chats.delete_chats_by_user_id(id)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
# Delete User
|
# Delete User
|
||||||
@ -300,5 +307,10 @@ class UsersTable:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_valid_user_ids(self, user_ids: list[str]) -> list[str]:
|
||||||
|
with get_db() as db:
|
||||||
|
users = db.query(User).filter(User.id.in_(user_ids)).all()
|
||||||
|
return [user.id for user in users]
|
||||||
|
|
||||||
|
|
||||||
Users = UsersTable()
|
Users = UsersTable()
|
||||||
|
@ -7,13 +7,14 @@ from typing import Optional
|
|||||||
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
MILVUS_URI,
|
MILVUS_URI,
|
||||||
|
MILVUS_DB,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MilvusClient:
|
class MilvusClient:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.collection_prefix = "open_webui"
|
self.collection_prefix = "open_webui"
|
||||||
self.client = Client(uri=MILVUS_URI)
|
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB)
|
||||||
|
|
||||||
def _result_to_get_result(self, result) -> GetResult:
|
def _result_to_get_result(self, result) -> GetResult:
|
||||||
ids = []
|
ids = []
|
||||||
|
@ -23,7 +23,7 @@ def search_bing(
|
|||||||
filter_list: Optional[list[str]] = None,
|
filter_list: Optional[list[str]] = None,
|
||||||
) -> list[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
mkt = locale
|
mkt = locale
|
||||||
params = {"q": query, "mkt": mkt, "answerCount": count}
|
params = {"q": query, "mkt": mkt, "count": count}
|
||||||
headers = {"Ocp-Apim-Subscription-Key": subscription_key}
|
headers = {"Ocp-Apim-Subscription-Key": subscription_key}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -170,6 +170,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||||||
LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL
|
LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL
|
||||||
LDAP_SERVER_HOST = request.app.state.config.LDAP_SERVER_HOST
|
LDAP_SERVER_HOST = request.app.state.config.LDAP_SERVER_HOST
|
||||||
LDAP_SERVER_PORT = request.app.state.config.LDAP_SERVER_PORT
|
LDAP_SERVER_PORT = request.app.state.config.LDAP_SERVER_PORT
|
||||||
|
LDAP_ATTRIBUTE_FOR_MAIL = request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL
|
||||||
LDAP_ATTRIBUTE_FOR_USERNAME = request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME
|
LDAP_ATTRIBUTE_FOR_USERNAME = request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME
|
||||||
LDAP_SEARCH_BASE = request.app.state.config.LDAP_SEARCH_BASE
|
LDAP_SEARCH_BASE = request.app.state.config.LDAP_SEARCH_BASE
|
||||||
LDAP_SEARCH_FILTERS = request.app.state.config.LDAP_SEARCH_FILTERS
|
LDAP_SEARCH_FILTERS = request.app.state.config.LDAP_SEARCH_FILTERS
|
||||||
@ -218,7 +219,11 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||||||
search_success = connection_app.search(
|
search_success = connection_app.search(
|
||||||
search_base=LDAP_SEARCH_BASE,
|
search_base=LDAP_SEARCH_BASE,
|
||||||
search_filter=f"(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})",
|
search_filter=f"(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})",
|
||||||
attributes=[f"{LDAP_ATTRIBUTE_FOR_USERNAME}", "mail", "cn"],
|
attributes=[
|
||||||
|
f"{LDAP_ATTRIBUTE_FOR_USERNAME}",
|
||||||
|
f"{LDAP_ATTRIBUTE_FOR_MAIL}",
|
||||||
|
"cn",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
if not search_success:
|
if not search_success:
|
||||||
@ -226,7 +231,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||||||
|
|
||||||
entry = connection_app.entries[0]
|
entry = connection_app.entries[0]
|
||||||
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
|
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
|
||||||
mail = str(entry["mail"])
|
mail = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
|
||||||
|
if not mail or mail == "" or mail == "[]":
|
||||||
|
raise HTTPException(400, f"User {form_data.user} does not have mail.")
|
||||||
cn = str(entry["cn"])
|
cn = str(entry["cn"])
|
||||||
user_dn = entry.entry_dn
|
user_dn = entry.entry_dn
|
||||||
|
|
||||||
@ -691,6 +698,7 @@ class LdapServerConfig(BaseModel):
|
|||||||
label: str
|
label: str
|
||||||
host: str
|
host: str
|
||||||
port: Optional[int] = None
|
port: Optional[int] = None
|
||||||
|
attribute_for_mail: str = "mail"
|
||||||
attribute_for_username: str = "uid"
|
attribute_for_username: str = "uid"
|
||||||
app_dn: str
|
app_dn: str
|
||||||
app_dn_password: str
|
app_dn_password: str
|
||||||
@ -707,6 +715,7 @@ async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
|
|||||||
"label": request.app.state.config.LDAP_SERVER_LABEL,
|
"label": request.app.state.config.LDAP_SERVER_LABEL,
|
||||||
"host": request.app.state.config.LDAP_SERVER_HOST,
|
"host": request.app.state.config.LDAP_SERVER_HOST,
|
||||||
"port": request.app.state.config.LDAP_SERVER_PORT,
|
"port": request.app.state.config.LDAP_SERVER_PORT,
|
||||||
|
"attribute_for_mail": request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL,
|
||||||
"attribute_for_username": request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
|
"attribute_for_username": request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
|
||||||
"app_dn": request.app.state.config.LDAP_APP_DN,
|
"app_dn": request.app.state.config.LDAP_APP_DN,
|
||||||
"app_dn_password": request.app.state.config.LDAP_APP_PASSWORD,
|
"app_dn_password": request.app.state.config.LDAP_APP_PASSWORD,
|
||||||
@ -725,6 +734,7 @@ async def update_ldap_server(
|
|||||||
required_fields = [
|
required_fields = [
|
||||||
"label",
|
"label",
|
||||||
"host",
|
"host",
|
||||||
|
"attribute_for_mail",
|
||||||
"attribute_for_username",
|
"attribute_for_username",
|
||||||
"app_dn",
|
"app_dn",
|
||||||
"app_dn_password",
|
"app_dn_password",
|
||||||
@ -743,6 +753,7 @@ async def update_ldap_server(
|
|||||||
request.app.state.config.LDAP_SERVER_LABEL = form_data.label
|
request.app.state.config.LDAP_SERVER_LABEL = form_data.label
|
||||||
request.app.state.config.LDAP_SERVER_HOST = form_data.host
|
request.app.state.config.LDAP_SERVER_HOST = form_data.host
|
||||||
request.app.state.config.LDAP_SERVER_PORT = form_data.port
|
request.app.state.config.LDAP_SERVER_PORT = form_data.port
|
||||||
|
request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = form_data.attribute_for_mail
|
||||||
request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = (
|
request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = (
|
||||||
form_data.attribute_for_username
|
form_data.attribute_for_username
|
||||||
)
|
)
|
||||||
@ -758,6 +769,7 @@ async def update_ldap_server(
|
|||||||
"label": request.app.state.config.LDAP_SERVER_LABEL,
|
"label": request.app.state.config.LDAP_SERVER_LABEL,
|
||||||
"host": request.app.state.config.LDAP_SERVER_HOST,
|
"host": request.app.state.config.LDAP_SERVER_HOST,
|
||||||
"port": request.app.state.config.LDAP_SERVER_PORT,
|
"port": request.app.state.config.LDAP_SERVER_PORT,
|
||||||
|
"attribute_for_mail": request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL,
|
||||||
"attribute_for_username": request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
|
"attribute_for_username": request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
|
||||||
"app_dn": request.app.state.config.LDAP_APP_DN,
|
"app_dn": request.app.state.config.LDAP_APP_DN,
|
||||||
"app_dn_password": request.app.state.config.LDAP_APP_PASSWORD,
|
"app_dn_password": request.app.state.config.LDAP_APP_PASSWORD,
|
||||||
|
@ -345,6 +345,8 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||||||
async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
|
async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
file = Files.get_file_by_id(id)
|
file = Files.get_file_by_id(id)
|
||||||
if file and (file.user_id == user.id or user.role == "admin"):
|
if file and (file.user_id == user.id or user.role == "admin"):
|
||||||
|
# We should add Chroma cleanup here
|
||||||
|
|
||||||
result = Files.delete_file_by_id(id)
|
result = Files.delete_file_by_id(id)
|
||||||
if result:
|
if result:
|
||||||
try:
|
try:
|
||||||
|
@ -2,6 +2,8 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.models.users import Users
|
||||||
from open_webui.models.groups import (
|
from open_webui.models.groups import (
|
||||||
Groups,
|
Groups,
|
||||||
GroupForm,
|
GroupForm,
|
||||||
@ -80,6 +82,9 @@ async def update_group_by_id(
|
|||||||
id: str, form_data: GroupUpdateForm, user=Depends(get_admin_user)
|
id: str, form_data: GroupUpdateForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
if form_data.user_ids:
|
||||||
|
form_data.user_ids = Users.get_valid_user_ids(form_data.user_ids)
|
||||||
|
|
||||||
group = Groups.update_group_by_id(id, form_data)
|
group = Groups.update_group_by_id(id, form_data)
|
||||||
if group:
|
if group:
|
||||||
return group
|
return group
|
||||||
|
@ -43,6 +43,7 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
|
|||||||
return {
|
return {
|
||||||
"enabled": request.app.state.config.ENABLE_IMAGE_GENERATION,
|
"enabled": request.app.state.config.ENABLE_IMAGE_GENERATION,
|
||||||
"engine": request.app.state.config.IMAGE_GENERATION_ENGINE,
|
"engine": request.app.state.config.IMAGE_GENERATION_ENGINE,
|
||||||
|
"prompt_generation": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
|
||||||
"openai": {
|
"openai": {
|
||||||
"OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
|
"OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
|
||||||
"OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
|
"OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
|
||||||
@ -86,6 +87,7 @@ class ComfyUIConfigForm(BaseModel):
|
|||||||
class ConfigForm(BaseModel):
|
class ConfigForm(BaseModel):
|
||||||
enabled: bool
|
enabled: bool
|
||||||
engine: str
|
engine: str
|
||||||
|
prompt_generation: bool
|
||||||
openai: OpenAIConfigForm
|
openai: OpenAIConfigForm
|
||||||
automatic1111: Automatic1111ConfigForm
|
automatic1111: Automatic1111ConfigForm
|
||||||
comfyui: ComfyUIConfigForm
|
comfyui: ComfyUIConfigForm
|
||||||
@ -98,6 +100,10 @@ async def update_config(
|
|||||||
request.app.state.config.IMAGE_GENERATION_ENGINE = form_data.engine
|
request.app.state.config.IMAGE_GENERATION_ENGINE = form_data.engine
|
||||||
request.app.state.config.ENABLE_IMAGE_GENERATION = form_data.enabled
|
request.app.state.config.ENABLE_IMAGE_GENERATION = form_data.enabled
|
||||||
|
|
||||||
|
request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = (
|
||||||
|
form_data.prompt_generation
|
||||||
|
)
|
||||||
|
|
||||||
request.app.state.config.IMAGES_OPENAI_API_BASE_URL = (
|
request.app.state.config.IMAGES_OPENAI_API_BASE_URL = (
|
||||||
form_data.openai.OPENAI_API_BASE_URL
|
form_data.openai.OPENAI_API_BASE_URL
|
||||||
)
|
)
|
||||||
@ -137,6 +143,7 @@ async def update_config(
|
|||||||
return {
|
return {
|
||||||
"enabled": request.app.state.config.ENABLE_IMAGE_GENERATION,
|
"enabled": request.app.state.config.ENABLE_IMAGE_GENERATION,
|
||||||
"engine": request.app.state.config.IMAGE_GENERATION_ENGINE,
|
"engine": request.app.state.config.IMAGE_GENERATION_ENGINE,
|
||||||
|
"prompt_generation": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
|
||||||
"openai": {
|
"openai": {
|
||||||
"OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
|
"OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
|
||||||
"OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
|
"OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
|
||||||
@ -408,10 +415,14 @@ def save_b64_image(b64_str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def save_url_image(url):
|
def save_url_image(url, headers=None):
|
||||||
image_id = str(uuid.uuid4())
|
image_id = str(uuid.uuid4())
|
||||||
try:
|
try:
|
||||||
r = requests.get(url)
|
if headers:
|
||||||
|
r = requests.get(url, headers=headers)
|
||||||
|
else:
|
||||||
|
r = requests.get(url)
|
||||||
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
if r.headers["content-type"].split("/")[0] == "image":
|
if r.headers["content-type"].split("/")[0] == "image":
|
||||||
mime_type = r.headers["content-type"]
|
mime_type = r.headers["content-type"]
|
||||||
@ -535,7 +546,13 @@ async def image_generations(
|
|||||||
images = []
|
images = []
|
||||||
|
|
||||||
for image in res["data"]:
|
for image in res["data"]:
|
||||||
image_filename = save_url_image(image["url"])
|
headers = None
|
||||||
|
if request.app.state.config.COMFYUI_API_KEY:
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}"
|
||||||
|
}
|
||||||
|
|
||||||
|
image_filename = save_url_image(image["url"], headers)
|
||||||
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
||||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ from open_webui.routers.retrieval import (
|
|||||||
process_files_batch,
|
process_files_batch,
|
||||||
BatchProcessFilesForm,
|
BatchProcessFilesForm,
|
||||||
)
|
)
|
||||||
|
from open_webui.storage.provider import Storage
|
||||||
|
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.utils.auth import get_verified_user
|
from open_webui.utils.auth import get_verified_user
|
||||||
@ -213,8 +213,12 @@ async def update_knowledge_by_id(
|
|||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
# Is the user the original creator, in a group with write access, or an admin
|
||||||
if knowledge.user_id != user.id and user.role != "admin":
|
if (
|
||||||
|
knowledge.user_id != user.id
|
||||||
|
and not has_access(user.id, "write", knowledge.access_control)
|
||||||
|
and user.role != "admin"
|
||||||
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
@ -420,6 +424,18 @@ def remove_file_from_knowledge_by_id(
|
|||||||
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
|
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Remove the file's collection from vector database
|
||||||
|
file_collection = f"file-{form_data.file_id}"
|
||||||
|
if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection):
|
||||||
|
VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection)
|
||||||
|
|
||||||
|
# Delete physical file
|
||||||
|
if file.path:
|
||||||
|
Storage.delete_file(file.path)
|
||||||
|
|
||||||
|
# Delete file from database
|
||||||
|
Files.delete_file_by_id(form_data.file_id)
|
||||||
|
|
||||||
if knowledge:
|
if knowledge:
|
||||||
data = knowledge.data or {}
|
data = knowledge.data or {}
|
||||||
file_ids = data.get("file_ids", [])
|
file_ids = data.get("file_ids", [])
|
||||||
|
@ -155,6 +155,16 @@ async def update_model_by_id(
|
|||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
model.user_id != user.id
|
||||||
|
and not has_access(user.id, "write", model.access_control)
|
||||||
|
and user.role != "admin"
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
model = Models.update_model_by_id(id, form_data)
|
model = Models.update_model_by_id(id, form_data)
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
@ -152,10 +152,12 @@ async def send_post_request(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_api_key(url, configs):
|
def get_api_key(idx, url, configs):
|
||||||
parsed_url = urlparse(url)
|
parsed_url = urlparse(url)
|
||||||
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||||
return configs.get(base_url, {}).get("key", None)
|
return configs.get(str(idx), configs.get(base_url, {})).get(
|
||||||
|
"key", None
|
||||||
|
) # Legacy support
|
||||||
|
|
||||||
|
|
||||||
##########################################
|
##########################################
|
||||||
@ -238,11 +240,13 @@ async def update_config(
|
|||||||
request.app.state.config.OLLAMA_BASE_URLS = form_data.OLLAMA_BASE_URLS
|
request.app.state.config.OLLAMA_BASE_URLS = form_data.OLLAMA_BASE_URLS
|
||||||
request.app.state.config.OLLAMA_API_CONFIGS = form_data.OLLAMA_API_CONFIGS
|
request.app.state.config.OLLAMA_API_CONFIGS = form_data.OLLAMA_API_CONFIGS
|
||||||
|
|
||||||
# Remove any extra configs
|
# Remove the API configs that are not in the API URLS
|
||||||
config_urls = request.app.state.config.OLLAMA_API_CONFIGS.keys()
|
keys = list(map(str, range(len(request.app.state.config.OLLAMA_BASE_URLS))))
|
||||||
for url in list(request.app.state.config.OLLAMA_BASE_URLS):
|
request.app.state.config.OLLAMA_API_CONFIGS = {
|
||||||
if url not in config_urls:
|
key: value
|
||||||
request.app.state.config.OLLAMA_API_CONFIGS.pop(url, None)
|
for key, value in request.app.state.config.OLLAMA_API_CONFIGS.items()
|
||||||
|
if key in keys
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ENABLE_OLLAMA_API": request.app.state.config.ENABLE_OLLAMA_API,
|
"ENABLE_OLLAMA_API": request.app.state.config.ENABLE_OLLAMA_API,
|
||||||
@ -256,12 +260,19 @@ async def get_all_models(request: Request):
|
|||||||
log.info("get_all_models()")
|
log.info("get_all_models()")
|
||||||
if request.app.state.config.ENABLE_OLLAMA_API:
|
if request.app.state.config.ENABLE_OLLAMA_API:
|
||||||
request_tasks = []
|
request_tasks = []
|
||||||
|
|
||||||
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS):
|
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS):
|
||||||
if url not in request.app.state.config.OLLAMA_API_CONFIGS:
|
if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and (
|
||||||
|
url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support
|
||||||
|
):
|
||||||
request_tasks.append(send_get_request(f"{url}/api/tags"))
|
request_tasks.append(send_get_request(f"{url}/api/tags"))
|
||||||
else:
|
else:
|
||||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(url, {})
|
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
str(idx),
|
||||||
|
request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
url, {}
|
||||||
|
), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
enable = api_config.get("enable", True)
|
enable = api_config.get("enable", True)
|
||||||
key = api_config.get("key", None)
|
key = api_config.get("key", None)
|
||||||
|
|
||||||
@ -275,7 +286,12 @@ async def get_all_models(request: Request):
|
|||||||
for idx, response in enumerate(responses):
|
for idx, response in enumerate(responses):
|
||||||
if response:
|
if response:
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[idx]
|
||||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(url, {})
|
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
str(idx),
|
||||||
|
request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
url, {}
|
||||||
|
), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
prefix_id = api_config.get("prefix_id", None)
|
prefix_id = api_config.get("prefix_id", None)
|
||||||
model_ids = api_config.get("model_ids", [])
|
model_ids = api_config.get("model_ids", [])
|
||||||
@ -349,7 +365,7 @@ async def get_ollama_tags(
|
|||||||
models = await get_all_models(request)
|
models = await get_all_models(request)
|
||||||
else:
|
else:
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
key = get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS)
|
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
try:
|
try:
|
||||||
@ -393,11 +409,14 @@ async def get_ollama_versions(request: Request, url_idx: Optional[int] = None):
|
|||||||
request_tasks = [
|
request_tasks = [
|
||||||
send_get_request(
|
send_get_request(
|
||||||
f"{url}/api/version",
|
f"{url}/api/version",
|
||||||
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}).get(
|
request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
"key", None
|
str(idx),
|
||||||
),
|
request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
url, {}
|
||||||
|
), # Legacy support
|
||||||
|
).get("key", None),
|
||||||
)
|
)
|
||||||
for url in request.app.state.config.OLLAMA_BASE_URLS
|
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS)
|
||||||
]
|
]
|
||||||
responses = await asyncio.gather(*request_tasks)
|
responses = await asyncio.gather(*request_tasks)
|
||||||
responses = list(filter(lambda x: x is not None, responses))
|
responses = list(filter(lambda x: x is not None, responses))
|
||||||
@ -454,11 +473,14 @@ async def get_ollama_loaded_models(request: Request, user=Depends(get_verified_u
|
|||||||
request_tasks = [
|
request_tasks = [
|
||||||
send_get_request(
|
send_get_request(
|
||||||
f"{url}/api/ps",
|
f"{url}/api/ps",
|
||||||
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}).get(
|
request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
"key", None
|
str(idx),
|
||||||
),
|
request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
url, {}
|
||||||
|
), # Legacy support
|
||||||
|
).get("key", None),
|
||||||
)
|
)
|
||||||
for url in request.app.state.config.OLLAMA_BASE_URLS
|
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS)
|
||||||
]
|
]
|
||||||
responses = await asyncio.gather(*request_tasks)
|
responses = await asyncio.gather(*request_tasks)
|
||||||
|
|
||||||
@ -488,7 +510,7 @@ async def pull_model(
|
|||||||
return await send_post_request(
|
return await send_post_request(
|
||||||
url=f"{url}/api/pull",
|
url=f"{url}/api/pull",
|
||||||
payload=json.dumps(payload),
|
payload=json.dumps(payload),
|
||||||
key=get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -524,16 +546,17 @@ async def push_model(
|
|||||||
return await send_post_request(
|
return await send_post_request(
|
||||||
url=f"{url}/api/push",
|
url=f"{url}/api/push",
|
||||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
key=get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CreateModelForm(BaseModel):
|
class CreateModelForm(BaseModel):
|
||||||
name: str
|
model: Optional[str] = None
|
||||||
modelfile: Optional[str] = None
|
|
||||||
stream: Optional[bool] = None
|
stream: Optional[bool] = None
|
||||||
path: Optional[str] = None
|
path: Optional[str] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/create")
|
@router.post("/api/create")
|
||||||
@router.post("/api/create/{url_idx}")
|
@router.post("/api/create/{url_idx}")
|
||||||
@ -549,7 +572,7 @@ async def create_model(
|
|||||||
return await send_post_request(
|
return await send_post_request(
|
||||||
url=f"{url}/api/create",
|
url=f"{url}/api/create",
|
||||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
key=get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -579,7 +602,7 @@ async def copy_model(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
key = get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS)
|
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
@ -634,7 +657,7 @@ async def delete_model(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
key = get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS)
|
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
@ -684,7 +707,7 @@ async def show_model_info(
|
|||||||
url_idx = random.choice(models[form_data.name]["urls"])
|
url_idx = random.choice(models[form_data.name]["urls"])
|
||||||
|
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
key = get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS)
|
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
@ -753,7 +776,7 @@ async def embed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
key = get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS)
|
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
@ -822,7 +845,7 @@ async def embeddings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
key = get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS)
|
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
@ -897,7 +920,10 @@ async def generate_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(url, {})
|
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
str(url_idx),
|
||||||
|
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
prefix_id = api_config.get("prefix_id", None)
|
prefix_id = api_config.get("prefix_id", None)
|
||||||
if prefix_id:
|
if prefix_id:
|
||||||
@ -906,7 +932,7 @@ async def generate_completion(
|
|||||||
return await send_post_request(
|
return await send_post_request(
|
||||||
url=f"{url}/api/generate",
|
url=f"{url}/api/generate",
|
||||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
key=get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -936,7 +962,7 @@ async def get_ollama_url(request: Request, model: str, url_idx: Optional[int] =
|
|||||||
)
|
)
|
||||||
url_idx = random.choice(models[model].get("urls", []))
|
url_idx = random.choice(models[model].get("urls", []))
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
return url
|
return url, url_idx
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/chat")
|
@router.post("/api/chat")
|
||||||
@ -1004,8 +1030,11 @@ async def generate_chat_completion(
|
|||||||
if ":" not in payload["model"]:
|
if ":" not in payload["model"]:
|
||||||
payload["model"] = f"{payload['model']}:latest"
|
payload["model"] = f"{payload['model']}:latest"
|
||||||
|
|
||||||
url = await get_ollama_url(request, payload["model"], url_idx)
|
url, url_idx = await get_ollama_url(request, payload["model"], url_idx)
|
||||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(url, {})
|
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
str(url_idx),
|
||||||
|
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
prefix_id = api_config.get("prefix_id", None)
|
prefix_id = api_config.get("prefix_id", None)
|
||||||
if prefix_id:
|
if prefix_id:
|
||||||
@ -1015,7 +1044,7 @@ async def generate_chat_completion(
|
|||||||
url=f"{url}/api/chat",
|
url=f"{url}/api/chat",
|
||||||
payload=json.dumps(payload),
|
payload=json.dumps(payload),
|
||||||
stream=form_data.stream,
|
stream=form_data.stream,
|
||||||
key=get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
content_type="application/x-ndjson",
|
content_type="application/x-ndjson",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1103,8 +1132,11 @@ async def generate_openai_completion(
|
|||||||
if ":" not in payload["model"]:
|
if ":" not in payload["model"]:
|
||||||
payload["model"] = f"{payload['model']}:latest"
|
payload["model"] = f"{payload['model']}:latest"
|
||||||
|
|
||||||
url = await get_ollama_url(request, payload["model"], url_idx)
|
url, url_idx = await get_ollama_url(request, payload["model"], url_idx)
|
||||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(url, {})
|
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
str(url_idx),
|
||||||
|
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
prefix_id = api_config.get("prefix_id", None)
|
prefix_id = api_config.get("prefix_id", None)
|
||||||
|
|
||||||
@ -1115,7 +1147,7 @@ async def generate_openai_completion(
|
|||||||
url=f"{url}/v1/completions",
|
url=f"{url}/v1/completions",
|
||||||
payload=json.dumps(payload),
|
payload=json.dumps(payload),
|
||||||
stream=payload.get("stream", False),
|
stream=payload.get("stream", False),
|
||||||
key=get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1177,8 +1209,11 @@ async def generate_openai_chat_completion(
|
|||||||
if ":" not in payload["model"]:
|
if ":" not in payload["model"]:
|
||||||
payload["model"] = f"{payload['model']}:latest"
|
payload["model"] = f"{payload['model']}:latest"
|
||||||
|
|
||||||
url = await get_ollama_url(request, payload["model"], url_idx)
|
url, url_idx = await get_ollama_url(request, payload["model"], url_idx)
|
||||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(url, {})
|
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
|
str(url_idx),
|
||||||
|
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
prefix_id = api_config.get("prefix_id", None)
|
prefix_id = api_config.get("prefix_id", None)
|
||||||
if prefix_id:
|
if prefix_id:
|
||||||
@ -1188,7 +1223,7 @@ async def generate_openai_chat_completion(
|
|||||||
url=f"{url}/v1/chat/completions",
|
url=f"{url}/v1/chat/completions",
|
||||||
payload=json.dumps(payload),
|
payload=json.dumps(payload),
|
||||||
stream=payload.get("stream", False),
|
stream=payload.get("stream", False),
|
||||||
key=get_api_key(url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -145,11 +145,13 @@ async def update_config(
|
|||||||
|
|
||||||
request.app.state.config.OPENAI_API_CONFIGS = form_data.OPENAI_API_CONFIGS
|
request.app.state.config.OPENAI_API_CONFIGS = form_data.OPENAI_API_CONFIGS
|
||||||
|
|
||||||
# Remove any extra configs
|
# Remove the API configs that are not in the API URLS
|
||||||
config_urls = request.app.state.config.OPENAI_API_CONFIGS.keys()
|
keys = list(map(str, range(len(request.app.state.config.OPENAI_API_BASE_URLS))))
|
||||||
for idx, url in enumerate(request.app.state.config.OPENAI_API_BASE_URLS):
|
request.app.state.config.OPENAI_API_CONFIGS = {
|
||||||
if url not in config_urls:
|
key: value
|
||||||
request.app.state.config.OPENAI_API_CONFIGS.pop(url, None)
|
for key, value in request.app.state.config.OPENAI_API_CONFIGS.items()
|
||||||
|
if key in keys
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ENABLE_OPENAI_API": request.app.state.config.ENABLE_OPENAI_API,
|
"ENABLE_OPENAI_API": request.app.state.config.ENABLE_OPENAI_API,
|
||||||
@ -264,14 +266,21 @@ async def get_all_models_responses(request: Request) -> list:
|
|||||||
|
|
||||||
request_tasks = []
|
request_tasks = []
|
||||||
for idx, url in enumerate(request.app.state.config.OPENAI_API_BASE_URLS):
|
for idx, url in enumerate(request.app.state.config.OPENAI_API_BASE_URLS):
|
||||||
if url not in request.app.state.config.OPENAI_API_CONFIGS:
|
if (str(idx) not in request.app.state.config.OPENAI_API_CONFIGS) and (
|
||||||
|
url not in request.app.state.config.OPENAI_API_CONFIGS # Legacy support
|
||||||
|
):
|
||||||
request_tasks.append(
|
request_tasks.append(
|
||||||
send_get_request(
|
send_get_request(
|
||||||
f"{url}/models", request.app.state.config.OPENAI_API_KEYS[idx]
|
f"{url}/models", request.app.state.config.OPENAI_API_KEYS[idx]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(url, {})
|
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||||
|
str(idx),
|
||||||
|
request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||||
|
url, {}
|
||||||
|
), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
enable = api_config.get("enable", True)
|
enable = api_config.get("enable", True)
|
||||||
model_ids = api_config.get("model_ids", [])
|
model_ids = api_config.get("model_ids", [])
|
||||||
@ -310,7 +319,12 @@ async def get_all_models_responses(request: Request) -> list:
|
|||||||
for idx, response in enumerate(responses):
|
for idx, response in enumerate(responses):
|
||||||
if response:
|
if response:
|
||||||
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||||
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(url, {})
|
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||||
|
str(idx),
|
||||||
|
request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||||
|
url, {}
|
||||||
|
), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
prefix_id = api_config.get("prefix_id", None)
|
prefix_id = api_config.get("prefix_id", None)
|
||||||
|
|
||||||
@ -573,6 +587,7 @@ async def generate_chat_completion(
|
|||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await get_all_models(request)
|
||||||
model = request.app.state.OPENAI_MODELS.get(model_id)
|
model = request.app.state.OPENAI_MODELS.get(model_id)
|
||||||
if model:
|
if model:
|
||||||
idx = model["urlIdx"]
|
idx = model["urlIdx"]
|
||||||
@ -584,7 +599,10 @@ async def generate_chat_completion(
|
|||||||
|
|
||||||
# Get the API config for the model
|
# Get the API config for the model
|
||||||
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
|
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||||
request.app.state.config.OPENAI_API_BASE_URLS[idx], {}
|
str(idx),
|
||||||
|
request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||||
|
request.app.state.config.OPENAI_API_BASE_URLS[idx], {}
|
||||||
|
), # Legacy support
|
||||||
)
|
)
|
||||||
|
|
||||||
prefix_id = api_config.get("prefix_id", None)
|
prefix_id = api_config.get("prefix_id", None)
|
||||||
|
@ -112,7 +112,12 @@ async def update_prompt_by_command(
|
|||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
if prompt.user_id != user.id and user.role != "admin":
|
# Is the user the original creator, in a group with write access, or an admin
|
||||||
|
if (
|
||||||
|
prompt.user_id != user.id
|
||||||
|
and not has_access(user.id, "write", prompt.access_control)
|
||||||
|
and user.role != "admin"
|
||||||
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
@ -9,6 +9,7 @@ from open_webui.utils.chat import generate_chat_completion
|
|||||||
from open_webui.utils.task import (
|
from open_webui.utils.task import (
|
||||||
title_generation_template,
|
title_generation_template,
|
||||||
query_generation_template,
|
query_generation_template,
|
||||||
|
image_prompt_generation_template,
|
||||||
autocomplete_generation_template,
|
autocomplete_generation_template,
|
||||||
tags_generation_template,
|
tags_generation_template,
|
||||||
emoji_generation_template,
|
emoji_generation_template,
|
||||||
@ -23,6 +24,7 @@ from open_webui.utils.task import get_task_model_id
|
|||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE,
|
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE,
|
DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
|
||||||
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE,
|
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
|
DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
|
||||||
DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE,
|
DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE,
|
||||||
@ -50,6 +52,7 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)):
|
|||||||
"TASK_MODEL": request.app.state.config.TASK_MODEL,
|
"TASK_MODEL": request.app.state.config.TASK_MODEL,
|
||||||
"TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
|
"TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
"TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
"TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
"IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
|
||||||
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
||||||
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
||||||
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||||
@ -65,6 +68,7 @@ class TaskConfigForm(BaseModel):
|
|||||||
TASK_MODEL: Optional[str]
|
TASK_MODEL: Optional[str]
|
||||||
TASK_MODEL_EXTERNAL: Optional[str]
|
TASK_MODEL_EXTERNAL: Optional[str]
|
||||||
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
||||||
|
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: str
|
||||||
ENABLE_AUTOCOMPLETE_GENERATION: bool
|
ENABLE_AUTOCOMPLETE_GENERATION: bool
|
||||||
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: int
|
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: int
|
||||||
TAGS_GENERATION_PROMPT_TEMPLATE: str
|
TAGS_GENERATION_PROMPT_TEMPLATE: str
|
||||||
@ -114,6 +118,7 @@ async def update_task_config(
|
|||||||
"TASK_MODEL": request.app.state.config.TASK_MODEL,
|
"TASK_MODEL": request.app.state.config.TASK_MODEL,
|
||||||
"TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
|
"TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
"TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
"TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
"IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
|
||||||
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
||||||
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
||||||
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||||
@ -256,6 +261,66 @@ async def generate_chat_tags(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/image_prompt/completions")
|
||||||
|
async def generate_image_prompt(
|
||||||
|
request: Request, form_data: dict, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
|
models = request.app.state.MODELS
|
||||||
|
|
||||||
|
model_id = form_data["model"]
|
||||||
|
if model_id not in models:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Model not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the user has a custom task model
|
||||||
|
# If the user has a custom task model, use that model
|
||||||
|
task_model_id = get_task_model_id(
|
||||||
|
model_id,
|
||||||
|
request.app.state.config.TASK_MODEL,
|
||||||
|
request.app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
models,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
f"generating image prompt using model {task_model_id} for user {user.email} "
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE != "":
|
||||||
|
template = request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
|
||||||
|
else:
|
||||||
|
template = DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
|
||||||
|
|
||||||
|
content = image_prompt_generation_template(
|
||||||
|
template,
|
||||||
|
form_data["messages"],
|
||||||
|
user={
|
||||||
|
"name": user.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": task_model_id,
|
||||||
|
"messages": [{"role": "user", "content": content}],
|
||||||
|
"stream": False,
|
||||||
|
"metadata": {
|
||||||
|
"task": str(TASKS.IMAGE_PROMPT_GENERATION),
|
||||||
|
"task_body": form_data,
|
||||||
|
"chat_id": form_data.get("chat_id", None),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await generate_chat_completion(request, form_data=payload, user=user)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Exception occurred", exc_info=True)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content={"detail": "An internal error has occurred."},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/queries/completions")
|
@router.post("/queries/completions")
|
||||||
async def generate_queries(
|
async def generate_queries(
|
||||||
request: Request, form_data: dict, user=Depends(get_verified_user)
|
request: Request, form_data: dict, user=Depends(get_verified_user)
|
||||||
|
@ -70,7 +70,7 @@ async def create_new_tools(
|
|||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
if user.role != "admin" and not has_permission(
|
if user.role != "admin" and not has_permission(
|
||||||
user.id, "workspace.knowledge", request.app.state.config.USER_PERMISSIONS
|
user.id, "workspace.tools", request.app.state.config.USER_PERMISSIONS
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@ -165,7 +165,12 @@ async def update_tools_by_id(
|
|||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
if tools.user_id != user.id and user.role != "admin":
|
# Is the user the original creator, in a group with write access, or an admin
|
||||||
|
if (
|
||||||
|
tools.user_id != user.id
|
||||||
|
and not has_access(user.id, "write", tools.access_control)
|
||||||
|
and user.role != "admin"
|
||||||
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
@ -304,6 +309,17 @@ async def update_tools_valves_by_id(
|
|||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
tools.user_id != user.id
|
||||||
|
and not has_access(user.id, "write", tools.access_control)
|
||||||
|
and user.role != "admin"
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
if id in request.app.state.TOOLS:
|
if id in request.app.state.TOOLS:
|
||||||
tools_module = request.app.state.TOOLS[id]
|
tools_module = request.app.state.TOOLS[id]
|
||||||
else:
|
else:
|
||||||
|
@ -62,27 +62,44 @@ async def get_user_permissisions(user=Depends(get_verified_user)):
|
|||||||
# User Default Permissions
|
# User Default Permissions
|
||||||
############################
|
############################
|
||||||
class WorkspacePermissions(BaseModel):
|
class WorkspacePermissions(BaseModel):
|
||||||
models: bool
|
models: bool = False
|
||||||
knowledge: bool
|
knowledge: bool = False
|
||||||
prompts: bool
|
prompts: bool = False
|
||||||
tools: bool
|
tools: bool = False
|
||||||
|
|
||||||
|
|
||||||
class ChatPermissions(BaseModel):
|
class ChatPermissions(BaseModel):
|
||||||
file_upload: bool
|
controls: bool = True
|
||||||
delete: bool
|
file_upload: bool = True
|
||||||
edit: bool
|
delete: bool = True
|
||||||
temporary: bool
|
edit: bool = True
|
||||||
|
temporary: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturesPermissions(BaseModel):
|
||||||
|
web_search: bool = True
|
||||||
|
image_generation: bool = True
|
||||||
|
|
||||||
|
|
||||||
class UserPermissions(BaseModel):
|
class UserPermissions(BaseModel):
|
||||||
workspace: WorkspacePermissions
|
workspace: WorkspacePermissions
|
||||||
chat: ChatPermissions
|
chat: ChatPermissions
|
||||||
|
features: FeaturesPermissions
|
||||||
|
|
||||||
|
|
||||||
@router.get("/default/permissions")
|
@router.get("/default/permissions", response_model=UserPermissions)
|
||||||
async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
|
async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
|
||||||
return request.app.state.config.USER_PERMISSIONS
|
return {
|
||||||
|
"workspace": WorkspacePermissions(
|
||||||
|
**request.app.state.config.USER_PERMISSIONS.get("workspace", {})
|
||||||
|
),
|
||||||
|
"chat": ChatPermissions(
|
||||||
|
**request.app.state.config.USER_PERMISSIONS.get("chat", {})
|
||||||
|
),
|
||||||
|
"features": FeaturesPermissions(
|
||||||
|
**request.app.state.config.USER_PERMISSIONS.get("features", {})
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/default/permissions")
|
@router.post("/default/permissions")
|
||||||
|
@ -26,7 +26,7 @@ class RedisLock:
|
|||||||
|
|
||||||
def release_lock(self):
|
def release_lock(self):
|
||||||
lock_value = self.redis.get(self.lock_name)
|
lock_value = self.redis.get(self.lock_name)
|
||||||
if lock_value and lock_value.decode("utf-8") == self.lock_id:
|
if lock_value and lock_value == self.lock_id:
|
||||||
self.redis.delete(self.lock_name)
|
self.redis.delete(self.lock_name)
|
||||||
|
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.2 KiB |
@ -1,121 +1,73 @@
|
|||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import BinaryIO, Tuple
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
import shutil
|
|
||||||
|
|
||||||
|
|
||||||
from typing import BinaryIO, Tuple, Optional, Union
|
|
||||||
|
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
STORAGE_PROVIDER,
|
|
||||||
S3_ACCESS_KEY_ID,
|
S3_ACCESS_KEY_ID,
|
||||||
S3_SECRET_ACCESS_KEY,
|
|
||||||
S3_BUCKET_NAME,
|
S3_BUCKET_NAME,
|
||||||
S3_REGION_NAME,
|
|
||||||
S3_ENDPOINT_URL,
|
S3_ENDPOINT_URL,
|
||||||
|
S3_REGION_NAME,
|
||||||
|
S3_SECRET_ACCESS_KEY,
|
||||||
|
GCS_BUCKET_NAME,
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS_JSON,
|
||||||
|
STORAGE_PROVIDER,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
)
|
)
|
||||||
|
from google.cloud import storage
|
||||||
|
from google.cloud.exceptions import GoogleCloudError, NotFound
|
||||||
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
|
||||||
import boto3
|
class StorageProvider(ABC):
|
||||||
from botocore.exceptions import ClientError
|
@abstractmethod
|
||||||
from typing import BinaryIO, Tuple, Optional
|
def get_file(self, file_path: str) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_all_files(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_file(self, file_path: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class StorageProvider:
|
class LocalStorageProvider(StorageProvider):
|
||||||
def __init__(self, provider: Optional[str] = None):
|
@staticmethod
|
||||||
self.storage_provider: str = provider or STORAGE_PROVIDER
|
def upload_file(file: BinaryIO, filename: str) -> Tuple[bytes, str]:
|
||||||
|
contents = file.read()
|
||||||
self.s3_client = None
|
if not contents:
|
||||||
self.s3_bucket_name: Optional[str] = None
|
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
|
||||||
|
|
||||||
if self.storage_provider == "s3":
|
|
||||||
self._initialize_s3()
|
|
||||||
|
|
||||||
def _initialize_s3(self) -> None:
|
|
||||||
"""Initializes the S3 client and bucket name if using S3 storage."""
|
|
||||||
self.s3_client = boto3.client(
|
|
||||||
"s3",
|
|
||||||
region_name=S3_REGION_NAME,
|
|
||||||
endpoint_url=S3_ENDPOINT_URL,
|
|
||||||
aws_access_key_id=S3_ACCESS_KEY_ID,
|
|
||||||
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
|
|
||||||
)
|
|
||||||
self.bucket_name = S3_BUCKET_NAME
|
|
||||||
|
|
||||||
def _upload_to_s3(self, file_path: str, filename: str) -> Tuple[bytes, str]:
|
|
||||||
"""Handles uploading of the file to S3 storage."""
|
|
||||||
if not self.s3_client:
|
|
||||||
raise RuntimeError("S3 Client is not initialized.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.s3_client.upload_file(file_path, self.bucket_name, filename)
|
|
||||||
return (
|
|
||||||
open(file_path, "rb").read(),
|
|
||||||
"s3://" + self.bucket_name + "/" + filename,
|
|
||||||
)
|
|
||||||
except ClientError as e:
|
|
||||||
raise RuntimeError(f"Error uploading file to S3: {e}")
|
|
||||||
|
|
||||||
def _upload_to_local(self, contents: bytes, filename: str) -> Tuple[bytes, str]:
|
|
||||||
"""Handles uploading of the file to local storage."""
|
|
||||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
file_path = f"{UPLOAD_DIR}/{filename}"
|
||||||
with open(file_path, "wb") as f:
|
with open(file_path, "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
return contents, file_path
|
return contents, file_path
|
||||||
|
|
||||||
def _get_file_from_s3(self, file_path: str) -> str:
|
@staticmethod
|
||||||
"""Handles downloading of the file from S3 storage."""
|
def get_file(file_path: str) -> str:
|
||||||
if not self.s3_client:
|
|
||||||
raise RuntimeError("S3 Client is not initialized.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
bucket_name, key = file_path.split("//")[1].split("/")
|
|
||||||
local_file_path = f"{UPLOAD_DIR}/{key}"
|
|
||||||
self.s3_client.download_file(bucket_name, key, local_file_path)
|
|
||||||
return local_file_path
|
|
||||||
except ClientError as e:
|
|
||||||
raise RuntimeError(f"Error downloading file from S3: {e}")
|
|
||||||
|
|
||||||
def _get_file_from_local(self, file_path: str) -> str:
|
|
||||||
"""Handles downloading of the file from local storage."""
|
"""Handles downloading of the file from local storage."""
|
||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
def _delete_from_s3(self, filename: str) -> None:
|
@staticmethod
|
||||||
"""Handles deletion of the file from S3 storage."""
|
def delete_file(file_path: str) -> None:
|
||||||
if not self.s3_client:
|
|
||||||
raise RuntimeError("S3 Client is not initialized.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.s3_client.delete_object(Bucket=self.bucket_name, Key=filename)
|
|
||||||
except ClientError as e:
|
|
||||||
raise RuntimeError(f"Error deleting file from S3: {e}")
|
|
||||||
|
|
||||||
def _delete_from_local(self, filename: str) -> None:
|
|
||||||
"""Handles deletion of the file from local storage."""
|
"""Handles deletion of the file from local storage."""
|
||||||
|
filename = file_path.split("/")[-1]
|
||||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
file_path = f"{UPLOAD_DIR}/{filename}"
|
||||||
if os.path.isfile(file_path):
|
if os.path.isfile(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
else:
|
else:
|
||||||
print(f"File {file_path} not found in local storage.")
|
print(f"File {file_path} not found in local storage.")
|
||||||
|
|
||||||
def _delete_all_from_s3(self) -> None:
|
@staticmethod
|
||||||
"""Handles deletion of all files from S3 storage."""
|
def delete_all_files() -> None:
|
||||||
if not self.s3_client:
|
|
||||||
raise RuntimeError("S3 Client is not initialized.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self.s3_client.list_objects_v2(Bucket=self.bucket_name)
|
|
||||||
if "Contents" in response:
|
|
||||||
for content in response["Contents"]:
|
|
||||||
self.s3_client.delete_object(
|
|
||||||
Bucket=self.bucket_name, Key=content["Key"]
|
|
||||||
)
|
|
||||||
except ClientError as e:
|
|
||||||
raise RuntimeError(f"Error deleting all files from S3: {e}")
|
|
||||||
|
|
||||||
def _delete_all_from_local(self) -> None:
|
|
||||||
"""Handles deletion of all files from local storage."""
|
"""Handles deletion of all files from local storage."""
|
||||||
if os.path.exists(UPLOAD_DIR):
|
if os.path.exists(UPLOAD_DIR):
|
||||||
for filename in os.listdir(UPLOAD_DIR):
|
for filename in os.listdir(UPLOAD_DIR):
|
||||||
@ -130,40 +82,141 @@ class StorageProvider:
|
|||||||
else:
|
else:
|
||||||
print(f"Directory {UPLOAD_DIR} not found in local storage.")
|
print(f"Directory {UPLOAD_DIR} not found in local storage.")
|
||||||
|
|
||||||
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
|
|
||||||
"""Uploads a file either to S3 or the local file system."""
|
|
||||||
contents = file.read()
|
|
||||||
if not contents:
|
|
||||||
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
|
|
||||||
contents, file_path = self._upload_to_local(contents, filename)
|
|
||||||
|
|
||||||
if self.storage_provider == "s3":
|
class S3StorageProvider(StorageProvider):
|
||||||
return self._upload_to_s3(file_path, filename)
|
def __init__(self):
|
||||||
return contents, file_path
|
self.s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name=S3_REGION_NAME,
|
||||||
|
endpoint_url=S3_ENDPOINT_URL,
|
||||||
|
aws_access_key_id=S3_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
|
||||||
|
)
|
||||||
|
self.bucket_name = S3_BUCKET_NAME
|
||||||
|
|
||||||
|
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
|
||||||
|
"""Handles uploading of the file to S3 storage."""
|
||||||
|
_, file_path = LocalStorageProvider.upload_file(file, filename)
|
||||||
|
try:
|
||||||
|
self.s3_client.upload_file(file_path, self.bucket_name, filename)
|
||||||
|
return (
|
||||||
|
open(file_path, "rb").read(),
|
||||||
|
"s3://" + self.bucket_name + "/" + filename,
|
||||||
|
)
|
||||||
|
except ClientError as e:
|
||||||
|
raise RuntimeError(f"Error uploading file to S3: {e}")
|
||||||
|
|
||||||
def get_file(self, file_path: str) -> str:
|
def get_file(self, file_path: str) -> str:
|
||||||
"""Downloads a file either from S3 or the local file system and returns the file path."""
|
"""Handles downloading of the file from S3 storage."""
|
||||||
if self.storage_provider == "s3":
|
try:
|
||||||
return self._get_file_from_s3(file_path)
|
bucket_name, key = file_path.split("//")[1].split("/")
|
||||||
return self._get_file_from_local(file_path)
|
local_file_path = f"{UPLOAD_DIR}/{key}"
|
||||||
|
self.s3_client.download_file(bucket_name, key, local_file_path)
|
||||||
|
return local_file_path
|
||||||
|
except ClientError as e:
|
||||||
|
raise RuntimeError(f"Error downloading file from S3: {e}")
|
||||||
|
|
||||||
def delete_file(self, file_path: str) -> None:
|
def delete_file(self, file_path: str) -> None:
|
||||||
"""Deletes a file either from S3 or the local file system."""
|
"""Handles deletion of the file from S3 storage."""
|
||||||
filename = file_path.split("/")[-1]
|
filename = file_path.split("/")[-1]
|
||||||
|
try:
|
||||||
if self.storage_provider == "s3":
|
self.s3_client.delete_object(Bucket=self.bucket_name, Key=filename)
|
||||||
self._delete_from_s3(filename)
|
except ClientError as e:
|
||||||
|
raise RuntimeError(f"Error deleting file from S3: {e}")
|
||||||
|
|
||||||
# Always delete from local storage
|
# Always delete from local storage
|
||||||
self._delete_from_local(filename)
|
LocalStorageProvider.delete_file(file_path)
|
||||||
|
|
||||||
def delete_all_files(self) -> None:
|
def delete_all_files(self) -> None:
|
||||||
"""Deletes all files from the storage."""
|
"""Handles deletion of all files from S3 storage."""
|
||||||
if self.storage_provider == "s3":
|
try:
|
||||||
self._delete_all_from_s3()
|
response = self.s3_client.list_objects_v2(Bucket=self.bucket_name)
|
||||||
|
if "Contents" in response:
|
||||||
|
for content in response["Contents"]:
|
||||||
|
self.s3_client.delete_object(
|
||||||
|
Bucket=self.bucket_name, Key=content["Key"]
|
||||||
|
)
|
||||||
|
except ClientError as e:
|
||||||
|
raise RuntimeError(f"Error deleting all files from S3: {e}")
|
||||||
|
|
||||||
# Always delete from local storage
|
# Always delete from local storage
|
||||||
self._delete_all_from_local()
|
LocalStorageProvider.delete_all_files()
|
||||||
|
|
||||||
|
|
||||||
Storage = StorageProvider(provider=STORAGE_PROVIDER)
|
class GCSStorageProvider(StorageProvider):
|
||||||
|
def __init__(self):
|
||||||
|
self.bucket_name = GCS_BUCKET_NAME
|
||||||
|
|
||||||
|
if GOOGLE_APPLICATION_CREDENTIALS_JSON:
|
||||||
|
self.gcs_client = storage.Client.from_service_account_info(
|
||||||
|
info=json.loads(GOOGLE_APPLICATION_CREDENTIALS_JSON)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# if no credentials json is provided, credentials will be picked up from the environment
|
||||||
|
# if running on local environment, credentials would be user credentials
|
||||||
|
# if running on a Compute Engine instance, credentials would be from Google Metadata server
|
||||||
|
self.gcs_client = storage.Client()
|
||||||
|
self.bucket = self.gcs_client.bucket(GCS_BUCKET_NAME)
|
||||||
|
|
||||||
|
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
|
||||||
|
"""Handles uploading of the file to GCS storage."""
|
||||||
|
contents, file_path = LocalStorageProvider.upload_file(file, filename)
|
||||||
|
try:
|
||||||
|
blob = self.bucket.blob(filename)
|
||||||
|
blob.upload_from_filename(file_path)
|
||||||
|
return contents, "gs://" + self.bucket_name + "/" + filename
|
||||||
|
except GoogleCloudError as e:
|
||||||
|
raise RuntimeError(f"Error uploading file to GCS: {e}")
|
||||||
|
|
||||||
|
def get_file(self, file_path: str) -> str:
|
||||||
|
"""Handles downloading of the file from GCS storage."""
|
||||||
|
try:
|
||||||
|
filename = file_path.removeprefix("gs://").split("/")[1]
|
||||||
|
local_file_path = f"{UPLOAD_DIR}/{filename}"
|
||||||
|
blob = self.bucket.get_blob(filename)
|
||||||
|
blob.download_to_filename(local_file_path)
|
||||||
|
|
||||||
|
return local_file_path
|
||||||
|
except NotFound as e:
|
||||||
|
raise RuntimeError(f"Error downloading file from GCS: {e}")
|
||||||
|
|
||||||
|
def delete_file(self, file_path: str) -> None:
|
||||||
|
"""Handles deletion of the file from GCS storage."""
|
||||||
|
try:
|
||||||
|
filename = file_path.removeprefix("gs://").split("/")[1]
|
||||||
|
blob = self.bucket.get_blob(filename)
|
||||||
|
blob.delete()
|
||||||
|
except NotFound as e:
|
||||||
|
raise RuntimeError(f"Error deleting file from GCS: {e}")
|
||||||
|
|
||||||
|
# Always delete from local storage
|
||||||
|
LocalStorageProvider.delete_file(file_path)
|
||||||
|
|
||||||
|
def delete_all_files(self) -> None:
|
||||||
|
"""Handles deletion of all files from GCS storage."""
|
||||||
|
try:
|
||||||
|
blobs = self.bucket.list_blobs()
|
||||||
|
|
||||||
|
for blob in blobs:
|
||||||
|
blob.delete()
|
||||||
|
|
||||||
|
except NotFound as e:
|
||||||
|
raise RuntimeError(f"Error deleting all files from GCS: {e}")
|
||||||
|
|
||||||
|
# Always delete from local storage
|
||||||
|
LocalStorageProvider.delete_all_files()
|
||||||
|
|
||||||
|
|
||||||
|
def get_storage_provider(storage_provider: str):
|
||||||
|
if storage_provider == "local":
|
||||||
|
Storage = LocalStorageProvider()
|
||||||
|
elif storage_provider == "s3":
|
||||||
|
Storage = S3StorageProvider()
|
||||||
|
elif storage_provider == "gcs":
|
||||||
|
Storage = GCSStorageProvider()
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unsupported storage provider: {storage_provider}")
|
||||||
|
return Storage
|
||||||
|
|
||||||
|
|
||||||
|
Storage = get_storage_provider(STORAGE_PROVIDER)
|
||||||
|
274
backend/open_webui/test/apps/webui/storage/test_provider.py
Normal file
274
backend/open_webui/test/apps/webui/storage/test_provider.py
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import boto3
|
||||||
|
import pytest
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
from moto import mock_aws
|
||||||
|
from open_webui.storage import provider
|
||||||
|
from gcp_storage_emulator.server import create_server
|
||||||
|
from google.cloud import storage
|
||||||
|
|
||||||
|
|
||||||
|
def mock_upload_dir(monkeypatch, tmp_path):
|
||||||
|
"""Fixture to monkey-patch the UPLOAD_DIR and create a temporary directory."""
|
||||||
|
directory = tmp_path / "uploads"
|
||||||
|
directory.mkdir()
|
||||||
|
monkeypatch.setattr(provider, "UPLOAD_DIR", str(directory))
|
||||||
|
return directory
|
||||||
|
|
||||||
|
|
||||||
|
def test_imports():
|
||||||
|
provider.StorageProvider
|
||||||
|
provider.LocalStorageProvider
|
||||||
|
provider.S3StorageProvider
|
||||||
|
provider.GCSStorageProvider
|
||||||
|
provider.Storage
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_storage_provider():
|
||||||
|
Storage = provider.get_storage_provider("local")
|
||||||
|
assert isinstance(Storage, provider.LocalStorageProvider)
|
||||||
|
Storage = provider.get_storage_provider("s3")
|
||||||
|
assert isinstance(Storage, provider.S3StorageProvider)
|
||||||
|
Storage = provider.get_storage_provider("gcs")
|
||||||
|
assert isinstance(Storage, provider.GCSStorageProvider)
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
provider.get_storage_provider("invalid")
|
||||||
|
|
||||||
|
|
||||||
|
def test_class_instantiation():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
provider.StorageProvider()
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
|
||||||
|
class Test(provider.StorageProvider):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Test()
|
||||||
|
provider.LocalStorageProvider()
|
||||||
|
provider.S3StorageProvider()
|
||||||
|
provider.GCSStorageProvider()
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocalStorageProvider:
|
||||||
|
Storage = provider.LocalStorageProvider()
|
||||||
|
file_content = b"test content"
|
||||||
|
file_bytesio = io.BytesIO(file_content)
|
||||||
|
filename = "test.txt"
|
||||||
|
filename_extra = "test_exyta.txt"
|
||||||
|
file_bytesio_empty = io.BytesIO()
|
||||||
|
|
||||||
|
def test_upload_file(self, monkeypatch, tmp_path):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
contents, file_path = self.Storage.upload_file(self.file_bytesio, self.filename)
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
assert (upload_dir / self.filename).read_bytes() == self.file_content
|
||||||
|
assert contents == self.file_content
|
||||||
|
assert file_path == str(upload_dir / self.filename)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
self.Storage.upload_file(self.file_bytesio_empty, self.filename)
|
||||||
|
|
||||||
|
def test_get_file(self, monkeypatch, tmp_path):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
file_path = str(upload_dir / self.filename)
|
||||||
|
file_path_return = self.Storage.get_file(file_path)
|
||||||
|
assert file_path == file_path_return
|
||||||
|
|
||||||
|
def test_delete_file(self, monkeypatch, tmp_path):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
(upload_dir / self.filename).write_bytes(self.file_content)
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
file_path = str(upload_dir / self.filename)
|
||||||
|
self.Storage.delete_file(file_path)
|
||||||
|
assert not (upload_dir / self.filename).exists()
|
||||||
|
|
||||||
|
def test_delete_all_files(self, monkeypatch, tmp_path):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
(upload_dir / self.filename).write_bytes(self.file_content)
|
||||||
|
(upload_dir / self.filename_extra).write_bytes(self.file_content)
|
||||||
|
self.Storage.delete_all_files()
|
||||||
|
assert not (upload_dir / self.filename).exists()
|
||||||
|
assert not (upload_dir / self.filename_extra).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@mock_aws
|
||||||
|
class TestS3StorageProvider:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.Storage = provider.S3StorageProvider()
|
||||||
|
self.Storage.bucket_name = "my-bucket"
|
||||||
|
self.s3_client = boto3.resource("s3", region_name="us-east-1")
|
||||||
|
self.file_content = b"test content"
|
||||||
|
self.filename = "test.txt"
|
||||||
|
self.filename_extra = "test_exyta.txt"
|
||||||
|
self.file_bytesio_empty = io.BytesIO()
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def test_upload_file(self, monkeypatch, tmp_path):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
# S3 checks
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
|
||||||
|
self.s3_client.create_bucket(Bucket=self.Storage.bucket_name)
|
||||||
|
contents, s3_file_path = self.Storage.upload_file(
|
||||||
|
io.BytesIO(self.file_content), self.filename
|
||||||
|
)
|
||||||
|
object = self.s3_client.Object(self.Storage.bucket_name, self.filename)
|
||||||
|
assert self.file_content == object.get()["Body"].read()
|
||||||
|
# local checks
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
assert (upload_dir / self.filename).read_bytes() == self.file_content
|
||||||
|
assert contents == self.file_content
|
||||||
|
assert s3_file_path == "s3://" + self.Storage.bucket_name + "/" + self.filename
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
self.Storage.upload_file(self.file_bytesio_empty, self.filename)
|
||||||
|
|
||||||
|
def test_get_file(self, monkeypatch, tmp_path):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
self.s3_client.create_bucket(Bucket=self.Storage.bucket_name)
|
||||||
|
contents, s3_file_path = self.Storage.upload_file(
|
||||||
|
io.BytesIO(self.file_content), self.filename
|
||||||
|
)
|
||||||
|
file_path = self.Storage.get_file(s3_file_path)
|
||||||
|
assert file_path == str(upload_dir / self.filename)
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
|
||||||
|
def test_delete_file(self, monkeypatch, tmp_path):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
self.s3_client.create_bucket(Bucket=self.Storage.bucket_name)
|
||||||
|
contents, s3_file_path = self.Storage.upload_file(
|
||||||
|
io.BytesIO(self.file_content), self.filename
|
||||||
|
)
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
self.Storage.delete_file(s3_file_path)
|
||||||
|
assert not (upload_dir / self.filename).exists()
|
||||||
|
with pytest.raises(ClientError) as exc:
|
||||||
|
self.s3_client.Object(self.Storage.bucket_name, self.filename).load()
|
||||||
|
error = exc.value.response["Error"]
|
||||||
|
assert error["Code"] == "404"
|
||||||
|
assert error["Message"] == "Not Found"
|
||||||
|
|
||||||
|
def test_delete_all_files(self, monkeypatch, tmp_path):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
# create 2 files
|
||||||
|
self.s3_client.create_bucket(Bucket=self.Storage.bucket_name)
|
||||||
|
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
|
||||||
|
object = self.s3_client.Object(self.Storage.bucket_name, self.filename)
|
||||||
|
assert self.file_content == object.get()["Body"].read()
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
assert (upload_dir / self.filename).read_bytes() == self.file_content
|
||||||
|
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename_extra)
|
||||||
|
object = self.s3_client.Object(self.Storage.bucket_name, self.filename_extra)
|
||||||
|
assert self.file_content == object.get()["Body"].read()
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
assert (upload_dir / self.filename).read_bytes() == self.file_content
|
||||||
|
|
||||||
|
self.Storage.delete_all_files()
|
||||||
|
assert not (upload_dir / self.filename).exists()
|
||||||
|
with pytest.raises(ClientError) as exc:
|
||||||
|
self.s3_client.Object(self.Storage.bucket_name, self.filename).load()
|
||||||
|
error = exc.value.response["Error"]
|
||||||
|
assert error["Code"] == "404"
|
||||||
|
assert error["Message"] == "Not Found"
|
||||||
|
assert not (upload_dir / self.filename_extra).exists()
|
||||||
|
with pytest.raises(ClientError) as exc:
|
||||||
|
self.s3_client.Object(self.Storage.bucket_name, self.filename_extra).load()
|
||||||
|
error = exc.value.response["Error"]
|
||||||
|
assert error["Code"] == "404"
|
||||||
|
assert error["Message"] == "Not Found"
|
||||||
|
|
||||||
|
self.Storage.delete_all_files()
|
||||||
|
assert not (upload_dir / self.filename).exists()
|
||||||
|
assert not (upload_dir / self.filename_extra).exists()
|
||||||
|
|
||||||
|
|
||||||
|
class TestGCSStorageProvider:
|
||||||
|
Storage = provider.GCSStorageProvider()
|
||||||
|
Storage.bucket_name = "my-bucket"
|
||||||
|
file_content = b"test content"
|
||||||
|
filename = "test.txt"
|
||||||
|
filename_extra = "test_exyta.txt"
|
||||||
|
file_bytesio_empty = io.BytesIO()
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def setup(self):
|
||||||
|
host, port = "localhost", 9023
|
||||||
|
|
||||||
|
server = create_server(host, port, in_memory=True)
|
||||||
|
server.start()
|
||||||
|
os.environ["STORAGE_EMULATOR_HOST"] = f"http://{host}:{port}"
|
||||||
|
|
||||||
|
gcs_client = storage.Client()
|
||||||
|
bucket = gcs_client.bucket(self.Storage.bucket_name)
|
||||||
|
bucket.create()
|
||||||
|
self.Storage.gcs_client, self.Storage.bucket = gcs_client, bucket
|
||||||
|
yield
|
||||||
|
bucket.delete(force=True)
|
||||||
|
server.stop()
|
||||||
|
|
||||||
|
def test_upload_file(self, monkeypatch, tmp_path, setup):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
# catch error if bucket does not exist
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
self.Storage.bucket = monkeypatch(self.Storage, "bucket", None)
|
||||||
|
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
|
||||||
|
contents, gcs_file_path = self.Storage.upload_file(
|
||||||
|
io.BytesIO(self.file_content), self.filename
|
||||||
|
)
|
||||||
|
object = self.Storage.bucket.get_blob(self.filename)
|
||||||
|
assert self.file_content == object.download_as_bytes()
|
||||||
|
# local checks
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
assert (upload_dir / self.filename).read_bytes() == self.file_content
|
||||||
|
assert contents == self.file_content
|
||||||
|
assert gcs_file_path == "gs://" + self.Storage.bucket_name + "/" + self.filename
|
||||||
|
# test error if file is empty
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
self.Storage.upload_file(self.file_bytesio_empty, self.filename)
|
||||||
|
|
||||||
|
def test_get_file(self, monkeypatch, tmp_path, setup):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
contents, gcs_file_path = self.Storage.upload_file(
|
||||||
|
io.BytesIO(self.file_content), self.filename
|
||||||
|
)
|
||||||
|
file_path = self.Storage.get_file(gcs_file_path)
|
||||||
|
assert file_path == str(upload_dir / self.filename)
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
|
||||||
|
def test_delete_file(self, monkeypatch, tmp_path, setup):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
contents, gcs_file_path = self.Storage.upload_file(
|
||||||
|
io.BytesIO(self.file_content), self.filename
|
||||||
|
)
|
||||||
|
# ensure that local directory has the uploaded file as well
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
assert self.Storage.bucket.get_blob(self.filename).name == self.filename
|
||||||
|
self.Storage.delete_file(gcs_file_path)
|
||||||
|
# check that deleting file from gcs will delete the local file as well
|
||||||
|
assert not (upload_dir / self.filename).exists()
|
||||||
|
assert self.Storage.bucket.get_blob(self.filename) == None
|
||||||
|
|
||||||
|
def test_delete_all_files(self, monkeypatch, tmp_path, setup):
|
||||||
|
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
|
||||||
|
# create 2 files
|
||||||
|
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
|
||||||
|
object = self.Storage.bucket.get_blob(self.filename)
|
||||||
|
assert (upload_dir / self.filename).exists()
|
||||||
|
assert (upload_dir / self.filename).read_bytes() == self.file_content
|
||||||
|
assert self.Storage.bucket.get_blob(self.filename).name == self.filename
|
||||||
|
assert self.file_content == object.download_as_bytes()
|
||||||
|
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename_extra)
|
||||||
|
object = self.Storage.bucket.get_blob(self.filename_extra)
|
||||||
|
assert (upload_dir / self.filename_extra).exists()
|
||||||
|
assert (upload_dir / self.filename_extra).read_bytes() == self.file_content
|
||||||
|
assert (
|
||||||
|
self.Storage.bucket.get_blob(self.filename_extra).name
|
||||||
|
== self.filename_extra
|
||||||
|
)
|
||||||
|
assert self.file_content == object.download_as_bytes()
|
||||||
|
|
||||||
|
self.Storage.delete_all_files()
|
||||||
|
assert not (upload_dir / self.filename).exists()
|
||||||
|
assert not (upload_dir / self.filename_extra).exists()
|
||||||
|
assert self.Storage.bucket.get_blob(self.filename) == None
|
||||||
|
assert self.Storage.bucket.get_blob(self.filename_extra) == None
|
@ -1,9 +1,30 @@
|
|||||||
from typing import Optional, Union, List, Dict, Any
|
from typing import Optional, Union, List, Dict, Any
|
||||||
from open_webui.models.users import Users, UserModel
|
from open_webui.models.users import Users, UserModel
|
||||||
from open_webui.models.groups import Groups
|
from open_webui.models.groups import Groups
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.config import DEFAULT_USER_PERMISSIONS
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def fill_missing_permissions(
|
||||||
|
permissions: Dict[str, Any], default_permissions: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Recursively fills in missing properties in the permissions dictionary
|
||||||
|
using the default permissions as a template.
|
||||||
|
"""
|
||||||
|
for key, value in default_permissions.items():
|
||||||
|
if key not in permissions:
|
||||||
|
permissions[key] = value
|
||||||
|
elif isinstance(value, dict) and isinstance(
|
||||||
|
permissions[key], dict
|
||||||
|
): # Both are nested dictionaries
|
||||||
|
permissions[key] = fill_missing_permissions(permissions[key], value)
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
|
||||||
def get_permissions(
|
def get_permissions(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
default_permissions: Dict[str, Any],
|
default_permissions: Dict[str, Any],
|
||||||
@ -27,39 +48,45 @@ def get_permissions(
|
|||||||
if key not in permissions:
|
if key not in permissions:
|
||||||
permissions[key] = value
|
permissions[key] = value
|
||||||
else:
|
else:
|
||||||
permissions[key] = permissions[key] or value
|
permissions[key] = (
|
||||||
|
permissions[key] or value
|
||||||
|
) # Use the most permissive value (True > False)
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
user_groups = Groups.get_groups_by_member_id(user_id)
|
user_groups = Groups.get_groups_by_member_id(user_id)
|
||||||
|
|
||||||
# deep copy default permissions to avoid modifying the original dict
|
# Deep copy default permissions to avoid modifying the original dict
|
||||||
permissions = json.loads(json.dumps(default_permissions))
|
permissions = json.loads(json.dumps(default_permissions))
|
||||||
|
|
||||||
|
# Combine permissions from all user groups
|
||||||
for group in user_groups:
|
for group in user_groups:
|
||||||
group_permissions = group.permissions
|
group_permissions = group.permissions
|
||||||
permissions = combine_permissions(permissions, group_permissions)
|
permissions = combine_permissions(permissions, group_permissions)
|
||||||
|
|
||||||
|
# Ensure all fields from default_permissions are present and filled in
|
||||||
|
permissions = fill_missing_permissions(permissions, default_permissions)
|
||||||
|
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
|
|
||||||
def has_permission(
|
def has_permission(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
permission_key: str,
|
permission_key: str,
|
||||||
default_permissions: Dict[str, bool] = {},
|
default_permissions: Dict[str, Any] = {},
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if a user has a specific permission by checking the group permissions
|
Check if a user has a specific permission by checking the group permissions
|
||||||
and falls back to default permissions if not found in any group.
|
and fall back to default permissions if not found in any group.
|
||||||
|
|
||||||
Permission keys can be hierarchical and separated by dots ('.').
|
Permission keys can be hierarchical and separated by dots ('.').
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_permission(permissions: Dict[str, bool], keys: List[str]) -> bool:
|
def get_permission(permissions: Dict[str, Any], keys: List[str]) -> bool:
|
||||||
"""Traverse permissions dict using a list of keys (from dot-split permission_key)."""
|
"""Traverse permissions dict using a list of keys (from dot-split permission_key)."""
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if key not in permissions:
|
if key not in permissions:
|
||||||
return False # If any part of the hierarchy is missing, deny access
|
return False # If any part of the hierarchy is missing, deny access
|
||||||
permissions = permissions[key] # Go one level deeper
|
permissions = permissions[key] # Traverse one level deeper
|
||||||
|
|
||||||
return bool(permissions) # Return the boolean at the final level
|
return bool(permissions) # Return the boolean at the final level
|
||||||
|
|
||||||
@ -73,7 +100,10 @@ def has_permission(
|
|||||||
if get_permission(group_permissions, permission_hierarchy):
|
if get_permission(group_permissions, permission_hierarchy):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check default permissions afterwards if the group permissions don't allow it
|
# Check default permissions afterward if the group permissions don't allow it
|
||||||
|
default_permissions = fill_missing_permissions(
|
||||||
|
default_permissions, DEFAULT_USER_PERMISSIONS
|
||||||
|
)
|
||||||
return get_permission(default_permissions, permission_hierarchy)
|
return get_permission(default_permissions, permission_hierarchy)
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,9 +28,13 @@ from open_webui.socket.main import (
|
|||||||
from open_webui.routers.tasks import (
|
from open_webui.routers.tasks import (
|
||||||
generate_queries,
|
generate_queries,
|
||||||
generate_title,
|
generate_title,
|
||||||
|
generate_image_prompt,
|
||||||
generate_chat_tags,
|
generate_chat_tags,
|
||||||
)
|
)
|
||||||
from open_webui.routers.retrieval import process_web_search, SearchForm
|
from open_webui.routers.retrieval import process_web_search, SearchForm
|
||||||
|
from open_webui.routers.images import image_generations, GenerateImageForm
|
||||||
|
|
||||||
|
|
||||||
from open_webui.utils.webhook import post_webhook
|
from open_webui.utils.webhook import post_webhook
|
||||||
|
|
||||||
|
|
||||||
@ -486,6 +490,100 @@ async def chat_web_search_handler(
|
|||||||
return form_data
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
|
async def chat_image_generation_handler(
|
||||||
|
request: Request, form_data: dict, extra_params: dict, user
|
||||||
|
):
|
||||||
|
__event_emitter__ = extra_params["__event_emitter__"]
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "status",
|
||||||
|
"data": {"description": "Generating an image", "done": False},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = form_data["messages"]
|
||||||
|
user_message = get_last_user_message(messages)
|
||||||
|
|
||||||
|
prompt = user_message
|
||||||
|
negative_prompt = ""
|
||||||
|
|
||||||
|
if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION:
|
||||||
|
try:
|
||||||
|
res = await generate_image_prompt(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
"model": form_data["model"],
|
||||||
|
"messages": messages,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = res["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
bracket_start = response.find("{")
|
||||||
|
bracket_end = response.rfind("}") + 1
|
||||||
|
|
||||||
|
if bracket_start == -1 or bracket_end == -1:
|
||||||
|
raise Exception("No JSON object found in the response")
|
||||||
|
|
||||||
|
response = response[bracket_start:bracket_end]
|
||||||
|
response = json.loads(response)
|
||||||
|
prompt = response.get("prompt", [])
|
||||||
|
except Exception as e:
|
||||||
|
prompt = user_message
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
prompt = user_message
|
||||||
|
|
||||||
|
system_message_content = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
images = await image_generations(
|
||||||
|
request=request,
|
||||||
|
form_data=GenerateImageForm(**{"prompt": prompt}),
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "status",
|
||||||
|
"data": {"description": "Generated an image", "done": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for image in images:
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"data": {"content": f"\n"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
system_message_content = "<context>User is shown the generated image, tell the user that the image has been generated</context>"
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "status",
|
||||||
|
"data": {
|
||||||
|
"description": f"An error occured while generating an image",
|
||||||
|
"done": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
system_message_content = "<context>Unable to generate an image, tell the user that an error occured</context>"
|
||||||
|
|
||||||
|
if system_message_content:
|
||||||
|
form_data["messages"] = add_or_update_system_message(
|
||||||
|
system_message_content, form_data["messages"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
async def chat_completion_files_handler(
|
async def chat_completion_files_handler(
|
||||||
request: Request, body: dict, user: UserModel
|
request: Request, body: dict, user: UserModel
|
||||||
) -> tuple[dict, dict[str, list]]:
|
) -> tuple[dict, dict[str, list]]:
|
||||||
@ -523,17 +621,28 @@ async def chat_completion_files_handler(
|
|||||||
if len(queries) == 0:
|
if len(queries) == 0:
|
||||||
queries = [get_last_user_message(body["messages"])]
|
queries = [get_last_user_message(body["messages"])]
|
||||||
|
|
||||||
sources = get_sources_from_files(
|
try:
|
||||||
files=files,
|
# Offload get_sources_from_files to a separate thread
|
||||||
queries=queries,
|
loop = asyncio.get_running_loop()
|
||||||
embedding_function=request.app.state.EMBEDDING_FUNCTION,
|
with ThreadPoolExecutor() as executor:
|
||||||
k=request.app.state.config.TOP_K,
|
sources = await loop.run_in_executor(
|
||||||
reranking_function=request.app.state.rf,
|
executor,
|
||||||
r=request.app.state.config.RELEVANCE_THRESHOLD,
|
lambda: get_sources_from_files(
|
||||||
hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
files=files,
|
||||||
)
|
queries=queries,
|
||||||
|
embedding_function=request.app.state.EMBEDDING_FUNCTION,
|
||||||
|
k=request.app.state.config.TOP_K,
|
||||||
|
reranking_function=request.app.state.rf,
|
||||||
|
r=request.app.state.config.RELEVANCE_THRESHOLD,
|
||||||
|
hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
|
||||||
log.debug(f"rag_contexts:sources: {sources}")
|
log.debug(f"rag_contexts:sources: {sources}")
|
||||||
|
|
||||||
return body, {"sources": sources}
|
return body, {"sources": sources}
|
||||||
|
|
||||||
|
|
||||||
@ -562,6 +671,10 @@ def apply_params_to_form_data(form_data, model):
|
|||||||
|
|
||||||
if "frequency_penalty" in params:
|
if "frequency_penalty" in params:
|
||||||
form_data["frequency_penalty"] = params["frequency_penalty"]
|
form_data["frequency_penalty"] = params["frequency_penalty"]
|
||||||
|
|
||||||
|
if "reasoning_effort" in params:
|
||||||
|
form_data["reasoning_effort"] = params["reasoning_effort"]
|
||||||
|
|
||||||
return form_data
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
@ -640,6 +753,11 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
|||||||
request, form_data, extra_params, user
|
request, form_data, extra_params, user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "image_generation" in features and features["image_generation"]:
|
||||||
|
form_data = await chat_image_generation_handler(
|
||||||
|
request, form_data, extra_params, user
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
form_data, flags = await chat_completion_filter_functions_handler(
|
form_data, flags = await chat_completion_filter_functions_handler(
|
||||||
request, form_data, model, extra_params
|
request, form_data, model, extra_params
|
||||||
@ -958,6 +1076,16 @@ async def process_chat_response(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# We might want to disable this by default
|
||||||
|
detect_reasoning = True
|
||||||
|
reasoning_tags = ["think", "reason", "reasoning", "thought", "Thought"]
|
||||||
|
current_tag = None
|
||||||
|
|
||||||
|
reasoning_start_time = None
|
||||||
|
|
||||||
|
reasoning_content = ""
|
||||||
|
ongoing_content = ""
|
||||||
|
|
||||||
async for line in response.body_iterator:
|
async for line in response.body_iterator:
|
||||||
line = line.decode("utf-8") if isinstance(line, bytes) else line
|
line = line.decode("utf-8") if isinstance(line, bytes) else line
|
||||||
data = line
|
data = line
|
||||||
@ -966,12 +1094,12 @@ async def process_chat_response(
|
|||||||
if not data.strip():
|
if not data.strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# "data: " is the prefix for each event
|
# "data:" is the prefix for each event
|
||||||
if not data.startswith("data: "):
|
if not data.startswith("data:"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Remove the prefix
|
# Remove the prefix
|
||||||
data = data[len("data: ") :]
|
data = data[len("data:") :].strip()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
@ -984,7 +1112,6 @@ async def process_chat_response(
|
|||||||
"selectedModelId": data["selected_model_id"],
|
"selectedModelId": data["selected_model_id"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
value = (
|
value = (
|
||||||
data.get("choices", [])[0]
|
data.get("choices", [])[0]
|
||||||
@ -995,6 +1122,73 @@ async def process_chat_response(
|
|||||||
if value:
|
if value:
|
||||||
content = f"{content}{value}"
|
content = f"{content}{value}"
|
||||||
|
|
||||||
|
if detect_reasoning:
|
||||||
|
for tag in reasoning_tags:
|
||||||
|
start_tag = f"<{tag}>\n"
|
||||||
|
end_tag = f"</{tag}>\n"
|
||||||
|
|
||||||
|
if start_tag in content:
|
||||||
|
# Remove the start tag
|
||||||
|
content = content.replace(start_tag, "")
|
||||||
|
ongoing_content = content
|
||||||
|
|
||||||
|
reasoning_start_time = time.time()
|
||||||
|
reasoning_content = ""
|
||||||
|
|
||||||
|
current_tag = tag
|
||||||
|
break
|
||||||
|
|
||||||
|
if reasoning_start_time is not None:
|
||||||
|
# Remove the last value from the content
|
||||||
|
content = content[: -len(value)]
|
||||||
|
|
||||||
|
reasoning_content += value
|
||||||
|
|
||||||
|
end_tag = f"</{current_tag}>\n"
|
||||||
|
if end_tag in reasoning_content:
|
||||||
|
reasoning_end_time = time.time()
|
||||||
|
reasoning_duration = int(
|
||||||
|
reasoning_end_time
|
||||||
|
- reasoning_start_time
|
||||||
|
)
|
||||||
|
reasoning_content = (
|
||||||
|
reasoning_content.strip(
|
||||||
|
f"<{current_tag}>\n"
|
||||||
|
)
|
||||||
|
.strip(end_tag)
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
if reasoning_content:
|
||||||
|
reasoning_display_content = "\n".join(
|
||||||
|
(
|
||||||
|
f"> {line}"
|
||||||
|
if not line.startswith(">")
|
||||||
|
else line
|
||||||
|
)
|
||||||
|
for line in reasoning_content.splitlines()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format reasoning with <details> tag
|
||||||
|
content = f'{ongoing_content}<details type="reasoning" done="true" duration="{reasoning_duration}">\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n'
|
||||||
|
else:
|
||||||
|
content = ""
|
||||||
|
|
||||||
|
reasoning_start_time = None
|
||||||
|
else:
|
||||||
|
|
||||||
|
reasoning_display_content = "\n".join(
|
||||||
|
(
|
||||||
|
f"> {line}"
|
||||||
|
if not line.startswith(">")
|
||||||
|
else line
|
||||||
|
)
|
||||||
|
for line in reasoning_content.splitlines()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show ongoing thought process
|
||||||
|
content = f'{ongoing_content}<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{reasoning_display_content}\n</details>\n'
|
||||||
|
|
||||||
if ENABLE_REALTIME_CHAT_SAVE:
|
if ENABLE_REALTIME_CHAT_SAVE:
|
||||||
# Save message in the database
|
# Save message in the database
|
||||||
Chats.upsert_message_to_chat_by_id_and_message_id(
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
@ -1015,10 +1209,8 @@ async def process_chat_response(
|
|||||||
"data": data,
|
"data": data,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
done = "data: [DONE]" in line
|
done = "data: [DONE]" in line
|
||||||
|
|
||||||
if done:
|
if done:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -63,17 +63,8 @@ auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
|||||||
class OAuthManager:
|
class OAuthManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.oauth = OAuth()
|
self.oauth = OAuth()
|
||||||
for provider_name, provider_config in OAUTH_PROVIDERS.items():
|
for _, provider_config in OAUTH_PROVIDERS.items():
|
||||||
self.oauth.register(
|
provider_config["register"](self.oauth)
|
||||||
name=provider_name,
|
|
||||||
client_id=provider_config["client_id"],
|
|
||||||
client_secret=provider_config["client_secret"],
|
|
||||||
server_metadata_url=provider_config["server_metadata_url"],
|
|
||||||
client_kwargs={
|
|
||||||
"scope": provider_config["scope"],
|
|
||||||
},
|
|
||||||
redirect_uri=provider_config["redirect_uri"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_client(self, provider_name):
|
def get_client(self, provider_name):
|
||||||
return self.oauth.create_client(provider_name)
|
return self.oauth.create_client(provider_name)
|
||||||
@ -200,14 +191,14 @@ class OAuthManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"OAuth callback error: {e}")
|
log.warning(f"OAuth callback error: {e}")
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
user_data: UserInfo = token["userinfo"]
|
user_data: UserInfo = token.get("userinfo")
|
||||||
if not user_data:
|
if not user_data:
|
||||||
user_data: UserInfo = await client.userinfo(token=token)
|
user_data: UserInfo = await client.userinfo(token=token)
|
||||||
if not user_data:
|
if not user_data:
|
||||||
log.warning(f"OAuth callback failed, user data is missing: {token}")
|
log.warning(f"OAuth callback failed, user data is missing: {token}")
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
|
|
||||||
sub = user_data.get("sub")
|
sub = user_data.get(OAUTH_PROVIDERS[provider].get("sub_claim", "sub"))
|
||||||
if not sub:
|
if not sub:
|
||||||
log.warning(f"OAuth callback failed, sub is missing: {user_data}")
|
log.warning(f"OAuth callback failed, sub is missing: {user_data}")
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
@ -255,12 +246,20 @@ class OAuthManager:
|
|||||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||||
|
|
||||||
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
|
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
|
||||||
picture_url = user_data.get(picture_claim, "")
|
picture_url = user_data.get(
|
||||||
|
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
|
||||||
|
)
|
||||||
if picture_url:
|
if picture_url:
|
||||||
# Download the profile image into a base64 string
|
# Download the profile image into a base64 string
|
||||||
try:
|
try:
|
||||||
|
access_token = token.get("access_token")
|
||||||
|
get_kwargs = {}
|
||||||
|
if access_token:
|
||||||
|
get_kwargs["headers"] = {
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
}
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(picture_url) as resp:
|
async with session.get(picture_url, **get_kwargs) as resp:
|
||||||
picture = await resp.read()
|
picture = await resp.read()
|
||||||
base64_encoded_picture = base64.b64encode(
|
base64_encoded_picture = base64.b64encode(
|
||||||
picture
|
picture
|
||||||
|
@ -47,6 +47,7 @@ def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
|
|||||||
"top_p": float,
|
"top_p": float,
|
||||||
"max_tokens": int,
|
"max_tokens": int,
|
||||||
"frequency_penalty": float,
|
"frequency_penalty": float,
|
||||||
|
"reasoning_effort": str,
|
||||||
"seed": lambda x: x,
|
"seed": lambda x: x,
|
||||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||||
}
|
}
|
||||||
|
@ -217,6 +217,24 @@ def tags_generation_template(
|
|||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def image_prompt_generation_template(
|
||||||
|
template: str, messages: list[dict], user: Optional[dict] = None
|
||||||
|
) -> str:
|
||||||
|
prompt = get_last_user_message(messages)
|
||||||
|
template = replace_prompt_variable(template, prompt)
|
||||||
|
template = replace_messages_variable(template, messages)
|
||||||
|
|
||||||
|
template = prompt_template(
|
||||||
|
template,
|
||||||
|
**(
|
||||||
|
{"user_name": user.get("name"), "user_location": user.get("location")}
|
||||||
|
if user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
def emoji_generation_template(
|
def emoji_generation_template(
|
||||||
template: str, prompt: str, user: Optional[dict] = None
|
template: str, prompt: str, user: Optional[dict] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
|
@ -47,6 +47,8 @@ pymilvus==2.5.0
|
|||||||
qdrant-client~=1.12.0
|
qdrant-client~=1.12.0
|
||||||
opensearch-py==2.7.1
|
opensearch-py==2.7.1
|
||||||
|
|
||||||
|
|
||||||
|
transformers
|
||||||
sentence-transformers==3.3.1
|
sentence-transformers==3.3.1
|
||||||
colbert-ai==0.2.21
|
colbert-ai==0.2.21
|
||||||
einops==0.8.0
|
einops==0.8.0
|
||||||
@ -87,7 +89,7 @@ pytube==15.0.0
|
|||||||
|
|
||||||
extract_msg
|
extract_msg
|
||||||
pydub
|
pydub
|
||||||
duckduckgo-search~=6.3.5
|
duckduckgo-search~=7.2.1
|
||||||
|
|
||||||
## Google Drive
|
## Google Drive
|
||||||
google-api-python-client
|
google-api-python-client
|
||||||
@ -100,6 +102,7 @@ pytest~=8.3.2
|
|||||||
pytest-docker~=3.1.1
|
pytest-docker~=3.1.1
|
||||||
|
|
||||||
googleapis-common-protos==1.63.2
|
googleapis-common-protos==1.63.2
|
||||||
|
google-cloud-storage==2.19.0
|
||||||
|
|
||||||
## LDAP
|
## LDAP
|
||||||
ldap3==2.9.1
|
ldap3==2.9.1
|
||||||
|
22
package-lock.json
generated
22
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.5.4",
|
"version": "0.5.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.5.4",
|
"version": "0.5.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"js-sha256": "^0.10.1",
|
"js-sha256": "^0.10.1",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.21",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"mermaid": "^10.9.3",
|
"mermaid": "^10.9.3",
|
||||||
"paneforge": "^0.0.6",
|
"paneforge": "^0.0.6",
|
||||||
@ -89,7 +89,7 @@
|
|||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.3.5",
|
"vite": "^5.4.14",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -7110,13 +7110,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/katex": {
|
"node_modules/katex": {
|
||||||
"version": "0.16.10",
|
"version": "0.16.21",
|
||||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",
|
||||||
"integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
|
"integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==",
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://opencollective.com/katex",
|
"https://opencollective.com/katex",
|
||||||
"https://github.com/sponsors/katex"
|
"https://github.com/sponsors/katex"
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^8.3.0"
|
"commander": "^8.3.0"
|
||||||
},
|
},
|
||||||
@ -11677,9 +11678,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.6",
|
"version": "5.4.14",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
|
||||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.5.4",
|
"version": "0.5.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
|
"dev:5050": "npm run pyodide:fetch && vite dev --port 5050",
|
||||||
"build": "npm run pyodide:fetch && vite build",
|
"build": "npm run pyodide:fetch && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
@ -44,7 +45,7 @@
|
|||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.3.5",
|
"vite": "^5.4.14",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -82,7 +83,7 @@
|
|||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"js-sha256": "^0.10.1",
|
"js-sha256": "^0.10.1",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.21",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"mermaid": "^10.9.3",
|
"mermaid": "^10.9.3",
|
||||||
"paneforge": "^0.0.6",
|
"paneforge": "^0.0.6",
|
||||||
|
@ -54,6 +54,7 @@ dependencies = [
|
|||||||
"qdrant-client~=1.12.0",
|
"qdrant-client~=1.12.0",
|
||||||
"opensearch-py==2.7.1",
|
"opensearch-py==2.7.1",
|
||||||
|
|
||||||
|
"transformers",
|
||||||
"sentence-transformers==3.3.1",
|
"sentence-transformers==3.3.1",
|
||||||
"colbert-ai==0.2.21",
|
"colbert-ai==0.2.21",
|
||||||
"einops==0.8.0",
|
"einops==0.8.0",
|
||||||
@ -93,15 +94,22 @@ dependencies = [
|
|||||||
|
|
||||||
"extract_msg",
|
"extract_msg",
|
||||||
"pydub",
|
"pydub",
|
||||||
"duckduckgo-search~=6.3.5",
|
"duckduckgo-search~=7.2.1",
|
||||||
|
|
||||||
|
"google-api-python-client",
|
||||||
|
"google-auth-httplib2",
|
||||||
|
"google-auth-oauthlib",
|
||||||
|
|
||||||
"docker~=7.1.0",
|
"docker~=7.1.0",
|
||||||
"pytest~=8.3.2",
|
"pytest~=8.3.2",
|
||||||
"pytest-docker~=3.1.1",
|
"pytest-docker~=3.1.1",
|
||||||
|
"moto[s3]>=5.0.26",
|
||||||
|
|
||||||
"googleapis-common-protos==1.63.2",
|
"googleapis-common-protos==1.63.2",
|
||||||
|
"google-cloud-storage==2.19.0",
|
||||||
|
|
||||||
"ldap3==2.9.1"
|
"ldap3==2.9.1",
|
||||||
|
"gcp-storage-emulator>=2024.8.3",
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">= 3.11, < 3.13.0a1"
|
requires-python = ">= 3.11, < 3.13.0a1"
|
||||||
|
65
src/app.css
65
src/app.css
@ -53,11 +53,11 @@ math {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-prose {
|
.markdown-prose {
|
||||||
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
@apply prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-prose-xs {
|
.markdown-prose-xs {
|
||||||
@apply text-xs prose dark:prose-invert prose-headings:font-semibold prose-hr:my-0 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
@apply text-xs prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown a {
|
.markdown a {
|
||||||
@ -68,6 +68,19 @@ math {
|
|||||||
font-family: 'Archivo', sans-serif;
|
font-family: 'Archivo', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drag-region {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-region a,
|
||||||
|
.drag-region button {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-drag-region {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
@apply rounded-lg;
|
@apply rounded-lg;
|
||||||
}
|
}
|
||||||
@ -102,18 +115,62 @@ li p {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
|
||||||
background-position: right 0.5rem center;
|
background-position: right 0rem center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 1.5em 1.5em;
|
background-size: 1.5em 1.5em;
|
||||||
padding-right: 2.5rem;
|
|
||||||
-webkit-print-color-adjust: exact;
|
-webkit-print-color-adjust: exact;
|
||||||
print-color-adjust: exact;
|
print-color-adjust: exact;
|
||||||
|
/* padding-right: 2.5rem; */
|
||||||
/* for Firefox */
|
/* for Firefox */
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
/* for Chrome */
|
/* for Chrome */
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shimmer {
|
||||||
|
background: linear-gradient(90deg, #9a9b9e 25%, #2a2929 50%, #9a9b9e 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: shimmer 4s linear infinite;
|
||||||
|
color: #818286; /* Fallback color */
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .shimmer {
|
||||||
|
background: linear-gradient(90deg, #818286 25%, #eae5e5 50%, #818286 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: shimmer 4s linear infinite;
|
||||||
|
color: #a1a3a7; /* Darker fallback color for dark mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes smoothFadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-description {
|
||||||
|
animation: smoothFadeIn 0.2s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
.katex-mathml {
|
.katex-mathml {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -360,12 +360,7 @@ export const generateChatCompletion = async (token: string = '', body: object) =
|
|||||||
return [res, controller];
|
return [res, controller];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createModel = async (
|
export const createModel = async (token: string, payload: object, urlIdx: string | null = null) => {
|
||||||
token: string,
|
|
||||||
tagName: string,
|
|
||||||
content: string,
|
|
||||||
urlIdx: string | null = null
|
|
||||||
) => {
|
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@ -377,10 +372,7 @@ export const createModel = async (
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload)
|
||||||
name: tagName,
|
|
||||||
modelfile: content
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
error = err;
|
error = err;
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
const shareHandler = async (func) => {
|
const shareHandler = async (func) => {
|
||||||
const item = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
const item = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -88,7 +88,7 @@
|
|||||||
|
|
||||||
const cloneHandler = async (func) => {
|
const cloneHandler = async (func) => {
|
||||||
const _function = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
const _function = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -104,7 +104,7 @@
|
|||||||
|
|
||||||
const exportHandler = async (func) => {
|
const exportHandler = async (func) => {
|
||||||
const _function = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
const _function = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -118,7 +118,7 @@
|
|||||||
|
|
||||||
const deleteHandler = async (func) => {
|
const deleteHandler = async (func) => {
|
||||||
const res = await deleteFunctionById(localStorage.token, func.id).catch((error) => {
|
const res = await deleteFunctionById(localStorage.token, func.id).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -132,7 +132,7 @@
|
|||||||
|
|
||||||
const toggleGlobalHandler = async (func) => {
|
const toggleGlobalHandler = async (func) => {
|
||||||
const res = await toggleGlobalById(localStorage.token, func.id).catch((error) => {
|
const res = await toggleGlobalById(localStorage.token, func.id).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -418,7 +418,7 @@
|
|||||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
const _functions = await exportFunctions(localStorage.token).catch((error) => {
|
const _functions = await exportFunctions(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -510,7 +510,7 @@
|
|||||||
|
|
||||||
for (const func of _functions) {
|
for (const func of _functions) {
|
||||||
const res = await createNewFunction(localStorage.token, func).catch((error) => {
|
const res = await createNewFunction(localStorage.token, func).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -43,9 +43,8 @@
|
|||||||
|
|
||||||
const updateOpenAIHandler = async () => {
|
const updateOpenAIHandler = async () => {
|
||||||
if (ENABLE_OPENAI_API !== null) {
|
if (ENABLE_OPENAI_API !== null) {
|
||||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
// Remove trailing slashes
|
||||||
(url, urlIdx) => OPENAI_API_BASE_URLS.indexOf(url) === urlIdx && url !== ''
|
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
|
||||||
).map((url) => url.replace(/\/$/, ''));
|
|
||||||
|
|
||||||
// Check if API KEYS length is same than API URLS length
|
// Check if API KEYS length is same than API URLS length
|
||||||
if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
|
if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
|
||||||
@ -69,7 +68,7 @@
|
|||||||
OPENAI_API_KEYS: OPENAI_API_KEYS,
|
OPENAI_API_KEYS: OPENAI_API_KEYS,
|
||||||
OPENAI_API_CONFIGS: OPENAI_API_CONFIGS
|
OPENAI_API_CONFIGS: OPENAI_API_CONFIGS
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -81,24 +80,15 @@
|
|||||||
|
|
||||||
const updateOllamaHandler = async () => {
|
const updateOllamaHandler = async () => {
|
||||||
if (ENABLE_OLLAMA_API !== null) {
|
if (ENABLE_OLLAMA_API !== null) {
|
||||||
// Remove duplicate URLs
|
// Remove trailing slashes
|
||||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter(
|
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.map((url) => url.replace(/\/$/, ''));
|
||||||
(url, urlIdx) => OLLAMA_BASE_URLS.indexOf(url) === urlIdx && url !== ''
|
|
||||||
).map((url) => url.replace(/\/$/, ''));
|
|
||||||
|
|
||||||
console.log(OLLAMA_BASE_URLS);
|
|
||||||
|
|
||||||
if (OLLAMA_BASE_URLS.length === 0) {
|
|
||||||
ENABLE_OLLAMA_API = false;
|
|
||||||
toast.info($i18n.t('Ollama API disabled'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await updateOllamaConfig(localStorage.token, {
|
const res = await updateOllamaConfig(localStorage.token, {
|
||||||
ENABLE_OLLAMA_API: ENABLE_OLLAMA_API,
|
ENABLE_OLLAMA_API: ENABLE_OLLAMA_API,
|
||||||
OLLAMA_BASE_URLS: OLLAMA_BASE_URLS,
|
OLLAMA_BASE_URLS: OLLAMA_BASE_URLS,
|
||||||
OLLAMA_API_CONFIGS: OLLAMA_API_CONFIGS
|
OLLAMA_API_CONFIGS: OLLAMA_API_CONFIGS
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -111,14 +101,14 @@
|
|||||||
const addOpenAIConnectionHandler = async (connection) => {
|
const addOpenAIConnectionHandler = async (connection) => {
|
||||||
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
|
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
|
||||||
OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
|
OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
|
||||||
OPENAI_API_CONFIGS[connection.url] = connection.config;
|
OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length] = connection.config;
|
||||||
|
|
||||||
await updateOpenAIHandler();
|
await updateOpenAIHandler();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addOllamaConnectionHandler = async (connection) => {
|
const addOllamaConnectionHandler = async (connection) => {
|
||||||
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
|
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
|
||||||
OLLAMA_API_CONFIGS[connection.url] = connection.config;
|
OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length] = connection.config;
|
||||||
|
|
||||||
await updateOllamaHandler();
|
await updateOllamaHandler();
|
||||||
};
|
};
|
||||||
@ -148,15 +138,17 @@
|
|||||||
OLLAMA_API_CONFIGS = ollamaConfig.OLLAMA_API_CONFIGS;
|
OLLAMA_API_CONFIGS = ollamaConfig.OLLAMA_API_CONFIGS;
|
||||||
|
|
||||||
if (ENABLE_OPENAI_API) {
|
if (ENABLE_OPENAI_API) {
|
||||||
for (const url of OPENAI_API_BASE_URLS) {
|
// get url and idx
|
||||||
if (!OPENAI_API_CONFIGS[url]) {
|
for (const [idx, url] of OPENAI_API_BASE_URLS.entries()) {
|
||||||
OPENAI_API_CONFIGS[url] = {};
|
if (!OPENAI_API_CONFIGS[idx]) {
|
||||||
|
// Legacy support, url as key
|
||||||
|
OPENAI_API_CONFIGS[idx] = OPENAI_API_CONFIGS[url] || {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
|
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
|
||||||
OPENAI_API_CONFIGS[url] = OPENAI_API_CONFIGS[url] || {};
|
OPENAI_API_CONFIGS[idx] = OPENAI_API_CONFIGS[idx] || {};
|
||||||
if (!(OPENAI_API_CONFIGS[url]?.enable ?? true)) {
|
if (!(OPENAI_API_CONFIGS[idx]?.enable ?? true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await getOpenAIModels(localStorage.token, idx);
|
const res = await getOpenAIModels(localStorage.token, idx);
|
||||||
@ -167,9 +159,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ENABLE_OLLAMA_API) {
|
if (ENABLE_OLLAMA_API) {
|
||||||
for (const url of OLLAMA_BASE_URLS) {
|
for (const [idx, url] of OLLAMA_BASE_URLS.entries()) {
|
||||||
if (!OLLAMA_API_CONFIGS[url]) {
|
if (!OLLAMA_API_CONFIGS[idx]) {
|
||||||
OLLAMA_API_CONFIGS[url] = {};
|
OLLAMA_API_CONFIGS[idx] = OLLAMA_API_CONFIGS[url] || {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -242,7 +234,7 @@
|
|||||||
pipeline={pipelineUrls[url] ? true : false}
|
pipeline={pipelineUrls[url] ? true : false}
|
||||||
bind:url
|
bind:url
|
||||||
bind:key={OPENAI_API_KEYS[idx]}
|
bind:key={OPENAI_API_KEYS[idx]}
|
||||||
bind:config={OPENAI_API_CONFIGS[url]}
|
bind:config={OPENAI_API_CONFIGS[idx]}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
updateOpenAIHandler();
|
updateOpenAIHandler();
|
||||||
}}
|
}}
|
||||||
@ -251,6 +243,8 @@
|
|||||||
(url, urlIdx) => idx !== urlIdx
|
(url, urlIdx) => idx !== urlIdx
|
||||||
);
|
);
|
||||||
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||||
|
|
||||||
|
delete OPENAI_API_CONFIGS[idx];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
@ -301,13 +295,14 @@
|
|||||||
{#each OLLAMA_BASE_URLS as url, idx}
|
{#each OLLAMA_BASE_URLS as url, idx}
|
||||||
<OllamaConnection
|
<OllamaConnection
|
||||||
bind:url
|
bind:url
|
||||||
bind:config={OLLAMA_API_CONFIGS[url]}
|
bind:config={OLLAMA_API_CONFIGS[idx]}
|
||||||
{idx}
|
{idx}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
updateOllamaHandler();
|
updateOllamaHandler();
|
||||||
}}
|
}}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
||||||
|
delete OLLAMA_API_CONFIGS[idx];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
const verifyOllamaHandler = async () => {
|
const verifyOllamaHandler = async () => {
|
||||||
const res = await verifyOllamaConnection(localStorage.token, url, key).catch((error) => {
|
const res = await verifyOllamaConnection(localStorage.token, url, key).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
const verifyOpenAIHandler = async () => {
|
const verifyOpenAIHandler = async () => {
|
||||||
const res = await verifyOpenAIConnection(localStorage.token, url, key).catch((error) => {
|
const res = await verifyOpenAIConnection(localStorage.token, url, key).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@
|
|||||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||||
import Wrench from '$lib/components/icons/Wrench.svelte';
|
import Wrench from '$lib/components/icons/Wrench.svelte';
|
||||||
import ManageOllamaModal from './ManageOllamaModal.svelte';
|
import ManageOllamaModal from './ManageOllamaModal.svelte';
|
||||||
|
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
||||||
|
|
||||||
export let onDelete = () => {};
|
export let onDelete = () => {};
|
||||||
export let onSubmit = () => {};
|
export let onSubmit = () => {};
|
||||||
@ -70,7 +71,7 @@
|
|||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Wrench />
|
<ArrowDownTray />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
reader.onload = async (e) => {
|
reader.onload = async (e) => {
|
||||||
const res = await importConfig(localStorage.token, JSON.parse(e.target.result)).catch(
|
const res = await importConfig(localStorage.token, JSON.parse(e.target.result)).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -132,7 +132,7 @@
|
|||||||
// exportAllUserChats();
|
// exportAllUserChats();
|
||||||
|
|
||||||
downloadDatabase(localStorage.token).catch((error) => {
|
downloadDatabase(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -119,7 +119,7 @@
|
|||||||
url: OpenAIUrl
|
url: OpenAIUrl
|
||||||
}
|
}
|
||||||
}).catch(async (error) => {
|
}).catch(async (error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
await setEmbeddingConfig();
|
await setEmbeddingConfig();
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@ -142,7 +142,7 @@
|
|||||||
const res = await updateRerankingConfig(localStorage.token, {
|
const res = await updateRerankingConfig(localStorage.token, {
|
||||||
reranking_model: rerankingModel
|
reranking_model: rerankingModel
|
||||||
}).catch(async (error) => {
|
}).catch(async (error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
await setRerankingConfig();
|
await setRerankingConfig();
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@ -258,7 +258,7 @@
|
|||||||
bind:show={showResetUploadDirConfirm}
|
bind:show={showResetUploadDirConfirm}
|
||||||
on:confirm={async () => {
|
on:confirm={async () => {
|
||||||
const res = await deleteAllFiles(localStorage.token).catch((error) => {
|
const res = await deleteAllFiles(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -272,7 +272,7 @@
|
|||||||
bind:show={showResetConfirm}
|
bind:show={showResetConfirm}
|
||||||
on:confirm={() => {
|
on:confirm={() => {
|
||||||
const res = resetVectorDB(localStorage.token).catch((error) => {
|
const res = resetVectorDB(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
label: '',
|
label: '',
|
||||||
host: '',
|
host: '',
|
||||||
port: '',
|
port: '',
|
||||||
|
attribute_for_mail: 'mail',
|
||||||
attribute_for_username: 'uid',
|
attribute_for_username: 'uid',
|
||||||
app_dn: '',
|
app_dn: '',
|
||||||
app_dn_password: '',
|
app_dn_password: '',
|
||||||
@ -41,7 +42,7 @@
|
|||||||
const updateLdapServerHandler = async () => {
|
const updateLdapServerHandler = async () => {
|
||||||
if (!ENABLE_LDAP) return;
|
if (!ENABLE_LDAP) return;
|
||||||
const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
|
const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -342,6 +343,26 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Attribute for Mail')}
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t(
|
||||||
|
'The LDAP attribute that maps to the mail that users use to sign in.'
|
||||||
|
)}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-none py-0.5"
|
||||||
|
required
|
||||||
|
placeholder={$i18n.t('Example: mail')}
|
||||||
|
bind:value={LDAP_SERVER.attribute_for_mail}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex w-full gap-2">
|
<div class="flex w-full gap-2">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
const getModels = async () => {
|
const getModels = async () => {
|
||||||
models = await getImageGenerationModels(localStorage.token).catch((error) => {
|
models = await getImageGenerationModels(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -107,11 +107,11 @@
|
|||||||
const updateConfigHandler = async () => {
|
const updateConfigHandler = async () => {
|
||||||
const res = await updateConfig(localStorage.token, config)
|
const res = await updateConfig(localStorage.token, config)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -159,13 +159,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await updateConfig(localStorage.token, config).catch((error) => {
|
await updateConfig(localStorage.token, config).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
loading = false;
|
loading = false;
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateImageGenerationConfig(localStorage.token, imageGenerationConfig).catch((error) => {
|
await updateImageGenerationConfig(localStorage.token, imageGenerationConfig).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
loading = false;
|
loading = false;
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@ -178,7 +178,7 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user.role === 'admin') {
|
if ($user.role === 'admin') {
|
||||||
const res = await getConfig(localStorage.token).catch((error) => {
|
const res = await getConfig(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -211,7 +211,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const imageConfigRes = await getImageGenerationConfig(localStorage.token).catch((error) => {
|
const imageConfigRes = await getImageGenerationConfig(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -234,7 +234,7 @@
|
|||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-1 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Image Generation (Experimental)')}
|
{$i18n.t('Image Generation (Experimental)')}
|
||||||
</div>
|
</div>
|
||||||
@ -271,7 +271,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
{#if config.enabled}
|
||||||
|
<div class=" py-1 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Image Prompt Generation')}</div>
|
||||||
|
<div class="px-1">
|
||||||
|
<Switch bind:state={config.prompt_generation} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class=" py-1 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
@ -309,7 +318,7 @@
|
|||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await updateConfigHandler();
|
await updateConfigHandler();
|
||||||
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
|
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -445,7 +454,7 @@
|
|||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await updateConfigHandler();
|
await updateConfigHandler();
|
||||||
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
|
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
TASK_MODEL: '',
|
TASK_MODEL: '',
|
||||||
TASK_MODEL_EXTERNAL: '',
|
TASK_MODEL_EXTERNAL: '',
|
||||||
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
||||||
|
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: '',
|
||||||
ENABLE_AUTOCOMPLETE_GENERATION: true,
|
ENABLE_AUTOCOMPLETE_GENERATION: true,
|
||||||
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: -1,
|
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: -1,
|
||||||
TAGS_GENERATION_PROMPT_TEMPLATE: '',
|
TAGS_GENERATION_PROMPT_TEMPLATE: '',
|
||||||
@ -140,6 +141,22 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Image Prompt Generation Prompt')}</div>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
|
||||||
|
placeholder={$i18n.t(
|
||||||
|
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
||||||
|
|
||||||
<div class="my-3 flex w-full items-center justify-between">
|
<div class="my-3 flex w-full items-center justify-between">
|
||||||
|
@ -26,6 +26,9 @@
|
|||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||||
import ConfigureModelsModal from './Models/ConfigureModelsModal.svelte';
|
import ConfigureModelsModal from './Models/ConfigureModelsModal.svelte';
|
||||||
|
import Wrench from '$lib/components/icons/Wrench.svelte';
|
||||||
|
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
||||||
|
import ManageModelsModal from './Models/ManageModelsModal.svelte';
|
||||||
|
|
||||||
let importFiles;
|
let importFiles;
|
||||||
let modelsImportInputElement: HTMLInputElement;
|
let modelsImportInputElement: HTMLInputElement;
|
||||||
@ -39,6 +42,7 @@
|
|||||||
let selectedModelId = null;
|
let selectedModelId = null;
|
||||||
|
|
||||||
let showConfigModal = false;
|
let showConfigModal = false;
|
||||||
|
let showManageModal = false;
|
||||||
|
|
||||||
$: if (models) {
|
$: if (models) {
|
||||||
filteredModels = models
|
filteredModels = models
|
||||||
@ -138,6 +142,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ConfigureModelsModal bind:show={showConfigModal} initHandler={init} />
|
<ConfigureModelsModal bind:show={showConfigModal} initHandler={init} />
|
||||||
|
<ManageModelsModal bind:show={showManageModal} />
|
||||||
|
|
||||||
{#if models !== null}
|
{#if models !== null}
|
||||||
{#if selectedModelId === null}
|
{#if selectedModelId === null}
|
||||||
@ -151,10 +156,22 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="flex items-center gap-1.5">
|
||||||
<Tooltip content={$i18n.t('Configure')}>
|
<Tooltip content={$i18n.t('Manage Models')}>
|
||||||
<button
|
<button
|
||||||
class=" px-2.5 py-1 rounded-full flex gap-1 items-center"
|
class=" p-1 rounded-full flex gap-1 items-center"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showManageModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowDownTray />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={$i18n.t('Settings')}>
|
||||||
|
<button
|
||||||
|
class=" p-1 rounded-full flex gap-1 items-center"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showConfigModal = true;
|
showConfigModal = true;
|
||||||
|
@ -33,6 +33,24 @@
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (selectedModelId) {
|
||||||
|
onModelSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onModelSelect = () => {
|
||||||
|
if (selectedModelId === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultModelIds.includes(selectedModelId)) {
|
||||||
|
selectedModelId = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultModelIds = [...defaultModelIds, selectedModelId];
|
||||||
|
selectedModelId = '';
|
||||||
|
};
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
config = await getModelsConfig(localStorage.token);
|
config = await getModelsConfig(localStorage.token);
|
||||||
|
|
||||||
@ -95,7 +113,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
|
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
|
||||||
<div class=" text-lg font-medium self-center font-primary">
|
<div class=" text-lg font-medium self-center font-primary">
|
||||||
{$i18n.t('Configure Models')}
|
{$i18n.t('Settings')}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
@ -143,6 +161,24 @@
|
|||||||
<div class="text-xs text-gray-500">{$i18n.t('Default Models')}</div>
|
<div class="text-xs text-gray-500">{$i18n.t('Default Models')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center -mr-1">
|
||||||
|
<select
|
||||||
|
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
|
||||||
|
? ''
|
||||||
|
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||||
|
bind:value={selectedModelId}
|
||||||
|
>
|
||||||
|
<option value="">{$i18n.t('Select a model')}</option>
|
||||||
|
{#each $models as model}
|
||||||
|
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
|
||||||
|
>{model.name}</option
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" /> -->
|
||||||
|
|
||||||
{#if defaultModelIds.length > 0}
|
{#if defaultModelIds.length > 0}
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#each defaultModelIds as modelId, modelIdx}
|
{#each defaultModelIds as modelId, modelIdx}
|
||||||
@ -170,44 +206,6 @@
|
|||||||
{$i18n.t('No models selected')}
|
{$i18n.t('No models selected')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<select
|
|
||||||
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
|
|
||||||
? ''
|
|
||||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
|
||||||
bind:value={selectedModelId}
|
|
||||||
>
|
|
||||||
<option value="">{$i18n.t('Select a model')}</option>
|
|
||||||
{#each $models as model}
|
|
||||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
|
|
||||||
>{model.name}</option
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
if (selectedModelId === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultModelIds.includes(selectedModelId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultModelIds = [...defaultModelIds, selectedModelId];
|
|
||||||
selectedModelId = '';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="size-3.5" strokeWidth="2" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import ManageOllama from './ManageOllama.svelte';
|
||||||
|
|
||||||
|
export let ollamaConfig = null;
|
||||||
|
|
||||||
|
let selectedUrlIdx = 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if ollamaConfig}
|
||||||
|
<div class="flex-1 mb-2.5 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850">
|
||||||
|
<select
|
||||||
|
class="w-full py-2 px-4 text-sm outline-none bg-transparent"
|
||||||
|
bind:value={selectedUrlIdx}
|
||||||
|
placeholder={$i18n.t('Select an Ollama instance')}
|
||||||
|
>
|
||||||
|
{#each ollamaConfig.OLLAMA_BASE_URLS as url, idx}
|
||||||
|
<option value={idx}>{url}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ManageOllama urlIdx={selectedUrlIdx} />
|
||||||
|
{/if}
|
1042
src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte
Normal file
1042
src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,110 @@
|
|||||||
|
<script>
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import { createEventDispatcher, getContext, onMount } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
import { user } from '$lib/stores';
|
||||||
|
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import ManageOllama from './Manage/ManageOllama.svelte';
|
||||||
|
import { getOllamaConfig } from '$lib/apis/ollama';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import ManageMultipleOllama from './Manage/ManageMultipleOllama.svelte';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
|
||||||
|
let selected = null;
|
||||||
|
let ollamaConfig = null;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if ($user.role === 'admin') {
|
||||||
|
await Promise.all([
|
||||||
|
(async () => {
|
||||||
|
ollamaConfig = await getOllamaConfig(localStorage.token);
|
||||||
|
})()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (ollamaConfig) {
|
||||||
|
selected = 'ollama';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selected = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal size="sm" bind:show>
|
||||||
|
<div>
|
||||||
|
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4">
|
||||||
|
<div class=" text-lg font-medium self-center font-primary">
|
||||||
|
{$i18n.t('Manage Models')}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row w-full px-3 pb-4 md:space-x-4 dark:text-gray-200">
|
||||||
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
{#if selected === ''}
|
||||||
|
<div class=" py-5 text-gray-400 text-xs">
|
||||||
|
<div>
|
||||||
|
{$i18n.t('No inference engine with management support found')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if selected !== null}
|
||||||
|
<div class=" flex w-full flex-col">
|
||||||
|
<div
|
||||||
|
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent dark:text-gray-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="min-w-fit rounded-full p-1.5 {selected === 'ollama'
|
||||||
|
? ''
|
||||||
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
|
on:click={() => {
|
||||||
|
selected = 'ollama';
|
||||||
|
}}>{$i18n.t('Ollama')}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- <button
|
||||||
|
class="min-w-fit rounded-full p-1.5 {selected === 'llamacpp'
|
||||||
|
? ''
|
||||||
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
|
on:click={() => {
|
||||||
|
selected = 'llamacpp';
|
||||||
|
}}>{$i18n.t('Llama.cpp')}</button
|
||||||
|
> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" px-1.5 py-1">
|
||||||
|
{#if selected === 'ollama'}
|
||||||
|
<ManageMultipleOllama {ollamaConfig} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class=" py-5">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
@ -57,7 +57,7 @@
|
|||||||
valves,
|
valves,
|
||||||
selectedPipelinesUrlIdx
|
selectedPipelinesUrlIdx
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -118,7 +118,7 @@
|
|||||||
pipelineDownloadUrl,
|
pipelineDownloadUrl,
|
||||||
selectedPipelinesUrlIdx
|
selectedPipelinesUrlIdx
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -172,7 +172,7 @@
|
|||||||
pipelines[selectedPipelineIdx].id,
|
pipelines[selectedPipelineIdx].id,
|
||||||
selectedPipelinesUrlIdx
|
selectedPipelinesUrlIdx
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,10 +53,15 @@
|
|||||||
tools: false
|
tools: false
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
|
controls: true,
|
||||||
file_upload: true,
|
file_upload: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
edit: true,
|
edit: true,
|
||||||
temporary: true
|
temporary: true
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
web_search: true,
|
||||||
|
image_generation: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,7 +74,7 @@
|
|||||||
|
|
||||||
const addGroupHandler = async (group) => {
|
const addGroupHandler = async (group) => {
|
||||||
const res = await createNewGroup(localStorage.token, group).catch((error) => {
|
const res = await createNewGroup(localStorage.token, group).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -84,7 +89,7 @@
|
|||||||
|
|
||||||
const res = await updateUserDefaultPermissions(localStorage.token, group.permissions).catch(
|
const res = await updateUserDefaultPermissions(localStorage.token, group.permissions).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -37,10 +37,15 @@
|
|||||||
tools: false
|
tools: false
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
|
controls: true,
|
||||||
file_upload: true,
|
file_upload: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
edit: true,
|
edit: true,
|
||||||
temporary: true
|
temporary: true
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
web_search: true,
|
||||||
|
image_generation: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export let userIds = [];
|
export let userIds = [];
|
||||||
@ -65,20 +70,8 @@
|
|||||||
if (group) {
|
if (group) {
|
||||||
name = group.name;
|
name = group.name;
|
||||||
description = group.description;
|
description = group.description;
|
||||||
permissions = group?.permissions ?? {
|
permissions = group?.permissions ?? {};
|
||||||
workspace: {
|
|
||||||
models: false,
|
|
||||||
knowledge: false,
|
|
||||||
prompts: false,
|
|
||||||
tools: false
|
|
||||||
},
|
|
||||||
chat: {
|
|
||||||
file_upload: true,
|
|
||||||
delete: true,
|
|
||||||
edit: true,
|
|
||||||
temporary: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
userIds = group?.user_ids ?? [];
|
userIds = group?.user_ids ?? [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
const updateHandler = async (_group) => {
|
const updateHandler = async (_group) => {
|
||||||
const res = await updateGroupById(localStorage.token, group.id, _group).catch((error) => {
|
const res = await updateGroupById(localStorage.token, group.id, _group).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
const deleteHandler = async () => {
|
const deleteHandler = async () => {
|
||||||
const res = await deleteGroupById(localStorage.token, group.id).catch((error) => {
|
const res = await deleteGroupById(localStorage.token, group.id).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
export let permissions = {
|
// Default values for permissions
|
||||||
|
const defaultPermissions = {
|
||||||
workspace: {
|
workspace: {
|
||||||
models: false,
|
models: false,
|
||||||
knowledge: false,
|
knowledge: false,
|
||||||
@ -13,12 +14,38 @@
|
|||||||
tools: false
|
tools: false
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
|
controls: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
edit: true,
|
edit: true,
|
||||||
temporary: true,
|
temporary: true,
|
||||||
file_upload: true
|
file_upload: true
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
web_search: true,
|
||||||
|
image_generation: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export let permissions = {};
|
||||||
|
|
||||||
|
// Reactive statement to ensure all fields are present in `permissions`
|
||||||
|
$: {
|
||||||
|
permissions = fillMissingProperties(permissions, defaultPermissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillMissingProperties(obj: any, defaults: any) {
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
...obj,
|
||||||
|
workspace: { ...defaults.workspace, ...obj.workspace },
|
||||||
|
chat: { ...defaults.chat, ...obj.chat },
|
||||||
|
features: { ...defaults.features, ...obj.features }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
permissions = fillMissingProperties(permissions, defaultPermissions);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -169,6 +196,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat Controls')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={permissions.chat.controls} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Allow File Upload')}
|
{$i18n.t('Allow File Upload')}
|
||||||
@ -201,4 +236,26 @@
|
|||||||
<Switch bind:state={permissions.chat.temporary} />
|
<Switch bind:state={permissions.chat.temporary} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Web Search')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={permissions.features.web_search} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Image Generation')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={permissions.features.image_generation} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
const updateRoleHandler = async (id, role) => {
|
const updateRoleHandler = async (id, role) => {
|
||||||
const res = await updateUserRole(localStorage.token, id, role).catch((error) => {
|
const res = await updateUserRole(localStorage.token, id, role).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
const deleteUserHandler = async (id) => {
|
const deleteUserHandler = async (id) => {
|
||||||
const res = await deleteUserById(localStorage.token, id).catch((error) => {
|
const res = await deleteUserById(localStorage.token, id).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
if (res) {
|
if (res) {
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
_user.password,
|
_user.password,
|
||||||
_user.role
|
_user.role
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -150,9 +150,13 @@
|
|||||||
submitHandler();
|
submitHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex text-center text-sm font-medium rounded-full bg-transparent/10 p-1 mb-2">
|
<div
|
||||||
|
class="flex -mt-2 mb-1.5 gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent dark:text-gray-200"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="w-full rounded-full p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
|
class="min-w-fit rounded-full p-1.5 {tab === ''
|
||||||
|
? ''
|
||||||
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tab = '';
|
tab = '';
|
||||||
@ -160,15 +164,16 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full rounded-full p-1 {tab === 'import'
|
class="min-w-fit rounded-full p-1.5 {tab === 'import'
|
||||||
? 'bg-gray-50 dark:bg-gray-850'
|
? ''
|
||||||
: ''}"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tab = 'import';
|
tab = 'import';
|
||||||
}}>{$i18n.t('CSV Import')}</button
|
}}>{$i18n.t('CSV Import')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
{#if tab === ''}
|
{#if tab === ''}
|
||||||
<div class="flex flex-col w-full mb-3">
|
<div class="flex flex-col w-full mb-3">
|
||||||
@ -176,7 +181,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
class="w-full capitalize rounded-lg text-sm bg-transparent dark:disabled:text-gray-500 outline-none"
|
||||||
bind:value={_user.role}
|
bind:value={_user.role}
|
||||||
placeholder={$i18n.t('Enter Your Role')}
|
placeholder={$i18n.t('Enter Your Role')}
|
||||||
required
|
required
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
const res = await updateUserById(localStorage.token, selectedUser.id, _user).catch((error) => {
|
const res = await updateUserById(localStorage.token, selectedUser.id, _user).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
const deleteChatHandler = async (chatId) => {
|
const deleteChatHandler = async (chatId) => {
|
||||||
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
|
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
chats = await getChatListByUserId(localStorage.token, user.id);
|
chats = await getChatListByUserId(localStorage.token, user.id);
|
||||||
|
65
src/lib/components/app/AppSidebar.svelte
Normal file
65
src/lib/components/app/AppSidebar.svelte
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
|
|
||||||
|
let selected = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="min-w-[4.5rem] bg-gray-50 dark:bg-gray-950 flex gap-2.5 flex-col pt-8">
|
||||||
|
<div class="flex justify-center relative">
|
||||||
|
{#if selected === 'home'}
|
||||||
|
<div class="absolute top-0 left-0 flex h-full">
|
||||||
|
<div class="my-auto rounded-r-lg w-1 h-8 bg-black dark:bg-white"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Tooltip content="Home" placement="right">
|
||||||
|
<button
|
||||||
|
class=" cursor-pointer {selected === 'home' ? 'rounded-2xl' : 'rounded-full'}"
|
||||||
|
on:click={() => {
|
||||||
|
selected = 'home';
|
||||||
|
|
||||||
|
if (window.electronAPI) {
|
||||||
|
window.electronAPI.load('home');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/static/splash.png"
|
||||||
|
class="size-11 dark:invert p-0.5"
|
||||||
|
alt="logo"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" -mt-1 border-[1.5px] border-gray-100 dark:border-gray-900 mx-4"></div>
|
||||||
|
|
||||||
|
<div class="flex justify-center relative group">
|
||||||
|
{#if selected === ''}
|
||||||
|
<div class="absolute top-0 left-0 flex h-full">
|
||||||
|
<div class="my-auto rounded-r-lg w-1 h-8 bg-black dark:bg-white"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class=" cursor-pointer bg-transparent"
|
||||||
|
on:click={() => {
|
||||||
|
selected = '';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/static/favicon.png"
|
||||||
|
class="size-10 {selected === '' ? 'rounded-2xl' : 'rounded-full'}"
|
||||||
|
alt="logo"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="flex justify-center relative group text-gray-400">
|
||||||
|
<button class=" cursor-pointer p-2" on:click={() => {}}>
|
||||||
|
<Plus className="size-4" strokeWidth="2" />
|
||||||
|
</button>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
const res = await sendMessage(localStorage.token, id, { content: content, data: data }).catch(
|
const res = await sendMessage(localStorage.token, id, { content: content, data: data }).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -199,7 +199,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="h-screen max-h-[100dvh] {$showSidebar
|
class="h-screen max-h-[100dvh] transition-width duration-200 ease-in-out {$showSidebar
|
||||||
? 'md:max-w-[calc(100%-260px)]'
|
? 'md:max-w-[calc(100%-260px)]'
|
||||||
: ''} w-full max-w-full flex flex-col"
|
: ''} w-full max-w-full flex flex-col"
|
||||||
id="channel-container"
|
id="channel-container"
|
||||||
@ -266,7 +266,7 @@
|
|||||||
threadId = null;
|
threadId = null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" {threadId !== null ? ' h-screen w-screen' : 'px-6 py-4'} h-full">
|
<div class=" {threadId !== null ? ' h-screen w-full' : 'px-6 py-4'} h-full">
|
||||||
<Thread
|
<Thread
|
||||||
{threadId}
|
{threadId}
|
||||||
{channel}
|
{channel}
|
||||||
|
@ -158,7 +158,7 @@
|
|||||||
// Check if the file is an audio file and transcribe/convert it to text file
|
// Check if the file is an audio file and transcribe/convert it to text file
|
||||||
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
||||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
const res = deleteMessage(localStorage.token, message.channel_id, message.id).catch(
|
const res = deleteMessage(localStorage.token, message.channel_id, message.id).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -118,7 +118,7 @@
|
|||||||
const res = updateMessage(localStorage.token, message.channel_id, message.id, {
|
const res = updateMessage(localStorage.token, message.channel_id, message.id, {
|
||||||
content: content
|
content: content
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@ -154,7 +154,7 @@
|
|||||||
message.id,
|
message.id,
|
||||||
name
|
name
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -180,7 +180,7 @@
|
|||||||
|
|
||||||
const res = addReaction(localStorage.token, message.channel_id, message.id, name).catch(
|
const res = addReaction(localStorage.token, message.channel_id, message.id, name).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
|
import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
|
||||||
import ReactionPicker from './Message/ReactionPicker.svelte';
|
import ReactionPicker from './Message/ReactionPicker.svelte';
|
||||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||||
|
import { formatDate } from '$lib/utils';
|
||||||
|
|
||||||
export let message;
|
export let message;
|
||||||
export let showUserProfile = true;
|
export let showUserProfile = true;
|
||||||
@ -45,19 +46,6 @@
|
|||||||
let edit = false;
|
let edit = false;
|
||||||
let editedContent = null;
|
let editedContent = null;
|
||||||
let showDeleteConfirmDialog = false;
|
let showDeleteConfirmDialog = false;
|
||||||
|
|
||||||
const formatDate = (inputDate) => {
|
|
||||||
const date = dayjs(inputDate);
|
|
||||||
const now = dayjs();
|
|
||||||
|
|
||||||
if (date.isToday()) {
|
|
||||||
return `Today at ${date.format('HH:mm')}`;
|
|
||||||
} else if (date.isYesterday()) {
|
|
||||||
return `Yesterday at ${date.format('HH:mm')}`;
|
|
||||||
} else {
|
|
||||||
return `${date.format('DD/MM/YYYY')} at ${date.format('HH:mm')}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
export let channel;
|
export let channel;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center">
|
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
|
||||||
<div
|
<div
|
||||||
class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
|
class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
|
||||||
></div>
|
></div>
|
||||||
@ -83,4 +83,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
|
@ -128,7 +128,7 @@
|
|||||||
content: content,
|
content: content,
|
||||||
data: data
|
data: data
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -44,7 +44,8 @@
|
|||||||
extractSentencesForAudio,
|
extractSentencesForAudio,
|
||||||
promptTemplate,
|
promptTemplate,
|
||||||
splitStream,
|
splitStream,
|
||||||
sleep
|
sleep,
|
||||||
|
removeDetailsWithReasoning
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
|
|
||||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
import { generateChatCompletion } from '$lib/apis/ollama';
|
||||||
@ -111,6 +112,7 @@
|
|||||||
$: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
|
$: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
|
||||||
|
|
||||||
let selectedToolIds = [];
|
let selectedToolIds = [];
|
||||||
|
let imageGenerationEnabled = false;
|
||||||
let webSearchEnabled = false;
|
let webSearchEnabled = false;
|
||||||
|
|
||||||
let chat = null;
|
let chat = null;
|
||||||
@ -137,6 +139,7 @@
|
|||||||
files = [];
|
files = [];
|
||||||
selectedToolIds = [];
|
selectedToolIds = [];
|
||||||
webSearchEnabled = false;
|
webSearchEnabled = false;
|
||||||
|
imageGenerationEnabled = false;
|
||||||
|
|
||||||
loaded = false;
|
loaded = false;
|
||||||
|
|
||||||
@ -152,6 +155,7 @@
|
|||||||
files = input.files;
|
files = input.files;
|
||||||
selectedToolIds = input.selectedToolIds;
|
selectedToolIds = input.selectedToolIds;
|
||||||
webSearchEnabled = input.webSearchEnabled;
|
webSearchEnabled = input.webSearchEnabled;
|
||||||
|
imageGenerationEnabled = input.imageGenerationEnabled;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,6 +322,19 @@
|
|||||||
eventConfirmationMessage = data.message;
|
eventConfirmationMessage = data.message;
|
||||||
eventConfirmationInputPlaceholder = data.placeholder;
|
eventConfirmationInputPlaceholder = data.placeholder;
|
||||||
eventConfirmationInputValue = data?.value ?? '';
|
eventConfirmationInputValue = data?.value ?? '';
|
||||||
|
} else if (type === 'notification') {
|
||||||
|
const toastType = data?.type ?? 'info';
|
||||||
|
const toastContent = data?.content ?? '';
|
||||||
|
|
||||||
|
if (toastType === 'success') {
|
||||||
|
toast.success(toastContent);
|
||||||
|
} else if (toastType === 'error') {
|
||||||
|
toast.error(toastContent);
|
||||||
|
} else if (toastType === 'warning') {
|
||||||
|
toast.warning(toastContent);
|
||||||
|
} else {
|
||||||
|
toast.info(toastContent);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Unknown message type', data);
|
console.log('Unknown message type', data);
|
||||||
}
|
}
|
||||||
@ -390,11 +407,13 @@
|
|||||||
files = input.files;
|
files = input.files;
|
||||||
selectedToolIds = input.selectedToolIds;
|
selectedToolIds = input.selectedToolIds;
|
||||||
webSearchEnabled = input.webSearchEnabled;
|
webSearchEnabled = input.webSearchEnabled;
|
||||||
|
imageGenerationEnabled = input.imageGenerationEnabled;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
prompt = '';
|
prompt = '';
|
||||||
files = [];
|
files = [];
|
||||||
selectedToolIds = [];
|
selectedToolIds = [];
|
||||||
webSearchEnabled = false;
|
webSearchEnabled = false;
|
||||||
|
imageGenerationEnabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,6 +715,9 @@
|
|||||||
if ($page.url.searchParams.get('web-search') === 'true') {
|
if ($page.url.searchParams.get('web-search') === 'true') {
|
||||||
webSearchEnabled = true;
|
webSearchEnabled = true;
|
||||||
}
|
}
|
||||||
|
if ($page.url.searchParams.get('image-generation') === 'true') {
|
||||||
|
imageGenerationEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
if ($page.url.searchParams.get('tools')) {
|
if ($page.url.searchParams.get('tools')) {
|
||||||
selectedToolIds = $page.url.searchParams
|
selectedToolIds = $page.url.searchParams
|
||||||
@ -830,7 +852,7 @@
|
|||||||
session_id: $socket?.id,
|
session_id: $socket?.id,
|
||||||
id: responseMessageId
|
id: responseMessageId
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
messages.at(-1).error = { content: error };
|
messages.at(-1).error = { content: error };
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -888,7 +910,7 @@
|
|||||||
session_id: $socket?.id,
|
session_id: $socket?.id,
|
||||||
id: responseMessageId
|
id: responseMessageId
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
messages.at(-1).error = { content: error };
|
messages.at(-1).error = { content: error };
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@ -1396,7 +1418,7 @@
|
|||||||
if ($settings?.memory ?? false) {
|
if ($settings?.memory ?? false) {
|
||||||
if (userContext === null) {
|
if (userContext === null) {
|
||||||
const res = await queryMemory(localStorage.token, prompt).catch((error) => {
|
const res = await queryMemory(localStorage.token, prompt).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -1482,7 +1504,10 @@
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
...createMessagesList(responseMessageId)
|
...createMessagesList(responseMessageId).map((message) => ({
|
||||||
|
...message,
|
||||||
|
content: removeDetailsWithReasoning(message.content)
|
||||||
|
}))
|
||||||
]
|
]
|
||||||
.filter((message) => message?.content?.trim())
|
.filter((message) => message?.content?.trim())
|
||||||
.map((message, idx, arr) => ({
|
.map((message, idx, arr) => ({
|
||||||
@ -1533,6 +1558,7 @@
|
|||||||
files: (files?.length ?? 0) > 0 ? files : undefined,
|
files: (files?.length ?? 0) > 0 ? files : undefined,
|
||||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||||
features: {
|
features: {
|
||||||
|
image_generation: imageGenerationEnabled,
|
||||||
web_search: webSearchEnabled
|
web_search: webSearchEnabled
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1829,13 +1855,13 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if !chatIdProp || (loaded && chatIdProp)}
|
<div
|
||||||
<div
|
class="h-screen max-h-[100dvh] transition-width duration-200 ease-in-out {$showSidebar
|
||||||
class="h-screen max-h-[100dvh] {$showSidebar
|
? ' md:max-w-[calc(100%-260px)]'
|
||||||
? 'md:max-w-[calc(100%-260px)]'
|
: ' '} w-full max-w-full flex flex-col"
|
||||||
: ''} w-full max-w-full flex flex-col"
|
id="chat-container"
|
||||||
id="chat-container"
|
>
|
||||||
>
|
{#if !chatIdProp || (loaded && chatIdProp)}
|
||||||
{#if $settings?.backgroundImageUrl ?? null}
|
{#if $settings?.backgroundImageUrl ?? null}
|
||||||
<div
|
<div
|
||||||
class="absolute {$showSidebar
|
class="absolute {$showSidebar
|
||||||
@ -1935,6 +1961,7 @@
|
|||||||
bind:prompt
|
bind:prompt
|
||||||
bind:autoScroll
|
bind:autoScroll
|
||||||
bind:selectedToolIds
|
bind:selectedToolIds
|
||||||
|
bind:imageGenerationEnabled
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
bind:atSelectedModel
|
bind:atSelectedModel
|
||||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||||
@ -1985,6 +2012,7 @@
|
|||||||
bind:prompt
|
bind:prompt
|
||||||
bind:autoScroll
|
bind:autoScroll
|
||||||
bind:selectedToolIds
|
bind:selectedToolIds
|
||||||
|
bind:imageGenerationEnabled
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
bind:atSelectedModel
|
bind:atSelectedModel
|
||||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||||
@ -2037,5 +2065,5 @@
|
|||||||
{eventTarget}
|
{eventTarget}
|
||||||
/>
|
/>
|
||||||
</PaneGroup>
|
</PaneGroup>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
@ -146,7 +146,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class=" {$showCallOverlay || $showOverview || $showArtifacts
|
class=" {$showCallOverlay || $showOverview || $showArtifacts
|
||||||
? ' h-screen w-screen'
|
? ' h-screen w-full'
|
||||||
: 'px-6 py-4'} h-full"
|
: 'px-6 py-4'} h-full"
|
||||||
>
|
>
|
||||||
{#if $showCallOverlay}
|
{#if $showCallOverlay}
|
||||||
|
@ -30,64 +30,70 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" dark:text-gray-200 text-sm font-primary py-0.5 px-0.5">
|
{#if $user.role === 'admin' || $user?.permissions.chat?.controls}
|
||||||
{#if chatFiles.length > 0}
|
<div class=" dark:text-gray-200 text-sm font-primary py-0.5 px-0.5">
|
||||||
<Collapsible title={$i18n.t('Files')} open={true} buttonClassName="w-full">
|
{#if chatFiles.length > 0}
|
||||||
<div class="flex flex-col gap-1 mt-1.5" slot="content">
|
<Collapsible title={$i18n.t('Files')} open={true} buttonClassName="w-full">
|
||||||
{#each chatFiles as file, fileIdx}
|
<div class="flex flex-col gap-1 mt-1.5" slot="content">
|
||||||
<FileItem
|
{#each chatFiles as file, fileIdx}
|
||||||
className="w-full"
|
<FileItem
|
||||||
item={file}
|
className="w-full"
|
||||||
edit={true}
|
item={file}
|
||||||
url={file?.url ? file.url : null}
|
edit={true}
|
||||||
name={file.name}
|
url={file?.url ? file.url : null}
|
||||||
type={file.type}
|
name={file.name}
|
||||||
size={file?.size}
|
type={file.type}
|
||||||
dismissible={true}
|
size={file?.size}
|
||||||
on:dismiss={() => {
|
dismissible={true}
|
||||||
// Remove the file from the chatFiles array
|
on:dismiss={() => {
|
||||||
|
// Remove the file from the chatFiles array
|
||||||
|
|
||||||
chatFiles.splice(fileIdx, 1);
|
chatFiles.splice(fileIdx, 1);
|
||||||
chatFiles = chatFiles;
|
chatFiles = chatFiles;
|
||||||
}}
|
}}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
console.log(file);
|
console.log(file);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Collapsible bind:open={showValves} title={$i18n.t('Valves')} buttonClassName="w-full">
|
||||||
|
<div class="text-sm" slot="content">
|
||||||
|
<Valves show={showValves} />
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Collapsible bind:open={showValves} title={$i18n.t('Valves')} buttonClassName="w-full">
|
<Collapsible title={$i18n.t('System Prompt')} open={true} buttonClassName="w-full">
|
||||||
<div class="text-sm" slot="content">
|
<div class="" slot="content">
|
||||||
<Valves show={showValves} />
|
<textarea
|
||||||
</div>
|
bind:value={params.system}
|
||||||
</Collapsible>
|
class="w-full text-xs py-1.5 bg-transparent outline-none resize-none"
|
||||||
|
rows="4"
|
||||||
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
placeholder={$i18n.t('Enter system prompt')}
|
||||||
|
/>
|
||||||
<Collapsible title={$i18n.t('System Prompt')} open={true} buttonClassName="w-full">
|
|
||||||
<div class="" slot="content">
|
|
||||||
<textarea
|
|
||||||
bind:value={params.system}
|
|
||||||
class="w-full text-xs py-1.5 bg-transparent outline-none resize-none"
|
|
||||||
rows="4"
|
|
||||||
placeholder={$i18n.t('Enter system prompt')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
|
||||||
|
|
||||||
<Collapsible title={$i18n.t('Advanced Params')} open={true} buttonClassName="w-full">
|
|
||||||
<div class="text-sm mt-1.5" slot="content">
|
|
||||||
<div>
|
|
||||||
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Collapsible>
|
||||||
</Collapsible>
|
|
||||||
</div>
|
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||||
|
|
||||||
|
<Collapsible title={$i18n.t('Advanced Params')} open={true} buttonClassName="w-full">
|
||||||
|
<div class="text-sm mt-1.5" slot="content">
|
||||||
|
<div>
|
||||||
|
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-sm dark:text-gray-300 text-center py-2 px-10">
|
||||||
|
{$i18n.t('You do not have permission to access this feature.')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,7 +82,7 @@
|
|||||||
if (tab === 'tools') {
|
if (tab === 'tools') {
|
||||||
const res = await updateToolUserValvesById(localStorage.token, selectedId, valves).catch(
|
const res = await updateToolUserValvesById(localStorage.token, selectedId, valves).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -97,7 +97,7 @@
|
|||||||
selectedId,
|
selectedId,
|
||||||
valves
|
valves
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
import { generateAutoCompletion } from '$lib/apis';
|
import { generateAutoCompletion } from '$lib/apis';
|
||||||
import { error, text } from '@sveltejs/kit';
|
import { error, text } from '@sveltejs/kit';
|
||||||
import Image from '../common/Image.svelte';
|
import Image from '../common/Image.svelte';
|
||||||
|
import { deleteFileById } from '$lib/apis/files';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@ -61,12 +62,15 @@
|
|||||||
export let files = [];
|
export let files = [];
|
||||||
|
|
||||||
export let selectedToolIds = [];
|
export let selectedToolIds = [];
|
||||||
|
|
||||||
|
export let imageGenerationEnabled = false;
|
||||||
export let webSearchEnabled = false;
|
export let webSearchEnabled = false;
|
||||||
|
|
||||||
$: onChange({
|
$: onChange({
|
||||||
prompt,
|
prompt,
|
||||||
files,
|
files,
|
||||||
selectedToolIds,
|
selectedToolIds,
|
||||||
|
imageGenerationEnabled,
|
||||||
webSearchEnabled
|
webSearchEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -165,7 +169,7 @@
|
|||||||
// Check if the file is an audio file and transcribe/convert it to text file
|
// Check if the file is an audio file and transcribe/convert it to text file
|
||||||
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
||||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -381,7 +385,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled}
|
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || imageGenerationEnabled}
|
||||||
<div
|
<div
|
||||||
class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white dark:from-gray-900 z-10"
|
class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white dark:from-gray-900 z-10"
|
||||||
>
|
>
|
||||||
@ -396,7 +400,7 @@
|
|||||||
<span class="relative inline-flex rounded-full size-2 bg-yellow-500" />
|
<span class="relative inline-flex rounded-full size-2 bg-yellow-500" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class=" translate-y-[0.5px] text-ellipsis line-clamp-1 flex">
|
<div class=" text-ellipsis line-clamp-1 flex">
|
||||||
{#each selectedToolIds.map((id) => {
|
{#each selectedToolIds.map((id) => {
|
||||||
return $tools ? $tools.find((t) => t.id === id) : { id: id, name: id };
|
return $tools ? $tools.find((t) => t.id === id) : { id: id, name: id };
|
||||||
}) as tool, toolIdx (toolIdx)}
|
}) as tool, toolIdx (toolIdx)}
|
||||||
@ -417,6 +421,22 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if imageGenerationEnabled}
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
|
||||||
|
<div class="pl-1">
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
<span
|
||||||
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||||
|
/>
|
||||||
|
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class=" ">{$i18n.t('Image generation')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if webSearchEnabled}
|
{#if webSearchEnabled}
|
||||||
<div class="flex items-center justify-between w-full">
|
<div class="flex items-center justify-between w-full">
|
||||||
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
|
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
|
||||||
@ -428,7 +448,7 @@
|
|||||||
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class=" translate-y-[0.5px]">{$i18n.t('Search the web')}</div>
|
<div class=" ">{$i18n.t('Search the web')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -548,15 +568,15 @@
|
|||||||
dir={$settings?.chatDirection ?? 'LTR'}
|
dir={$settings?.chatDirection ?? 'LTR'}
|
||||||
>
|
>
|
||||||
{#if files.length > 0}
|
{#if files.length > 0}
|
||||||
<div class="mx-1 mt-2.5 mb-1 flex flex-wrap gap-2">
|
<div class="mx-1 mt-2.5 mb-1 flex items-center flex-wrap gap-2">
|
||||||
{#each files as file, fileIdx}
|
{#each files as file, fileIdx}
|
||||||
{#if file.type === 'image'}
|
{#if file.type === 'image'}
|
||||||
<div class=" relative group">
|
<div class=" relative group">
|
||||||
<div class="relative">
|
<div class="relative flex items-center">
|
||||||
<Image
|
<Image
|
||||||
src={file.url}
|
src={file.url}
|
||||||
alt="input"
|
alt="input"
|
||||||
imageClassName=" h-16 w-16 rounded-xl object-cover"
|
imageClassName=" size-14 rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
|
{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -615,7 +635,15 @@
|
|||||||
loading={file.status === 'uploading'}
|
loading={file.status === 'uploading'}
|
||||||
dismissible={true}
|
dismissible={true}
|
||||||
edit={true}
|
edit={true}
|
||||||
on:dismiss={() => {
|
on:dismiss={async () => {
|
||||||
|
if (file.type !== 'collection' && !file?.collection) {
|
||||||
|
if (file.id) {
|
||||||
|
// This will handle both file deletion and Chroma cleanup
|
||||||
|
await deleteFileById(localStorage.token, file.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from UI state
|
||||||
files.splice(fileIdx, 1);
|
files.splice(fileIdx, 1);
|
||||||
files = files;
|
files = files;
|
||||||
}}
|
}}
|
||||||
@ -631,6 +659,7 @@
|
|||||||
<div class=" flex">
|
<div class=" flex">
|
||||||
<div class="ml-1 self-end mb-1.5 flex space-x-1">
|
<div class="ml-1 self-end mb-1.5 flex space-x-1">
|
||||||
<InputMenu
|
<InputMenu
|
||||||
|
bind:imageGenerationEnabled
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
bind:selectedToolIds
|
bind:selectedToolIds
|
||||||
{screenCaptureHandler}
|
{screenCaptureHandler}
|
||||||
@ -839,6 +868,7 @@
|
|||||||
atSelectedModel = undefined;
|
atSelectedModel = undefined;
|
||||||
selectedToolIds = [];
|
selectedToolIds = [];
|
||||||
webSearchEnabled = false;
|
webSearchEnabled = false;
|
||||||
|
imageGenerationEnabled = false;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:paste={async (e) => {
|
on:paste={async (e) => {
|
||||||
@ -1025,6 +1055,7 @@
|
|||||||
atSelectedModel = undefined;
|
atSelectedModel = undefined;
|
||||||
selectedToolIds = [];
|
selectedToolIds = [];
|
||||||
webSearchEnabled = false;
|
webSearchEnabled = false;
|
||||||
|
imageGenerationEnabled = false;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
rows="1"
|
rows="1"
|
||||||
|
@ -153,7 +153,7 @@
|
|||||||
const file = blobToFile(audioBlob, 'recording.wav');
|
const file = blobToFile(audioBlob, 'recording.wav');
|
||||||
|
|
||||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -78,6 +78,10 @@
|
|||||||
}}
|
}}
|
||||||
on:select={(e) => {
|
on:select={(e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
if (files.find((f) => f.id === e.detail.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
files = [
|
files = [
|
||||||
...files,
|
...files,
|
||||||
{
|
{
|
||||||
|
@ -127,7 +127,7 @@
|
|||||||
...a,
|
...a,
|
||||||
...(item?.files ?? []).map((file) => ({
|
...(item?.files ?? []).map((file) => ({
|
||||||
...file,
|
...file,
|
||||||
collection: { name: item.name, description: item.description }
|
collection: { name: item.name, description: item.description } // DO NOT REMOVE, USED IN FILE DESCRIPTION/ATTACHMENT
|
||||||
}))
|
}))
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
||||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||||
import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
|
import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
|
||||||
|
import PhotoSolid from '$lib/components/icons/PhotoSolid.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@ -24,11 +25,25 @@
|
|||||||
export let selectedToolIds: string[] = [];
|
export let selectedToolIds: string[] = [];
|
||||||
|
|
||||||
export let webSearchEnabled: boolean;
|
export let webSearchEnabled: boolean;
|
||||||
|
export let imageGenerationEnabled: boolean;
|
||||||
|
|
||||||
export let onClose: Function;
|
export let onClose: Function;
|
||||||
|
|
||||||
let tools = {};
|
let tools = {};
|
||||||
let show = false;
|
let show = false;
|
||||||
|
|
||||||
|
let showImageGeneration = false;
|
||||||
|
|
||||||
|
$: showImageGeneration =
|
||||||
|
$config?.features?.enable_image_generation &&
|
||||||
|
($user.role === 'admin' || $user?.permissions?.features?.image_generation);
|
||||||
|
|
||||||
|
let showWebSearch = false;
|
||||||
|
|
||||||
|
$: showWebSearch =
|
||||||
|
$config?.features?.enable_web_search &&
|
||||||
|
($user.role === 'admin' || $user?.permissions?.features?.web_search);
|
||||||
|
|
||||||
$: if (show) {
|
$: if (show) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
@ -63,7 +78,7 @@
|
|||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
class="w-full max-w-[220px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||||
sideOffset={15}
|
sideOffset={15}
|
||||||
alignOffset={-8}
|
alignOffset={-8}
|
||||||
side="top"
|
side="top"
|
||||||
@ -114,7 +129,23 @@
|
|||||||
<hr class="border-black/5 dark:border-white/5 my-1" />
|
<hr class="border-black/5 dark:border-white/5 my-1" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $config?.features?.enable_web_search}
|
{#if showImageGeneration}
|
||||||
|
<button
|
||||||
|
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||||
|
on:click={() => {
|
||||||
|
imageGenerationEnabled = !imageGenerationEnabled;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex-1 flex items-center gap-2">
|
||||||
|
<PhotoSolid />
|
||||||
|
<div class=" line-clamp-1">{$i18n.t('Image')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch state={imageGenerationEnabled} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showWebSearch}
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@ -128,7 +159,9 @@
|
|||||||
|
|
||||||
<Switch state={webSearchEnabled} />
|
<Switch state={webSearchEnabled} />
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showImageGeneration || showWebSearch}
|
||||||
<hr class="border-black/5 dark:border-white/5 my-1" />
|
<hr class="border-black/5 dark:border-white/5 my-1" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@
|
|||||||
const file = blobToFile(audioBlob, 'recording.wav');
|
const file = blobToFile(audioBlob, 'recording.wav');
|
||||||
|
|
||||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if token.type === 'details'}
|
{:else if token.type === 'details'}
|
||||||
<Collapsible title={token.summary} className="w-fit space-y-1">
|
<Collapsible title={token.summary} attributes={token?.attributes} className="w-fit space-y-1">
|
||||||
<div class=" mb-1.5" slot="content">
|
<div class=" mb-1.5" slot="content">
|
||||||
<svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
|
<svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
<div class=" self-center font-semibold mb-0.5 line-clamp-1 flex gap-1 items-center">
|
<div class=" self-center font-semibold line-clamp-1 flex gap-1 items-center">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
approximateToHumanReadable,
|
approximateToHumanReadable,
|
||||||
getMessageContentParts,
|
getMessageContentParts,
|
||||||
sanitizeResponseContent,
|
sanitizeResponseContent,
|
||||||
createMessagesList
|
createMessagesList,
|
||||||
|
formatDate
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
@ -229,7 +230,7 @@
|
|||||||
sentence
|
sentence
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
|
|
||||||
speaking = false;
|
speaking = false;
|
||||||
loadingSpeech = false;
|
loadingSpeech = false;
|
||||||
@ -321,7 +322,7 @@
|
|||||||
const generateImage = async (message: MessageType) => {
|
const generateImage = async (message: MessageType) => {
|
||||||
generatingImage = true;
|
generatingImage = true;
|
||||||
const res = await imageGenerations(localStorage.token, message.content).catch((error) => {
|
const res = await imageGenerations(localStorage.token, message.content).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
console.log(res);
|
console.log(res);
|
||||||
|
|
||||||
@ -356,7 +357,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const chat = await getChatById(localStorage.token, chatId).catch((error) => {
|
const chat = await getChatById(localStorage.token, chatId).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
return;
|
return;
|
||||||
@ -411,11 +412,11 @@
|
|||||||
message.feedbackId,
|
message.feedbackId,
|
||||||
feedbackItem
|
feedbackItem
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
feedback = await createNewFeedback(localStorage.token, feedbackItem).catch((error) => {
|
feedback = await createNewFeedback(localStorage.token, feedbackItem).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (feedback) {
|
if (feedback) {
|
||||||
@ -451,7 +452,7 @@
|
|||||||
updatedMessage.feedbackId,
|
updatedMessage.feedbackId,
|
||||||
feedbackItem
|
feedbackItem
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -496,11 +497,13 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
<span
|
<div
|
||||||
class=" self-center shrink-0 translate-y-0.5 invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
|
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
|
||||||
>
|
>
|
||||||
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
|
<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
|
||||||
</span>
|
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Name>
|
</Name>
|
||||||
|
|
||||||
@ -917,7 +920,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if $config?.features.enable_image_generation && !readOnly}
|
{#if $config?.features.enable_image_generation && ($user.role === 'admin' || $user?.permissions?.features?.image_generation) && !readOnly}
|
||||||
<Tooltip content={$i18n.t('Generate Image')} placement="bottom">
|
<Tooltip content={$i18n.t('Generate Image')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
@ -1180,20 +1183,22 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
actionMessage(action.id, message);
|
actionMessage(action.id, message);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if action.icon_url}
|
{#if action.icon_url}
|
||||||
<img
|
<div class="size-4">
|
||||||
src={action.icon_url}
|
<img
|
||||||
class="w-4 h-4 {action.icon_url.includes('svg')
|
src={action.icon_url}
|
||||||
? 'dark:invert-[80%]'
|
class="w-4 h-4 {action.icon_url.includes('svg')
|
||||||
: ''}"
|
? 'dark:invert-[80%]'
|
||||||
style="fill: currentColor;"
|
: ''}"
|
||||||
alt={action.name}
|
style="fill: currentColor;"
|
||||||
/>
|
alt={action.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Sparkles strokeWidth="2.1" className="size-4" />
|
<Sparkles strokeWidth="2.1" className="size-4" />
|
||||||
{/if}
|
{/if}
|
||||||
@ -1232,48 +1237,4 @@
|
|||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shimmer {
|
|
||||||
background: linear-gradient(90deg, #9a9b9e 25%, #2a2929 50%, #9a9b9e 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
animation: shimmer 4s linear infinite;
|
|
||||||
color: #818286; /* Fallback color */
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .shimmer {
|
|
||||||
background: linear-gradient(90deg, #818286 25%, #eae5e5 50%, #818286 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
animation: shimmer 4s linear infinite;
|
|
||||||
color: #a1a3a7; /* Darker fallback color for dark mode */
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes smoothFadeIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-description {
|
|
||||||
animation: smoothFadeIn 0.2s forwards;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { models, settings } from '$lib/stores';
|
import { models, settings } from '$lib/stores';
|
||||||
import { user as _user } from '$lib/stores';
|
import { user as _user } from '$lib/stores';
|
||||||
import { copyToClipboard as _copyToClipboard } from '$lib/utils';
|
import { copyToClipboard as _copyToClipboard, formatDate } from '$lib/utils';
|
||||||
|
|
||||||
import Name from './Name.svelte';
|
import Name from './Name.svelte';
|
||||||
import ProfileImage from './ProfileImage.svelte';
|
import ProfileImage from './ProfileImage.svelte';
|
||||||
@ -109,11 +109,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
<span
|
<div
|
||||||
class=" invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
|
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
|
||||||
>
|
>
|
||||||
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
|
<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
|
||||||
</span>
|
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Name>
|
</Name>
|
||||||
</div>
|
</div>
|
||||||
|
@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
|
const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -173,7 +173,7 @@
|
|||||||
error = error.message;
|
error = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
// opts.callback({ success: false, error, modelName: opts.modelName });
|
// opts.callback({ success: false, error, modelName: opts.modelName });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
|
<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
|
||||||
|
|
||||||
<div class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center">
|
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
|
||||||
<div
|
<div
|
||||||
class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
|
class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
|
||||||
></div>
|
></div>
|
||||||
@ -114,7 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</Menu>
|
</Menu>
|
||||||
{:else if $mobile}
|
{:else if $mobile && ($user.role === 'admin' || $user?.permissions.chat?.controls)}
|
||||||
<Tooltip content={$i18n.t('Controls')}>
|
<Tooltip content={$i18n.t('Controls')}>
|
||||||
<button
|
<button
|
||||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||||
@ -130,7 +130,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !$mobile}
|
{#if !$mobile && ($user.role === 'admin' || $user?.permissions.chat?.controls)}
|
||||||
<Tooltip content={$i18n.t('Controls')}>
|
<Tooltip content={$i18n.t('Controls')}>
|
||||||
<button
|
<button
|
||||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||||
@ -191,4 +191,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
export let files = [];
|
export let files = [];
|
||||||
|
|
||||||
export let selectedToolIds = [];
|
export let selectedToolIds = [];
|
||||||
|
export let imageGenerationEnabled = false;
|
||||||
export let webSearchEnabled = false;
|
export let webSearchEnabled = false;
|
||||||
|
|
||||||
let models = [];
|
let models = [];
|
||||||
@ -194,6 +195,7 @@
|
|||||||
bind:prompt
|
bind:prompt
|
||||||
bind:autoScroll
|
bind:autoScroll
|
||||||
bind:selectedToolIds
|
bind:selectedToolIds
|
||||||
|
bind:imageGenerationEnabled
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
bind:atSelectedModel
|
bind:atSelectedModel
|
||||||
{transparentBackground}
|
{transparentBackground}
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
if (newPassword === newPasswordConfirm) {
|
if (newPassword === newPasswordConfirm) {
|
||||||
const res = await updateUserPassword(localStorage.token, currentPassword, newPassword).catch(
|
const res = await updateUserPassword(localStorage.token, currentPassword, newPassword).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
seed: null,
|
seed: null,
|
||||||
stop: null,
|
stop: null,
|
||||||
temperature: null,
|
temperature: null,
|
||||||
|
reasoning_effort: null,
|
||||||
frequency_penalty: null,
|
frequency_penalty: null,
|
||||||
repeat_last_n: null,
|
repeat_last_n: null,
|
||||||
mirostat: null,
|
mirostat: null,
|
||||||
@ -158,7 +159,7 @@
|
|||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-1 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter stop sequence')}
|
placeholder={$i18n.t('Enter stop sequence')}
|
||||||
bind:value={params.stop}
|
bind:value={params.stop}
|
||||||
@ -224,6 +225,49 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" py-0.5 w-full justify-between">
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t(
|
||||||
|
'Constrains effort on reasoning for reasoning models. Only applicable to reasoning models from specific providers that support reasoning effort. (Default: medium)'
|
||||||
|
)}
|
||||||
|
placement="top-start"
|
||||||
|
className="inline-tooltip"
|
||||||
|
>
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Reasoning Effort')}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
params.reasoning_effort = (params?.reasoning_effort ?? null) === null ? 'medium' : null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if (params?.reasoning_effort ?? null) === null}
|
||||||
|
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{#if (params?.reasoning_effort ?? null) !== null}
|
||||||
|
<div class="flex mt-0.5 space-x-2">
|
||||||
|
<div class=" flex-1">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-1 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type="text"
|
||||||
|
placeholder={$i18n.t('Enter reasoning effort')}
|
||||||
|
bind:value={params.reasoning_effort}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t(
|
content={$i18n.t(
|
||||||
@ -1086,7 +1130,7 @@
|
|||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t(
|
content={$i18n.t(
|
||||||
'Set the number of GPU devices used for computation. This option controls how many GPU devices (if available) are used to process incoming requests. Increasing this value can significantly improve performance for models that are optimized for GPU acceleration but may also consume more power and GPU resources.'
|
'Set the number of layers, which will be off-loaded to GPU. Increasing this value can significantly improve performance for models that are optimized for GPU acceleration but may also consume more power and GPU resources.'
|
||||||
)}
|
)}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
className="inline-tooltip"
|
className="inline-tooltip"
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
const archiveAllChatsHandler = async () => {
|
const archiveAllChatsHandler = async () => {
|
||||||
await goto('/');
|
await goto('/');
|
||||||
await archiveAllChats(localStorage.token).catch((error) => {
|
await archiveAllChats(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
currentChatPage.set(1);
|
currentChatPage.set(1);
|
||||||
@ -88,7 +88,7 @@
|
|||||||
const deleteAllChatsHandler = async () => {
|
const deleteAllChatsHandler = async () => {
|
||||||
await goto('/');
|
await goto('/');
|
||||||
await deleteAllChats(localStorage.token).catch((error) => {
|
await deleteAllChats(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
currentChatPage.set(1);
|
currentChatPage.set(1);
|
||||||
|
@ -232,78 +232,79 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-3" />
|
{#if $user.role === 'admin' || $user?.permissions.chat?.controls}
|
||||||
|
<hr class=" dark:border-gray-850 my-3" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={system}
|
bind:value={system}
|
||||||
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
rows="4"
|
rows="4"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2 space-y-3 pr-1.5">
|
|
||||||
<div class="flex justify-between items-center text-sm">
|
|
||||||
<div class=" font-medium">{$i18n.t('Advanced Parameters')}</div>
|
|
||||||
<button
|
|
||||||
class=" text-xs font-medium text-gray-500"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
showAdvanced = !showAdvanced;
|
|
||||||
}}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showAdvanced}
|
<div class="mt-2 space-y-3 pr-1.5">
|
||||||
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
|
<div class="flex justify-between items-center text-sm">
|
||||||
<hr class=" dark:border-gray-850" />
|
<div class=" font-medium">{$i18n.t('Advanced Parameters')}</div>
|
||||||
|
<button
|
||||||
<div class=" py-1 w-full justify-between">
|
class=" text-xs font-medium text-gray-500"
|
||||||
<div class="flex w-full justify-between">
|
type="button"
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
|
on:click={() => {
|
||||||
|
showAdvanced = !showAdvanced;
|
||||||
<button
|
}}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
>
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
keepAlive = keepAlive === null ? '5m' : null;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if keepAlive === null}
|
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
|
||||||
{:else}
|
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if keepAlive !== null}
|
|
||||||
<div class="flex mt-1 space-x-2">
|
|
||||||
<input
|
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
|
||||||
type="text"
|
|
||||||
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
|
||||||
bind:value={keepAlive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{#if showAdvanced}
|
||||||
<div class=" py-1 flex w-full justify-between">
|
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<button
|
<div class=" py-1 w-full justify-between">
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
<div class="flex w-full justify-between">
|
||||||
on:click={() => {
|
<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
|
||||||
toggleRequestFormat();
|
|
||||||
}}
|
<button
|
||||||
>
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
{#if requestFormat === ''}
|
type="button"
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
on:click={() => {
|
||||||
{:else if requestFormat === 'json'}
|
keepAlive = keepAlive === null ? '5m' : null;
|
||||||
<!-- <svg
|
}}
|
||||||
|
>
|
||||||
|
{#if keepAlive === null}
|
||||||
|
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if keepAlive !== null}
|
||||||
|
<div class="flex mt-1 space-x-2">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type="text"
|
||||||
|
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
||||||
|
bind:value={keepAlive}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-1 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
on:click={() => {
|
||||||
|
toggleRequestFormat();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if requestFormat === ''}
|
||||||
|
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||||
|
{:else if requestFormat === 'json'}
|
||||||
|
<!-- <svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@ -313,13 +314,14 @@
|
|||||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||||
/>
|
/>
|
||||||
</svg> -->
|
</svg> -->
|
||||||
<span class="ml-2 self-center"> {$i18n.t('JSON')} </span>
|
<span class="ml-2 self-center"> {$i18n.t('JSON')} </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
const res = await addNewMemory(localStorage.token, content).catch((error) => {
|
const res = await addNewMemory(localStorage.token, content).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
const res = await updateMemoryById(localStorage.token, memory.id, content).catch((error) => {
|
const res = await updateMemoryById(localStorage.token, memory.id, content).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
@ -124,7 +124,7 @@
|
|||||||
localStorage.token,
|
localStorage.token,
|
||||||
memory.id
|
memory.id
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -177,7 +177,7 @@
|
|||||||
class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl"
|
class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
const res = await deleteMemoriesByUserId(localStorage.token).catch((error) => {
|
const res = await deleteMemoriesByUserId(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
const addTag = async (tagName) => {
|
const addTag = async (tagName) => {
|
||||||
const res = await addTagById(localStorage.token, chatId, tagName).catch(async (error) => {
|
const res = await addTagById(localStorage.token, chatId, tagName).catch(async (error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
if (!res) {
|
if (!res) {
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
export const formatPythonCodeHandler = async () => {
|
export const formatPythonCodeHandler = async () => {
|
||||||
if (codeEditor) {
|
if (codeEditor) {
|
||||||
const res = await formatPythonCode(_value).catch((error) => {
|
const res = await formatPythonCode(_value).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, createEventDispatcher } from 'svelte';
|
import { getContext, createEventDispatcher } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import dayjs from '$lib/dayjs';
|
||||||
|
import duration from 'dayjs/plugin/duration';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
|
dayjs.extend(duration);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
async function loadLocale(locales) {
|
||||||
|
for (const locale of locales) {
|
||||||
|
try {
|
||||||
|
dayjs.locale(locale);
|
||||||
|
break; // Stop after successfully loading the first available locale
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Could not load locale '${locale}':`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming $i18n.languages is an array of language codes
|
||||||
|
$: loadLocale($i18n.languages);
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
$: dispatch('change', open);
|
$: dispatch('change', open);
|
||||||
@ -9,12 +31,14 @@
|
|||||||
|
|
||||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
|
import Spinner from './Spinner.svelte';
|
||||||
|
|
||||||
export let open = false;
|
export let open = false;
|
||||||
export let className = '';
|
export let className = '';
|
||||||
export let buttonClassName =
|
export let buttonClassName =
|
||||||
'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
|
'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
|
||||||
export let title = null;
|
export let title = null;
|
||||||
|
export let attributes = null;
|
||||||
|
|
||||||
export let grow = false;
|
export let grow = false;
|
||||||
|
|
||||||
@ -34,12 +58,34 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" w-full font-medium flex items-center justify-between gap-2">
|
<div
|
||||||
|
class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
|
||||||
|
attributes?.done !== 'true'
|
||||||
|
? 'shimmer'
|
||||||
|
: ''}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{#if attributes?.done && attributes?.done !== 'true'}
|
||||||
|
<div>
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
{title}
|
{#if attributes?.type === 'reasoning'}
|
||||||
|
{#if attributes?.done === 'true' && attributes?.duration}
|
||||||
|
{$i18n.t('Thought for {{DURATION}}', {
|
||||||
|
DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
|
||||||
|
})}
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Thinking...')}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{title}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="flex self-center translate-y-[1px]">
|
||||||
{#if open}
|
{#if open}
|
||||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||||
{:else}
|
{:else}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user