Merge branch 'dev' into feat/trusted-email-header

This commit is contained in:
Jun Siang Cheah 2024-03-31 22:08:26 +01:00
commit 562e40a7bd
58 changed files with 2915 additions and 2152 deletions

View file

@ -1,27 +1,39 @@
name: Python CI name: Python CI
on: on:
push: push:
branches: ['main'] branches:
- main
- dev
pull_request: pull_request:
branches:
- main
- dev
jobs: jobs:
build: build:
name: 'Format Backend' name: 'Format Backend'
env:
PUBLIC_API_BASE_URL: ''
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: python-version: [3.11]
- latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use Python
uses: actions/setup-python@v4 - name: Set up Python
- name: Use Bun uses: actions/setup-python@v2
uses: oven-sh/setup-bun@v1 with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install yapf pip install black
- name: Format backend - name: Format backend
run: bun run format:backend run: black . --exclude "/venv/"
- name: Check for changes after format
run: git diff --exit-code

View file

@ -1,22 +1,36 @@
name: Bun CI name: Frontend Build
on: on:
push: push:
branches: ['main'] branches:
- main
- dev
pull_request: pull_request:
branches:
- main
- dev
jobs: jobs:
build: build:
name: 'Format & Build Frontend' name: 'Format & Build Frontend'
env:
PUBLIC_API_BASE_URL: ''
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Checkout Repository
- name: Use Bun uses: actions/checkout@v4
uses: oven-sh/setup-bun@v1
- run: bun --version - name: Setup Node.js
- name: Install frontend dependencies uses: actions/setup-node@v3
run: bun install with:
- name: Format frontend node-version: '20' # Or specify any other version you want to use
run: bun run format
- name: Build frontend - name: Install Dependencies
run: bun run build run: npm install
- name: Format Frontend
run: npm run format
- name: Check for Changes After Format
run: git diff --exit-code
- name: Build Frontend
run: npm run build

View file

@ -5,6 +5,24 @@ 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.1.116] - 2024-03-31
### Added
- **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations.
- **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding.
- **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams.
- **🌐 Localization**: Korean translation (I18n) now available.
- **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage.
- **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging.
### Fixed
- **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue.
- **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags.
- **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue.
- **✅ Spelling**: Corrected various spelling issues for improved readability.
## [0.1.115] - 2024-03-24 ## [0.1.115] - 2024-03-24
### Added ### Added

View file

@ -22,7 +22,13 @@ from utils.utils import (
) )
from utils.misc import calculate_sha256 from utils.misc import calculate_sha256
from config import SRC_LOG_LEVELS, CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
UPLOAD_DIR,
WHISPER_MODEL,
WHISPER_MODEL_DIR,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["AUDIO"]) log.setLevel(SRC_LOG_LEVELS["AUDIO"])

View file

@ -325,7 +325,7 @@ def save_url_image(url):
return image_id return image_id
except Exception as e: except Exception as e:
print(f"Error saving image: {e}") log.exception(f"Error saving image: {e}")
return None return None
@ -397,7 +397,7 @@ def generate_image(
user.id, user.id,
app.state.COMFYUI_BASE_URL, app.state.COMFYUI_BASE_URL,
) )
print(res) log.debug(f"res: {res}")
images = [] images = []
@ -409,7 +409,7 @@ def generate_image(
with open(file_body_path, "w") as f: with open(file_body_path, "w") as f:
json.dump(data.model_dump(exclude_none=True), f) json.dump(data.model_dump(exclude_none=True), f)
print(images) log.debug(f"images: {images}")
return images return images
else: else:
if form_data.model: if form_data.model:

View file

@ -4,6 +4,12 @@ import json
import urllib.request import urllib.request
import urllib.parse import urllib.parse
import random import random
import logging
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["COMFYUI"])
from pydantic import BaseModel from pydantic import BaseModel
@ -121,7 +127,7 @@ COMFYUI_DEFAULT_PROMPT = """
def queue_prompt(prompt, client_id, base_url): def queue_prompt(prompt, client_id, base_url):
print("queue_prompt") log.info("queue_prompt")
p = {"prompt": prompt, "client_id": client_id} p = {"prompt": prompt, "client_id": client_id}
data = json.dumps(p).encode("utf-8") data = json.dumps(p).encode("utf-8")
req = urllib.request.Request(f"{base_url}/prompt", data=data) req = urllib.request.Request(f"{base_url}/prompt", data=data)
@ -129,7 +135,7 @@ def queue_prompt(prompt, client_id, base_url):
def get_image(filename, subfolder, folder_type, base_url): def get_image(filename, subfolder, folder_type, base_url):
print("get_image") log.info("get_image")
data = {"filename": filename, "subfolder": subfolder, "type": folder_type} data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data) url_values = urllib.parse.urlencode(data)
with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response: with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response:
@ -137,14 +143,14 @@ def get_image(filename, subfolder, folder_type, base_url):
def get_image_url(filename, subfolder, folder_type, base_url): def get_image_url(filename, subfolder, folder_type, base_url):
print("get_image") log.info("get_image")
data = {"filename": filename, "subfolder": subfolder, "type": folder_type} data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data) url_values = urllib.parse.urlencode(data)
return f"{base_url}/view?{url_values}" return f"{base_url}/view?{url_values}"
def get_history(prompt_id, base_url): def get_history(prompt_id, base_url):
print("get_history") log.info("get_history")
with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response: with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response:
return json.loads(response.read()) return json.loads(response.read())
@ -212,15 +218,15 @@ def comfyui_generate_image(
try: try:
ws = websocket.WebSocket() ws = websocket.WebSocket()
ws.connect(f"ws://{host}/ws?clientId={client_id}") ws.connect(f"ws://{host}/ws?clientId={client_id}")
print("WebSocket connection established.") log.info("WebSocket connection established.")
except Exception as e: except Exception as e:
print(f"Failed to connect to WebSocket server: {e}") log.exception(f"Failed to connect to WebSocket server: {e}")
return None return None
try: try:
images = get_images(ws, comfyui_prompt, client_id, base_url) images = get_images(ws, comfyui_prompt, client_id, base_url)
except Exception as e: except Exception as e:
print(f"Error while receiving images: {e}") log.exception(f"Error while receiving images: {e}")
images = None images = None
ws.close() ws.close()

View file

@ -33,7 +33,13 @@ from constants import ERROR_MESSAGES
from utils.utils import decode_token, get_current_user, get_admin_user from utils.utils import decode_token, get_current_user, get_admin_user
from config import SRC_LOG_LEVELS, OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST, UPLOAD_DIR from config import (
SRC_LOG_LEVELS,
OLLAMA_BASE_URLS,
MODEL_FILTER_ENABLED,
MODEL_FILTER_LIST,
UPLOAD_DIR,
)
from utils.misc import calculate_sha256 from utils.misc import calculate_sha256
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -266,7 +272,7 @@ async def pull_model(
if request_id in REQUEST_POOL: if request_id in REQUEST_POOL:
yield chunk yield chunk
else: else:
print("User: canceled request") log.warning("User: canceled request")
break break
finally: finally:
if hasattr(r, "close"): if hasattr(r, "close"):
@ -664,7 +670,7 @@ async def generate_completion(
else: else:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="error_detail", detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
) )
url = app.state.OLLAMA_BASE_URLS[url_idx] url = app.state.OLLAMA_BASE_URLS[url_idx]
@ -770,7 +776,11 @@ async def generate_chat_completion(
r = None r = None
log.debug("form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(form_data.model_dump_json(exclude_none=True).encode())) log.debug(
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
form_data.model_dump_json(exclude_none=True).encode()
)
)
def get_request(): def get_request():
nonlocal form_data nonlocal form_data

View file

@ -333,7 +333,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
if overwrite: if overwrite:
for collection in CHROMA_CLIENT.list_collections(): for collection in CHROMA_CLIENT.list_collections():
if collection_name == collection.name: if collection_name == collection.name:
print(f"deleting existing collection {collection_name}") log.info(f"deleting existing collection {collection_name}")
CHROMA_CLIENT.delete_collection(name=collection_name) CHROMA_CLIENT.delete_collection(name=collection_name)
collection = CHROMA_CLIENT.create_collection( collection = CHROMA_CLIENT.create_collection(
@ -346,7 +346,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
) )
return True return True
except Exception as e: except Exception as e:
print(e) log.exception(e)
if e.__class__.__name__ == "UniqueConstraintError": if e.__class__.__name__ == "UniqueConstraintError":
return True return True
@ -575,7 +575,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
), ),
) )
except Exception as e: except Exception as e:
print(e) log.exception(e)
pass pass
except Exception as e: except Exception as e:

View file

@ -11,6 +11,7 @@ from utils.utils import verify_password
from apps.web.internal.db import DB from apps.web.internal.db import DB
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])

View file

@ -13,6 +13,7 @@ from apps.web.internal.db import DB
import json import json
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])

View file

@ -64,8 +64,8 @@ class ModelfilesTable:
self.db.create_tables([Modelfile]) self.db.create_tables([Modelfile])
def insert_new_modelfile( def insert_new_modelfile(
self, user_id: str, self, user_id: str, form_data: ModelfileForm
form_data: ModelfileForm) -> Optional[ModelfileModel]: ) -> Optional[ModelfileModel]:
if "tagName" in form_data.modelfile: if "tagName" in form_data.modelfile:
modelfile = ModelfileModel( modelfile = ModelfileModel(
**{ **{
@ -73,7 +73,8 @@ class ModelfilesTable:
"tag_name": form_data.modelfile["tagName"], "tag_name": form_data.modelfile["tagName"],
"modelfile": json.dumps(form_data.modelfile), "modelfile": json.dumps(form_data.modelfile),
"timestamp": int(time.time()), "timestamp": int(time.time()),
}) }
)
try: try:
result = Modelfile.create(**modelfile.model_dump()) result = Modelfile.create(**modelfile.model_dump())
@ -87,29 +88,28 @@ class ModelfilesTable:
else: else:
return None return None
def get_modelfile_by_tag_name(self, def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]:
tag_name: str) -> Optional[ModelfileModel]:
try: try:
modelfile = Modelfile.get(Modelfile.tag_name == tag_name) modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
return ModelfileModel(**model_to_dict(modelfile)) return ModelfileModel(**model_to_dict(modelfile))
except: except:
return None return None
def get_modelfiles(self, def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]:
skip: int = 0,
limit: int = 50) -> List[ModelfileResponse]:
return [ return [
ModelfileResponse( ModelfileResponse(
**{ **{
**model_to_dict(modelfile), **model_to_dict(modelfile),
"modelfile": "modelfile": json.loads(modelfile.modelfile),
json.loads(modelfile.modelfile), }
}) for modelfile in Modelfile.select() )
for modelfile in Modelfile.select()
# .limit(limit).offset(skip) # .limit(limit).offset(skip)
] ]
def update_modelfile_by_tag_name( def update_modelfile_by_tag_name(
self, tag_name: str, modelfile: dict) -> Optional[ModelfileModel]: self, tag_name: str, modelfile: dict
) -> Optional[ModelfileModel]:
try: try:
query = Modelfile.update( query = Modelfile.update(
modelfile=json.dumps(modelfile), modelfile=json.dumps(modelfile),

View file

@ -52,8 +52,9 @@ class PromptsTable:
self.db = db self.db = db
self.db.create_tables([Prompt]) self.db.create_tables([Prompt])
def insert_new_prompt(self, user_id: str, def insert_new_prompt(
form_data: PromptForm) -> Optional[PromptModel]: self, user_id: str, form_data: PromptForm
) -> Optional[PromptModel]:
prompt = PromptModel( prompt = PromptModel(
**{ **{
"user_id": user_id, "user_id": user_id,
@ -61,7 +62,8 @@ class PromptsTable:
"title": form_data.title, "title": form_data.title,
"content": form_data.content, "content": form_data.content,
"timestamp": int(time.time()), "timestamp": int(time.time()),
}) }
)
try: try:
result = Prompt.create(**prompt.model_dump()) result = Prompt.create(**prompt.model_dump())
@ -81,13 +83,14 @@ class PromptsTable:
def get_prompts(self) -> List[PromptModel]: def get_prompts(self) -> List[PromptModel]:
return [ return [
PromptModel(**model_to_dict(prompt)) for prompt in Prompt.select() PromptModel(**model_to_dict(prompt))
for prompt in Prompt.select()
# .limit(limit).offset(skip) # .limit(limit).offset(skip)
] ]
def update_prompt_by_command( def update_prompt_by_command(
self, command: str, self, command: str, form_data: PromptForm
form_data: PromptForm) -> Optional[PromptModel]: ) -> Optional[PromptModel]:
try: try:
query = Prompt.update( query = Prompt.update(
title=form_data.title, title=form_data.title,

View file

@ -11,6 +11,7 @@ import logging
from apps.web.internal.db import DB from apps.web.internal.db import DB
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])

View file

@ -29,6 +29,7 @@ from apps.web.models.tags import (
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])

View file

@ -10,7 +10,12 @@ import uuid
from apps.web.models.users import Users from apps.web.models.users import Users
from utils.utils import get_password_hash, get_current_user, get_admin_user, create_token from utils.utils import (
get_password_hash,
get_current_user,
get_admin_user,
create_token,
)
from utils.misc import get_gravatar_url, validate_email_format from utils.misc import get_gravatar_url, validate_email_format
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
@ -43,7 +48,6 @@ async def set_global_default_models(
return request.app.state.DEFAULT_MODELS return request.app.state.DEFAULT_MODELS
@router.post("/default/suggestions", response_model=List[PromptSuggestion]) @router.post("/default/suggestions", response_model=List[PromptSuggestion])
async def set_global_default_suggestions( async def set_global_default_suggestions(
request: Request, request: Request,

View file

@ -24,9 +24,9 @@ router = APIRouter()
@router.get("/", response_model=List[ModelfileResponse]) @router.get("/", response_model=List[ModelfileResponse])
async def get_modelfiles(skip: int = 0, async def get_modelfiles(
limit: int = 50, skip: int = 0, limit: int = 50, user=Depends(get_current_user)
user=Depends(get_current_user)): ):
return Modelfiles.get_modelfiles(skip, limit) return Modelfiles.get_modelfiles(skip, limit)
@ -36,17 +36,16 @@ async def get_modelfiles(skip: int = 0,
@router.post("/create", response_model=Optional[ModelfileResponse]) @router.post("/create", response_model=Optional[ModelfileResponse])
async def create_new_modelfile(form_data: ModelfileForm, async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)):
user=Depends(get_admin_user)):
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data) modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
if modelfile: if modelfile:
return ModelfileResponse( return ModelfileResponse(
**{ **{
**modelfile.model_dump(), **modelfile.model_dump(),
"modelfile": "modelfile": json.loads(modelfile.modelfile),
json.loads(modelfile.modelfile), }
}) )
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -60,17 +59,18 @@ async def create_new_modelfile(form_data: ModelfileForm,
@router.post("/", response_model=Optional[ModelfileResponse]) @router.post("/", response_model=Optional[ModelfileResponse])
async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm, async def get_modelfile_by_tag_name(
user=Depends(get_current_user)): form_data: ModelfileTagNameForm, user=Depends(get_current_user)
):
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name) modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
if modelfile: if modelfile:
return ModelfileResponse( return ModelfileResponse(
**{ **{
**modelfile.model_dump(), **modelfile.model_dump(),
"modelfile": "modelfile": json.loads(modelfile.modelfile),
json.loads(modelfile.modelfile), }
}) )
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -84,8 +84,9 @@ async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
@router.post("/update", response_model=Optional[ModelfileResponse]) @router.post("/update", response_model=Optional[ModelfileResponse])
async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm, async def update_modelfile_by_tag_name(
user=Depends(get_admin_user)): form_data: ModelfileUpdateForm, user=Depends(get_admin_user)
):
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name) modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
if modelfile: if modelfile:
updated_modelfile = { updated_modelfile = {
@ -94,14 +95,15 @@ async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
} }
modelfile = Modelfiles.update_modelfile_by_tag_name( modelfile = Modelfiles.update_modelfile_by_tag_name(
form_data.tag_name, updated_modelfile) form_data.tag_name, updated_modelfile
)
return ModelfileResponse( return ModelfileResponse(
**{ **{
**modelfile.model_dump(), **modelfile.model_dump(),
"modelfile": "modelfile": json.loads(modelfile.modelfile),
json.loads(modelfile.modelfile), }
}) )
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -115,7 +117,8 @@ async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
@router.delete("/delete", response_model=bool) @router.delete("/delete", response_model=bool)
async def delete_modelfile_by_tag_name(form_data: ModelfileTagNameForm, async def delete_modelfile_by_tag_name(
user=Depends(get_admin_user)): form_data: ModelfileTagNameForm, user=Depends(get_admin_user)
):
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name) result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
return result return result

View file

@ -16,6 +16,7 @@ from utils.utils import get_current_user, get_password_hash, get_admin_user
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])

View file

@ -117,7 +117,20 @@ else:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}") log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
log_sources = ["AUDIO", "CONFIG", "DB", "IMAGES", "LITELLM", "MAIN", "MODELS", "OLLAMA", "OPENAI", "RAG"] log_sources = [
"AUDIO",
"COMFYUI",
"CONFIG",
"DB",
"IMAGES",
"LITELLM",
"MAIN",
"MODELS",
"OLLAMA",
"OPENAI",
"RAG",
"WEBHOOK",
]
SRC_LOG_LEVELS = {} SRC_LOG_LEVELS = {}
@ -239,7 +252,7 @@ OLLAMA_API_BASE_URL = os.environ.get(
) )
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
KUBERNETES_SERVICE_HOST = os.environ.get("KUBERNETES_SERVICE_HOST", "") K8S_FLAG = os.environ.get("K8S_FLAG", "")
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
OLLAMA_BASE_URL = ( OLLAMA_BASE_URL = (
@ -249,9 +262,10 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
) )
if ENV == "prod": if ENV == "prod":
if OLLAMA_BASE_URL == "/ollama" and KUBERNETES_SERVICE_HOST == "": if OLLAMA_BASE_URL == "/ollama":
OLLAMA_BASE_URL = "http://host.docker.internal:11434" OLLAMA_BASE_URL = "http://host.docker.internal:11434"
else:
elif K8S_FLAG:
OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434" OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434"

View file

@ -1,33 +1,22 @@
{ {
"version": 0, "version": 0,
"ui": { "ui": {
"default_locale": "en-US",
"prompt_suggestions": [ "prompt_suggestions": [
{ {
"title": [ "title": ["Help me study", "vocabulary for a college entrance exam"],
"Help me study",
"vocabulary for a college entrance exam"
],
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option." "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option."
}, },
{ {
"title": [ "title": ["Give me ideas", "for what to do with my kids' art"],
"Give me ideas",
"for what to do with my kids' art"
],
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter." "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
}, },
{ {
"title": [ "title": ["Tell me a fun fact", "about the Roman Empire"],
"Tell me a fun fact",
"about the Roman Empire"
],
"content": "Tell me a random fun fact about the Roman Empire" "content": "Tell me a random fun fact about the Roman Empire"
}, },
{ {
"title": [ "title": ["Show me a code snippet", "of a website's sticky header"],
"Show me a code snippet",
"of a website's sticky header"
],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript." "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
} }
] ]

View file

@ -32,6 +32,7 @@ from utils.utils import get_admin_user
from apps.rag.utils import rag_messages from apps.rag.utils import rag_messages
from config import ( from config import (
CONFIG_DATA,
WEBUI_NAME, WEBUI_NAME,
ENV, ENV,
VERSION, VERSION,
@ -49,6 +50,7 @@ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"]) log.setLevel(SRC_LOG_LEVELS["MAIN"])
class SPAStaticFiles(StaticFiles): class SPAStaticFiles(StaticFiles):
async def get_response(self, path: str, scope): async def get_response(self, path: str, scope):
try: try:
@ -88,7 +90,6 @@ class RAGMiddleware(BaseHTTPMiddleware):
# Example: Add a new key-value pair or modify existing ones # Example: Add a new key-value pair or modify existing ones
# data["modified"] = True # Example modification # data["modified"] = True # Example modification
if "docs" in data: if "docs" in data:
data = {**data} data = {**data}
data["messages"] = rag_messages( data["messages"] = rag_messages(
data["docs"], data["docs"],
@ -163,11 +164,15 @@ app.mount("/rag/api/v1", rag_app)
@app.get("/api/config") @app.get("/api/config")
async def get_app_config(): async def get_app_config():
return { return {
"status": True, "status": True,
"name": WEBUI_NAME, "name": WEBUI_NAME,
"version": VERSION, "version": VERSION,
"default_locale": (
CONFIG_DATA["ui"]["default_locale"]
if "ui" in CONFIG_DATA and "default_locale" in CONFIG_DATA["ui"]
else "en-US"
),
"images": images_app.state.ENABLED, "images": images_app.state.ENABLED,
"default_models": webui_app.state.DEFAULT_MODELS, "default_models": webui_app.state.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
@ -192,7 +197,6 @@ class ModelFilterConfigForm(BaseModel):
async def update_model_filter_config( async def update_model_filter_config(
form_data: ModelFilterConfigForm, user=Depends(get_admin_user) form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
): ):
app.state.MODEL_FILTER_ENABLED = form_data.enabled app.state.MODEL_FILTER_ENABLED = form_data.enabled
app.state.MODEL_FILTER_LIST = form_data.models app.state.MODEL_FILTER_LIST = form_data.models
@ -202,6 +206,9 @@ async def update_model_filter_config(
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
litellm_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
return { return {
"enabled": app.state.MODEL_FILTER_ENABLED, "enabled": app.state.MODEL_FILTER_ENABLED,
"models": app.state.MODEL_FILTER_LIST, "models": app.state.MODEL_FILTER_LIST,
@ -232,7 +239,6 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
@app.get("/api/version") @app.get("/api/version")
async def get_app_config(): async def get_app_config():
return { return {
"version": VERSION, "version": VERSION,
} }
@ -240,7 +246,7 @@ async def get_app_config():
@app.get("/api/changelog") @app.get("/api/changelog")
async def get_app_changelog(): async def get_app_changelog():
return CHANGELOG return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5}
@app.get("/api/version/updates") @app.get("/api/version/updates")

View file

@ -1,6 +1,12 @@
import json import json
import requests import requests
from config import VERSION, WEBUI_FAVICON_URL, WEBUI_NAME import logging
from config import SRC_LOG_LEVELS, VERSION, WEBUI_FAVICON_URL, WEBUI_NAME
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
def post_webhook(url: str, message: str, event_data: dict) -> bool: def post_webhook(url: str, message: str, event_data: dict) -> bool:
try: try:
@ -38,9 +44,11 @@ def post_webhook(url: str, message: str, event_data: dict) -> bool:
else: else:
payload = {**event_data} payload = {**event_data}
log.debug(f"payload: {payload}")
r = requests.post(url, json=payload) r = requests.post(url, json=payload)
r.raise_for_status() r.raise_for_status()
log.debug(f"r.text: {r.text}")
return True return True
except Exception as e: except Exception as e:
print(e) log.exception(e)
return False return False

View file

@ -88,7 +88,7 @@ spec:
resources: resources:
requests: requests:
storage: {{ .Values.ollama.persistence.size | quote }} storage: {{ .Values.ollama.persistence.size | quote }}
storageClass: {{ .Values.ollama.persistence.storageClass }} storageClassName: {{ .Values.ollama.persistence.storageClass }}
{{- with .Values.ollama.persistence.selector }} {{- with .Values.ollama.persistence.selector }}
selector: selector:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}

View file

@ -17,7 +17,7 @@ spec:
resources: resources:
requests: requests:
storage: {{ .Values.webui.persistence.size }} storage: {{ .Values.webui.persistence.size }}
storageClass: {{ .Values.webui.persistence.storageClass }} storageClassName: {{ .Values.webui.persistence.storageClass }}
{{- with .Values.webui.persistence.selector }} {{- with .Values.webui.persistence.selector }}
selector: selector:
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.1.114", "version": "0.1.116",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.1.114", "version": "0.1.116",
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^1.3.1", "@sveltejs/adapter-node": "^1.3.1",
"async": "^3.2.5", "async": "^3.2.5",

View file

@ -1,6 +1,6 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.1.115", "version": "0.1.116",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev --host", "dev": "vite dev --host",
@ -13,7 +13,6 @@
"lint:types": "npm run check", "lint:types": "npm run check",
"lint:backend": "pylint backend/", "lint:backend": "pylint backend/",
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'", "format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
"format:backend": "yapf --recursive backend -p -i",
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'" "i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
}, },
"devDependencies": { "devDependencies": {

View file

@ -9,7 +9,11 @@
<script> <script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC // On page load or when changing themes, best to add inline in `head` to avoid FOUC
(() => { (() => {
if ( if (localStorage?.theme && localStorage?.theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-900', '#000000');
document.documentElement.style.setProperty('--color-gray-950', '#000000');
document.documentElement.classList.add('dark');
} else if (
localStorage.theme === 'light' || localStorage.theme === 'light' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches) (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
) { ) {

View file

@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Collapsible } from 'bits-ui';
import { setDefaultModels } from '$lib/apis/configs'; import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores'; import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Selector from './ModelSelector/Selector.svelte'; import Selector from './ModelSelector/Selector.svelte';
import Tooltip from '../common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -33,11 +36,11 @@
} }
</script> </script>
<div class="flex flex-col my-2 w-full"> <div class="flex flex-col mt-0.5 w-full">
{#each selectedModels as selectedModel, selectedModelIdx} {#each selectedModels as selectedModel, selectedModelIdx}
<div class="flex w-full"> <div class="flex w-full">
<div class="overflow-hidden w-full"> <div class="overflow-hidden w-full">
<div class="mr-2 max-w-full"> <div class="mr-0.5 max-w-full">
<Selector <Selector
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
items={$models items={$models
@ -53,10 +56,10 @@
</div> </div>
{#if selectedModelIdx === 0} {#if selectedModelIdx === 0}
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
<Tooltip content="Add Model">
<button <button
class=" self-center {selectedModelIdx === 0 class=" "
? 'mr-3'
: 'mr-7'} disabled:text-gray-600 disabled:hover:text-gray-600"
{disabled} {disabled}
on:click={() => { on:click={() => {
selectedModels = [...selectedModels, '']; selectedModels = [...selectedModels, ''];
@ -73,12 +76,12 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg> </svg>
</button> </button>
</Tooltip>
</div>
{:else} {:else}
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
<Tooltip content="Remove Model">
<button <button
class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 {selectedModelIdx ===
0
? 'mr-3'
: 'mr-7'}"
{disabled} {disabled}
on:click={() => { on:click={() => {
selectedModels.splice(selectedModelIdx, 1); selectedModels.splice(selectedModelIdx, 1);
@ -96,41 +99,13 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg> </svg>
</button> </button>
{/if} </Tooltip>
</div>
{#if selectedModelIdx === 0}
<button
class=" self-center dark:hover:text-gray-300"
id="open-settings-button"
on:click={async () => {
await showSettings.set(!$showSettings);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
{/if} {/if}
</div> </div>
{/each} {/each}
</div> </div>
<div class="text-left mt-1.5 ml-1 text-xs text-gray-500"> <div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button> <button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div> </div>

View file

@ -181,20 +181,20 @@
searchValue = ''; searchValue = '';
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0); window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
}} }}
selected={items.find((item) => item.value === value)} selected={items.find((item) => item.value === value) ?? ''}
onSelectedChange={(selectedItem) => { onSelectedChange={(selectedItem) => {
value = selectedItem.value; value = selectedItem.value;
}} }}
> >
<Select.Trigger class="relative w-full" aria-label={placeholder}> <Select.Trigger class="relative w-full" aria-label={placeholder}>
<Select.Value <Select.Value
class="inline-flex h-input px-0.5 w-full outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none" class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
{placeholder} {placeholder}
/> />
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" /> <ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
</Select.Trigger> </Select.Trigger>
<Select.Content <Select.Content
class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none" class=" z-40 w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
transition={flyAndScale} transition={flyAndScale}
sideOffset={4} sideOffset={4}
> >
@ -214,7 +214,7 @@
<hr class="border-gray-100 dark:border-gray-800" /> <hr class="border-gray-100 dark:border-gray-800" />
{/if} {/if}
<div class="px-3 my-2 max-h-80 overflow-y-auto"> <div class="px-3 my-2 max-h-72 overflow-y-auto">
{#each filteredItems as item} {#each filteredItems as item}
<Select.Item <Select.Item
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted" class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"

View file

@ -14,7 +14,7 @@
export let getModels: Function; export let getModels: Function;
// General // General
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light']; let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light', 'oled-dark'];
let selectedTheme = 'system'; let selectedTheme = 'system';
let languages = []; let languages = [];
@ -91,12 +91,17 @@
}); });
const applyTheme = (_theme: string) => { const applyTheme = (_theme: string) => {
let themeToApply = _theme; let themeToApply = _theme === 'oled-dark' ? 'dark' : _theme;
if (_theme === 'system') { if (_theme === 'system') {
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
} }
if (themeToApply === 'dark' && !_theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-900', '#171717');
document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d');
}
themes themes
.filter((e) => e !== themeToApply) .filter((e) => e !== themeToApply)
.forEach((e) => { .forEach((e) => {
@ -115,7 +120,11 @@
const themeChangeHandler = (_theme: string) => { const themeChangeHandler = (_theme: string) => {
theme.set(_theme); theme.set(_theme);
localStorage.setItem('theme', _theme); localStorage.setItem('theme', _theme);
if (_theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-900', '#000000');
document.documentElement.style.setProperty('--color-gray-950', '#000000');
document.documentElement.classList.add('dark');
}
applyTheme(_theme); applyTheme(_theme);
}; };
</script> </script>
@ -136,6 +145,7 @@
> >
<option value="system">⚙️ {$i18n.t('System')}</option> <option value="system">⚙️ {$i18n.t('System')}</option>
<option value="dark">🌑 {$i18n.t('Dark')}</option> <option value="dark">🌑 {$i18n.t('Dark')}</option>
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
<option value="light">☀️ {$i18n.t('Light')}</option> <option value="light">☀️ {$i18n.t('Light')}</option>
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option> <option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option> <option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
import Tags from '../common/Tags.svelte';
const i18n = getContext('i18n');
export let tags;
export let deleteTag: Function;
export let addTag: Function;
export let show = false;
</script>
<Modal bind:show size="xs">
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
<Tags {tags} {deleteTag} {addTag} />
</div>
</Modal>

View file

@ -8,7 +8,7 @@
export let addTag: Function; export let addTag: Function;
</script> </script>
<div class="flex flex-row space-x-0.5 line-clamp-1"> <div class="flex flex-row flex-wrap gap-0.5 line-clamp-1">
<TagList <TagList
{tags} {tags}
on:delete={(e) => { on:delete={(e) => {
@ -17,6 +17,7 @@
/> />
<TagInput <TagInput
label={tags.length == 0 ? 'Add Tags' : ''}
on:add={(e) => { on:add={(e) => {
addTag(e.detail); addTag(e.detail);
}} }}

View file

@ -1,24 +1,31 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, getContext } from 'svelte'; import { createEventDispatcher, getContext } from 'svelte';
import { tags } from '$lib/stores';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let label = '';
let showTagInput = false; let showTagInput = false;
let tagName = ''; let tagName = '';
const addTagHandler = async () => {
tagName = tagName.trim();
if (tagName !== '') {
dispatch('add', tagName);
tagName = '';
showTagInput = false;
} else {
toast.error('Invalid Tag');
}
};
</script> </script>
<div class="flex space-x-1 pl-1.5"> <div class="flex space-x-1 pl-1.5">
{#if showTagInput} {#if showTagInput}
<div class="flex items-center"> <div class="flex items-center">
<button <button type="button" on:click={addTagHandler}>
type="button"
on:click={() => {
dispatch('add', tagName);
tagName = '';
showTagInput = false;
}}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
@ -34,12 +41,21 @@
</button> </button>
<input <input
bind:value={tagName} bind:value={tagName}
class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[8rem]" class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[5.5rem]"
placeholder={$i18n.t('Add a tag')} placeholder={$i18n.t('Add a tag')}
list="tagOptions"
on:keydown={(event) => {
if (event.key === 'Enter') {
addTagHandler();
}
}}
/> />
<datalist id="tagOptions">
{#each $tags as tag}
<option value={tag.name} />
{/each}
</datalist>
</div> </div>
<!-- TODO: Tag Suggestions -->
{/if} {/if}
<button <button
@ -62,4 +78,8 @@
</svg> </svg>
</div> </div>
</button> </button>
{#if label && !showTagInput}
<span class="text-xs pl-1.5 self-center">{label}</span>
{/if}
</div> </div>

View file

@ -7,7 +7,7 @@
{#each tags as tag} {#each tags as tag}
<div <div
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-600 dark:text-white" class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-800 dark:text-white"
> >
<div class=" text-[0.7rem] font-medium self-center line-clamp-1"> <div class=" text-[0.7rem] font-medium self-center line-clamp-1">
{tag.name} {tag.name}

View file

@ -29,6 +29,6 @@
}); });
</script> </script>
<div bind:this={tooltipElement} aria-label={content}> <div bind:this={tooltipElement} aria-label={content} class="flex">
<slot /> <slot />
</div> </div>

View file

@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
/>
</svg>

View file

@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"
/>
</svg>

View file

@ -4,11 +4,21 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { Separator } from 'bits-ui';
import { getChatById } from '$lib/apis/chats'; import { getChatById } from '$lib/apis/chats';
import { WEBUI_NAME, chatId, modelfiles, settings } from '$lib/stores'; import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
import { slide } from 'svelte/transition';
import ShareChatModal from '../chat/ShareChatModal.svelte'; import ShareChatModal from '../chat/ShareChatModal.svelte';
import TagInput from '../common/Tags/TagInput.svelte'; import TagInput from '../common/Tags/TagInput.svelte';
import Tags from '../common/Tags.svelte'; import ModelSelector from '../chat/ModelSelector.svelte';
import Tooltip from '../common/Tooltip.svelte';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
import ChevronDown from '../icons/ChevronDown.svelte';
import ChevronUpDown from '../icons/ChevronUpDown.svelte';
import Menu from './Navbar/Menu.svelte';
import TagChatModal from '../chat/TagChatModal.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -16,14 +26,16 @@
export let title: string = $WEBUI_NAME; export let title: string = $WEBUI_NAME;
export let shareEnabled: boolean = false; export let shareEnabled: boolean = false;
export let selectedModels;
export let tags = []; export let tags = [];
export let addTag: Function; export let addTag: Function;
export let deleteTag: Function; export let deleteTag: Function;
let showShareChatModal = false; export let showModelSelector = false;
let tagName = ''; let showShareChatModal = false;
let showTagInput = false; let showTagChatModal = false;
const shareChat = async () => { const shareChat = async () => {
const chat = (await getChatById(localStorage.token, $chatId)).chat; const chat = (await getChatById(localStorage.token, $chatId)).chat;
@ -69,48 +81,27 @@
</script> </script>
<ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} /> <ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} />
<!-- <TagChatModal bind:show={showTagChatModal} {tags} {deleteTag} {addTag} /> -->
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30"> <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<div <div
class=" flex {$settings?.fullScreenMode ?? null class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}
? 'max-w-full' w-full mx-auto px-3"
: 'max-w-3xl'} w-full mx-auto px-3"
> >
<!-- {#if shareEnabled}
<div class="flex items-center w-full max-w-full"> <div class="flex items-center w-full max-w-full">
<div class="pr-2 self-start">
<button
id="new-chat-button"
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition"
on:click={initNewChat}
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
/>
<path
d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"
/>
</svg>
</div>
</button>
</div>
<div class=" flex-1 self-center font-medium line-clamp-1"> <div class=" flex-1 self-center font-medium line-clamp-1">
<div> <div>
{title != '' ? title : $WEBUI_NAME} {title != '' ? title : $WEBUI_NAME}
</div> </div>
</div> </div>
<div class="pl-2 self-center flex items-center">
<div class="pl-2 self-center flex items-center space-x-2"> <div class=" mr-1">
{#if shareEnabled}
<Tags {tags} {deleteTag} {addTag} /> <Tags {tags} {deleteTag} {addTag} />
</div>
<Tooltip content="Share">
<button <button
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition" class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
on:click={async () => { on:click={async () => {
showShareChatModal = !showShareChatModal; showShareChatModal = !showShareChatModal;
@ -132,7 +123,112 @@
</svg> </svg>
</div> </div>
</button> </button>
</Tooltip>
</div>
</div>
{/if} -->
<!-- <div class=" flex-1 self-center font-medium line-clamp-1">
<div>
{title != '' ? title : $WEBUI_NAME}
</div>
</div> -->
<div class="flex items-center w-full max-w-full">
<div class="w-full flex-1 overflow-hidden max-w-full">
<ModelSelector bind:selectedModels />
</div>
<div class="self-start flex flex-none items-center">
<div class="flex self-center w-[1px] h-5 mx-2 bg-stone-700" />
{#if !shareEnabled}
<Tooltip content="Settings">
<button
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
id="open-settings-button"
on:click={async () => {
await showSettings.set(!$showSettings);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
</Tooltip>
{:else}
<Menu
{shareEnabled}
shareHandler={() => {
showShareChatModal = !showShareChatModal;
}}
{tags}
{deleteTag}
{addTag}
>
<button
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
/>
</svg>
</div>
</button>
</Menu>
{/if} {/if}
<Tooltip content="New Chat">
<button
id="new-chat-button"
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
on:click={() => {
initNewChat();
}}
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
/>
<path
d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"
/>
</svg>
</div>
</button>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,123 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { showSettings } from '$lib/stores';
import Tags from '$lib/components/common/Tags.svelte';
export let shareEnabled: boolean = false;
export let shareHandler: Function;
// export let tagHandler: Function;
export let tags;
export let deleteTag: Function;
export let addTag: Function;
export let onClose: Function = () => {};
</script>
<Dropdown
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
>
<slot />
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[150px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
sideOffset={8}
side="bottom"
align="end"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={async () => {
await showSettings.set(!$showSettings);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
<div class="flex items-center">Settings</div>
</DropdownMenu.Item>
{#if shareEnabled}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
shareHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
clip-rule="evenodd"
/>
</svg>
<div class="flex items-center">Share</div>
</DropdownMenu.Item>
<hr class="border-gray-100 dark:border-gray-800 my-1" />
<div class="flex p-1">
<Tags {tags} {deleteTag} {addTag} />
</div>
<!-- <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
tagHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6Z" />
</svg>
<div class="flex items-center">Tag</div>
</DropdownMenu.Item> -->
{/if}
</DropdownMenu.Content>
</div>
</Dropdown>

View file

@ -107,7 +107,7 @@
bind:this={navElement} bind:this={navElement}
class="h-screen max-h-[100dvh] min-h-screen {show class="h-screen max-h-[100dvh] min-h-screen {show
? 'lg:relative w-[260px]' ? 'lg:relative w-[260px]'
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition z-40 fixed top-0 left-0 : '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0
" "
> >
<div <div
@ -706,7 +706,7 @@
</div> </div>
<div <div
class="fixed left-0 top-[50dvh] z-40 -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0" class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
> >
<Tooltip <Tooltip
placement="right" placement="right"

View file

@ -37,23 +37,28 @@ const createIsLoadingStore = (i18n: i18nType) => {
return isLoading; return isLoading;
}; };
i18next export const initI18n = (defaultLocale: string) => {
.use( let detectionOrder = defaultLocale
resourcesToBackend( ? ['querystring', 'localStorage']
(language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`) : ['querystring', 'localStorage', 'navigator'];
) let fallbackDefaultLocale = defaultLocale ? [defaultLocale] : ['en-US'];
)
const loadResource = (language: string, namespace: string) =>
import(`./locales/${language}/${namespace}.json`);
i18next
.use(resourcesToBackend(loadResource))
.use(LanguageDetector) .use(LanguageDetector)
.init({ .init({
debug: false, debug: false,
detection: { detection: {
order: ['querystring', 'localStorage', 'navigator'], order: detectionOrder,
caches: ['localStorage'], caches: ['localStorage'],
lookupQuerystring: 'lang', lookupQuerystring: 'lang',
lookupLocalStorage: 'locale' lookupLocalStorage: 'locale'
}, },
fallbackLng: { fallbackLng: {
default: ['en-US'] default: fallbackDefaultLocale
}, },
ns: 'translation', ns: 'translation',
returnEmptyString: false, returnEmptyString: false,
@ -61,6 +66,7 @@ i18next
escapeValue: false // not needed for svelte as it escapes by default escapeValue: false // not needed for svelte as it escapes by default
} }
}); });
};
const i18n = createI18nStore(i18next); const i18n = createI18nStore(i18next);
const isLoadingStore = createIsLoadingStore(i18next); const isLoadingStore = createIsLoadingStore(i18next);

View file

@ -1,128 +1,128 @@
{ {
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.", "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "",
"(Beta)": "(Beta)", "(Beta)": "",
"(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)", "(e.g. `sh webui.sh --api`)": "",
"(latest)": "(latest)", "(latest)": "",
"{{modelName}} is thinking...": "{{modelName}} is thinking...", "{{modelName}} is thinking...": "",
"{{webUIName}} Backend Required": "{{webUIName}} Backend Required", "{{webUIName}} Backend Required": "",
"a user": "", "a user": "",
"About": "About", "About": "",
"Account": "Account", "Account": "",
"Action": "Action", "Action": "",
"Add a model": "Add a model", "Add a model": "",
"Add a model tag name": "Add a model tag name", "Add a model tag name": "",
"Add a short description about what this modelfile does": "Add a short description about what this modelfile does", "Add a short description about what this modelfile does": "",
"Add a short title for this prompt": "Add a short title for this prompt", "Add a short title for this prompt": "",
"Add a tag": "Add a tag", "Add a tag": "",
"Add Docs": "Add Docs", "Add Docs": "",
"Add Files": "Add Files", "Add Files": "",
"Add message": "Add message", "Add message": "",
"add tags": "add tags", "add tags": "",
"Adjusting these settings will apply changes universally to all users.": "Adjusting these settings will apply changes universally to all users.", "Adjusting these settings will apply changes universally to all users.": "",
"admin": "admin", "admin": "",
"Admin Panel": "Admin Panel", "Admin Panel": "",
"Admin Settings": "Admin Settings", "Admin Settings": "",
"Advanced Parameters": "Advanced Parameters", "Advanced Parameters": "",
"all": "all", "all": "",
"All Users": "All Users", "All Users": "",
"Allow": "Allow", "Allow": "",
"Allow Chat Deletion": "Allow Chat Deletion", "Allow Chat Deletion": "",
"alphanumeric characters and hyphens": "alphanumeric characters and hyphens", "alphanumeric characters and hyphens": "",
"Already have an account?": "Already have an account?", "Already have an account?": "",
"an assistant": "", "an assistant": "",
"and": "and", "and": "",
"API Base URL": "API Base URL", "API Base URL": "",
"API Key": "API Key", "API Key": "",
"API RPM": "API RPM", "API RPM": "",
"are allowed - Activate this command by typing": "are allowed - Activate this command by typing", "are allowed - Activate this command by typing": "",
"Are you sure?": "", "Are you sure?": "",
"Audio": "Audio", "Audio": "",
"Auto-playback response": "Auto-playback response", "Auto-playback response": "",
"Auto-send input after 3 sec.": "Auto-send input after 3 sec.", "Auto-send input after 3 sec.": "",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL", "AUTOMATIC1111 Base URL": "",
"AUTOMATIC1111 Base URL is required.": "", "AUTOMATIC1111 Base URL is required.": "",
"available!": "available!", "available!": "",
"Back": "Back", "Back": "",
"Builder Mode": "Builder Mode", "Builder Mode": "",
"Cancel": "Cancel", "Cancel": "",
"Categories": "Categories", "Categories": "",
"Change Password": "Change Password", "Change Password": "",
"Chat": "Chat", "Chat": "",
"Chat History": "Chat History", "Chat History": "",
"Chat History is off for this browser.": "Chat History is off for this browser.", "Chat History is off for this browser.": "",
"Chats": "Chats", "Chats": "",
"Check Again": "Check Again", "Check Again": "",
"Check for updates": "Check for updates", "Check for updates": "",
"Checking for updates...": "Checking for updates...", "Checking for updates...": "",
"Choose a model before saving...": "Choose a model before saving...", "Choose a model before saving...": "",
"Chunk Overlap": "Chunk Overlap", "Chunk Overlap": "",
"Chunk Params": "Chunk Params", "Chunk Params": "",
"Chunk Size": "Chunk Size", "Chunk Size": "",
"Click here for help.": "Click here for help.", "Click here for help.": "",
"Click here to check other modelfiles.": "Click here to check other modelfiles.", "Click here to check other modelfiles.": "",
"Click here to select": "", "Click here to select": "",
"Click here to select documents.": "", "Click here to select documents.": "",
"click here.": "click here.", "click here.": "",
"Click on the user role button to change a user's role.": "Click on the user role button to change a user's role.", "Click on the user role button to change a user's role.": "",
"Close": "Close", "Close": "",
"Collection": "Collection", "Collection": "",
"Command": "Command", "Command": "",
"Confirm Password": "Confirm Password", "Confirm Password": "",
"Connections": "Connections", "Connections": "",
"Content": "Content", "Content": "",
"Context Length": "Context Length", "Context Length": "",
"Conversation Mode": "Conversation Mode", "Conversation Mode": "",
"Copy last code block": "Copy last code block", "Copy last code block": "",
"Copy last response": "Copy last response", "Copy last response": "",
"Copying to clipboard was successful!": "Copying to clipboard was successful!", "Copying to clipboard was successful!": "",
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':", "Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "",
"Create a modelfile": "Create a modelfile", "Create a modelfile": "",
"Create Account": "Create Account", "Create Account": "",
"Created at": "Created at", "Created at": "",
"Created by": "Created by", "Created by": "",
"Current Model": "Current Model", "Current Model": "",
"Current Password": "Current Password", "Current Password": "",
"Custom": "Custom", "Custom": "",
"Customize Ollama models for a specific purpose": "Customize Ollama models for a specific purpose", "Customize Ollama models for a specific purpose": "",
"Dark": "Dark", "Dark": "",
"Database": "Database", "Database": "",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm", "DD/MM/YYYY HH:mm": "",
"Default": "Default", "Default": "",
"Default (Automatic1111)": "", "Default (Automatic1111)": "",
"Default (Web API)": "Default (Web API)", "Default (Web API)": "",
"Default model updated": "Default model updated", "Default model updated": "",
"Default Prompt Suggestions": "Default Prompt Suggestions", "Default Prompt Suggestions": "",
"Default User Role": "Default User Role", "Default User Role": "",
"delete": "delete", "delete": "",
"Delete a model": "Delete a model", "Delete a model": "",
"Delete chat": "Delete chat", "Delete chat": "",
"Delete Chats": "Delete Chats", "Delete Chats": "",
"Deleted {{deleteModelTag}}": "Deleted {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "",
"Deleted {tagName}": "Deleted {tagName}", "Deleted {tagName}": "",
"Description": "Description", "Description": "",
"Desktop Notifications": "Notification", "Desktop Notifications": "",
"Disabled": "Disabled", "Disabled": "",
"Discover a modelfile": "Discover a modelfile", "Discover a modelfile": "",
"Discover a prompt": "Discover a prompt", "Discover a prompt": "",
"Discover, download, and explore custom prompts": "Discover, download, and explore custom prompts", "Discover, download, and explore custom prompts": "",
"Discover, download, and explore model presets": "Discover, download, and explore model presets", "Discover, download, and explore model presets": "",
"Display the username instead of You in the Chat": "Display the username instead of 'You' in the Chat", "Display the username instead of You in the Chat": "",
"Document": "Document", "Document": "",
"Document Settings": "Document Settings", "Document Settings": "",
"Documents": "Documents", "Documents": "",
"does not make any external connections, and your data stays securely on your locally hosted server.": "does not make any external connections, and your data stays securely on your locally hosted server.", "does not make any external connections, and your data stays securely on your locally hosted server.": "",
"Don't Allow": "Don't Allow", "Don't Allow": "",
"Don't have an account?": "Don't have an account?", "Don't have an account?": "",
"Download as a File": "Download as a File", "Download as a File": "",
"Download Database": "Download Database", "Download Database": "",
"Drop any files here to add to the conversation": "Drop any files here to add to the conversation", "Drop any files here to add to the conversation": "",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.", "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
"Edit Doc": "Edit Doc", "Edit Doc": "",
"Edit User": "Edit User", "Edit User": "",
"Email": "Email", "Email": "",
"Enable Chat History": "Enable Chat History", "Enable Chat History": "",
"Enable New Sign Ups": "Enable New Sign Ups", "Enable New Sign Ups": "",
"Enabled": "Enabled", "Enabled": "",
"Enter {{role}} message here": "", "Enter {{role}} message here": "",
"Enter API Key": "", "Enter API Key": "",
"Enter Chunk Overlap": "", "Enter Chunk Overlap": "",
@ -135,229 +135,229 @@
"Enter Max Tokens (litellm_params.max_tokens)": "", "Enter Max Tokens (litellm_params.max_tokens)": "",
"Enter model tag (e.g. {{modelTag}})": "", "Enter model tag (e.g. {{modelTag}})": "",
"Enter Number of Steps (e.g. 50)": "", "Enter Number of Steps (e.g. 50)": "",
"Enter stop sequence": "Enter stop sequence", "Enter stop sequence": "",
"Enter Top K": "", "Enter Top K": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "", "Enter URL (e.g. http://127.0.0.1:7860/)": "",
"Enter Your Email": "Enter Your Email", "Enter Your Email": "",
"Enter Your Full Name": "Enter Your Full Name", "Enter Your Full Name": "",
"Enter Your Password": "Enter Your Password", "Enter Your Password": "",
"Experimental": "Experimental", "Experimental": "",
"Export All Chats (All Users)": "Export All Chats (All Users)", "Export All Chats (All Users)": "",
"Export Chats": "Export Chats", "Export Chats": "",
"Export Documents Mapping": "Export Documents Mapping", "Export Documents Mapping": "",
"Export Modelfiles": "Export Modelfiles", "Export Modelfiles": "",
"Export Prompts": "Export Prompts", "Export Prompts": "",
"Failed to read clipboard contents": "Failed to read clipboard contents", "Failed to read clipboard contents": "",
"File Mode": "File Mode", "File Mode": "",
"File not found.": "File not found.", "File not found.": "",
"Focus chat input": "Focus chat input", "Focus chat input": "",
"Format your variables using square brackets like this:": "Format your variables using square brackets like this:", "Format your variables using square brackets like this:": "",
"From (Base Model)": "From (Base Model)", "From (Base Model)": "",
"Full Screen Mode": "Full Screen Mode", "Full Screen Mode": "",
"General": "General", "General": "",
"General Settings": "General Settings", "General Settings": "",
"Hello, {{name}}": "Hello, {{name}}", "Hello, {{name}}": "",
"Hide": "Hide", "Hide": "",
"Hide Additional Params": "Hide Additional Params", "Hide Additional Params": "",
"How can I help you today?": "How can I help you today?", "How can I help you today?": "",
"Image Generation (Experimental)": "Image Generation (Experimental)", "Image Generation (Experimental)": "",
"Image Generation Engine": "", "Image Generation Engine": "",
"Image Settings": "Image Settings", "Image Settings": "",
"Images": "Images", "Images": "",
"Import Chats": "Import Chats", "Import Chats": "",
"Import Documents Mapping": "Import Documents Mapping", "Import Documents Mapping": "",
"Import Modelfiles": "Import Modelfiles", "Import Modelfiles": "",
"Import Prompts": "Import Prompts", "Import Prompts": "",
"Include `--api` flag when running stable-diffusion-webui": "Include `--api` flag when running stable-diffusion-webui", "Include `--api` flag when running stable-diffusion-webui": "",
"Interface": "Interface", "Interface": "",
"join our Discord for help.": "join our Discord for help.", "join our Discord for help.": "",
"JSON": "JSON", "JSON": "",
"JWT Expiration": "JWT Expiration", "JWT Expiration": "",
"JWT Token": "JWT Token", "JWT Token": "",
"Keep Alive": "Keep Alive", "Keep Alive": "",
"Keyboard shortcuts": "Keyboard shortcuts", "Keyboard shortcuts": "",
"Language": "Language", "Language": "",
"Light": "Light", "Light": "",
"Listening...": "Listening...", "Listening...": "",
"LLMs can make mistakes. Verify important information.": "LLMs can make mistakes. Verify important information.", "LLMs can make mistakes. Verify important information.": "",
"Made by OpenWebUI Community": "Made by OpenWebUI Community", "Made by OpenWebUI Community": "",
"Make sure to enclose them with": "Make sure to enclose them with", "Make sure to enclose them with": "",
"Manage LiteLLM Models": "Manage LiteLLM Models", "Manage LiteLLM Models": "",
"Manage Models": "Manage Models", "Manage Models": "",
"Manage Ollama Models": "Manage Ollama Models", "Manage Ollama Models": "",
"Max Tokens": "Max Tokens", "Max Tokens": "",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maximum of 3 models can be downloaded simultaneously. Please try again later.", "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "",
"Mirostat": "Mirostat", "Mirostat": "",
"Mirostat Eta": "Mirostat Eta", "Mirostat Eta": "",
"Mirostat Tau": "Mirostat Tau", "Mirostat Tau": "",
"MMMM DD, YYYY": "MMMM DD, YYYY", "MMMM DD, YYYY": "",
"Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' has been successfully downloaded.", "Model '{{modelName}}' has been successfully downloaded.": "",
"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' is already in queue for downloading.", "Model '{{modelTag}}' is already in queue for downloading.": "",
"Model {{modelId}} not found": "Model {{modelId}} not found", "Model {{modelId}} not found": "",
"Model {{modelName}} already exists.": "Model {{modelName}} already exists.", "Model {{modelName}} already exists.": "",
"Model Name": "Model Name", "Model Name": "",
"Model not selected": "Model not selected", "Model not selected": "",
"Model Tag Name": "Model Tag Name", "Model Tag Name": "",
"Model Whitelisting": "Model Whitelisting", "Model Whitelisting": "",
"Model(s) Whitelisted": "Model(s) Whitelisted", "Model(s) Whitelisted": "",
"Modelfile": "Modelfile", "Modelfile": "",
"Modelfile Advanced Settings": "Modelfile Advanced Settings", "Modelfile Advanced Settings": "",
"Modelfile Content": "Modelfile Content", "Modelfile Content": "",
"Modelfiles": "Modelfiles", "Modelfiles": "",
"Models": "Models", "Models": "",
"My Documents": "My Documents", "My Documents": "",
"My Modelfiles": "My Modelfiles", "My Modelfiles": "",
"My Prompts": "My Prompts", "My Prompts": "",
"Name": "Name", "Name": "",
"Name Tag": "Name Tag", "Name Tag": "",
"Name your modelfile": "Name your modelfile", "Name your modelfile": "",
"New Chat": "New Chat", "New Chat": "",
"New Password": "New Password", "New Password": "",
"Not sure what to add?": "Not sure what to add?", "Not sure what to add?": "",
"Not sure what to write? Switch to": "Not sure what to write? Switch to", "Not sure what to write? Switch to": "",
"Off": "Off", "Off": "",
"Okay, Let's Go!": "Okay, Let's Go!", "Okay, Let's Go!": "",
"Ollama Base URL": "", "Ollama Base URL": "",
"Ollama Version": "Ollama Version", "Ollama Version": "",
"On": "On", "On": "",
"Only": "Only", "Only": "",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Only alphanumeric characters and hyphens are allowed in the command string.", "Only alphanumeric characters and hyphens are allowed in the command string.": "",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.", "Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Looks like the URL is invalid. Please double-check and try again.", "Oops! Looks like the URL is invalid. Please double-check and try again.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.", "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "",
"Open": "Open", "Open": "",
"Open AI": "Open AI", "Open AI": "",
"Open AI (Dall-E)": "", "Open AI (Dall-E)": "",
"Open new chat": "Open new chat", "Open new chat": "",
"OpenAI API": "OpenAI API", "OpenAI API": "",
"OpenAI API Key": "", "OpenAI API Key": "",
"OpenAI API Key is required.": "", "OpenAI API Key is required.": "",
"or": "or", "or": "",
"Parameters": "Parameters", "Parameters": "",
"Password": "Password", "Password": "",
"PDF Extract Images (OCR)": "PDF Extract Images (OCR)", "PDF Extract Images (OCR)": "",
"pending": "pending", "pending": "",
"Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}", "Permission denied when accessing microphone: {{error}}": "",
"Playground": "Playground", "Playground": "",
"Profile": "Profile", "Profile": "",
"Prompt Content": "Prompt Content", "Prompt Content": "",
"Prompt suggestions": "Prompt suggestions", "Prompt suggestions": "",
"Prompts": "Prompts", "Prompts": "",
"Pull a model from Ollama.com": "Pull a model from Ollama.com", "Pull a model from Ollama.com": "",
"Pull Progress": "Pull Progress", "Pull Progress": "",
"Query Params": "Query Params", "Query Params": "",
"RAG Template": "RAG Template", "RAG Template": "",
"Raw Format": "Raw Format", "Raw Format": "",
"Record voice": "Record voice", "Record voice": "",
"Redirecting you to OpenWebUI Community": "Redirecting you to OpenWebUI Community", "Redirecting you to OpenWebUI Community": "",
"Release Notes": "Release Notes", "Release Notes": "",
"Repeat Last N": "Repeat Last N", "Repeat Last N": "",
"Repeat Penalty": "Repeat Penalty", "Repeat Penalty": "",
"Request Mode": "Request Mode", "Request Mode": "",
"Reset Vector Storage": "Reset Vector Storage", "Reset Vector Storage": "",
"Response AutoCopy to Clipboard": "Response AutoCopy to Clipboard", "Response AutoCopy to Clipboard": "",
"Role": "Role", "Role": "",
"Rosé Pine": "Rosé Pine", "Rosé Pine": "",
"Rosé Pine Dawn": "Rosé Pine Dawn", "Rosé Pine Dawn": "",
"Save": "Save", "Save": "",
"Save & Create": "Save & Create", "Save & Create": "",
"Save & Submit": "Save & Submit", "Save & Submit": "",
"Save & Update": "Save & Update", "Save & Update": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through", "Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "",
"Scan": "Scan", "Scan": "",
"Scan complete!": "Scan complete!", "Scan complete!": "",
"Scan for documents from {{path}}": "Scan for documents from {{path}}", "Scan for documents from {{path}}": "",
"Search": "Search", "Search": "",
"Search Documents": "Search Documents", "Search Documents": "",
"Search Prompts": "Search Prompts", "Search Prompts": "",
"See readme.md for instructions": "See readme.md for instructions", "See readme.md for instructions": "",
"See what's new": "See what's new", "See what's new": "",
"Seed": "Seed", "Seed": "",
"Select a mode": "", "Select a mode": "",
"Select a model": "Select a model", "Select a model": "",
"Select an Ollama instance": "", "Select an Ollama instance": "",
"Send a Message": "Send a Message", "Send a Message": "",
"Send message": "Send message", "Send message": "",
"Server connection verified": "Server connection verified", "Server connection verified": "",
"Set as default": "Set as default", "Set as default": "",
"Set Default Model": "Set Default Model", "Set Default Model": "",
"Set Image Size": "Set Image Size", "Set Image Size": "",
"Set Steps": "Set Steps", "Set Steps": "",
"Set Title Auto-Generation Model": "Set Title Auto-Generation Model", "Set Title Auto-Generation Model": "",
"Set Voice": "Set Voice", "Set Voice": "",
"Settings": "Settings", "Settings": "",
"Settings saved successfully!": "Settings saved successfully!", "Settings saved successfully!": "",
"Share to OpenWebUI Community": "Share to OpenWebUI Community", "Share to OpenWebUI Community": "",
"short-summary": "short-summary", "short-summary": "",
"Show": "Show", "Show": "",
"Show Additional Params": "Show Additional Params", "Show Additional Params": "",
"Show shortcuts": "Show shortcuts", "Show shortcuts": "",
"sidebar": "sidebar", "sidebar": "",
"Sign in": "Sign in", "Sign in": "",
"Sign Out": "Sign Out", "Sign Out": "",
"Sign up": "Sign up", "Sign up": "",
"Speech recognition error: {{error}}": "Speech recognition error: {{error}}", "Speech recognition error: {{error}}": "",
"Speech-to-Text Engine": "Speech-to-Text Engine", "Speech-to-Text Engine": "",
"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API is not supported in this browser.", "SpeechRecognition API is not supported in this browser.": "",
"Stop Sequence": "Stop Sequence", "Stop Sequence": "",
"STT Settings": "STT Settings", "STT Settings": "",
"Submit": "Submit", "Submit": "",
"Success": "Success", "Success": "",
"Successfully updated.": "Successfully updated.", "Successfully updated.": "",
"Sync All": "Sync All", "Sync All": "",
"System": "System", "System": "",
"System Prompt": "System Prompt", "System Prompt": "",
"Tags": "Tags", "Tags": "",
"Temperature": "Temperature", "Temperature": "",
"Template": "Template", "Template": "",
"Text Completion": "Text Completion", "Text Completion": "",
"Text-to-Speech Engine": "Text-to-Speech Engine", "Text-to-Speech Engine": "",
"Tfs Z": "Tfs Z", "Tfs Z": "",
"Theme": "Theme", "Theme": "",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "This ensures that your valuable conversations are securely saved to your backend database. Thank you!", "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "",
"This setting does not sync across browsers or devices.": "This setting does not sync across browsers or devices.", "This setting does not sync across browsers or devices.": "",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.", "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "",
"Title": "Title", "Title": "",
"Title Auto-Generation": "Title Auto-Generation", "Title Auto-Generation": "",
"Title Generation Prompt": "Title Generation Prompt", "Title Generation Prompt": "",
"to": "to", "to": "",
"To access the available model names for downloading,": "To access the available model names for downloading,", "To access the available model names for downloading,": "",
"To access the GGUF models available for downloading,": "To access the GGUF models available for downloading,", "To access the GGUF models available for downloading,": "",
"to chat input.": "to chat input.", "to chat input.": "",
"Toggle settings": "Toggle settings", "Toggle settings": "",
"Toggle sidebar": "Toggle sidebar", "Toggle sidebar": "",
"Top K": "Top K", "Top K": "",
"Top P": "Top P", "Top P": "",
"Trouble accessing Ollama?": "Trouble accessing Ollama?", "Trouble accessing Ollama?": "",
"TTS Settings": "TTS Settings", "TTS Settings": "",
"Type Hugging Face Resolve (Download) URL": "", "Type Hugging Face Resolve (Download) URL": "",
"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! There was an issue connecting to {{provider}}.", "Uh-oh! There was an issue connecting to {{provider}}.": "",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Unknown File Type '{{file_type}}', but accepting and treating as plain text", "Unknown File Type '{{file_type}}', but accepting and treating as plain text": "",
"Update password": "Update password", "Update password": "",
"Upload a GGUF model": "Upload a GGUF model", "Upload a GGUF model": "",
"Upload files": "Upload files", "Upload files": "",
"Upload Progress": "Upload Progress", "Upload Progress": "",
"URL Mode": "URL Mode", "URL Mode": "",
"Use '#' in the prompt input to load and select your documents.": "Use '#' in the prompt input to load and select your documents.", "Use '#' in the prompt input to load and select your documents.": "",
"Use Gravatar": "", "Use Gravatar": "",
"user": "user", "user": "",
"User Permissions": "User Permissions", "User Permissions": "",
"Users": "Users", "Users": "",
"Utilize": "Utilize", "Utilize": "",
"Valid time units:": "Valid time units:", "Valid time units:": "",
"variable": "variable", "variable": "",
"variable to have them replaced with clipboard content.": "variable to have them replaced with clipboard content.", "variable to have them replaced with clipboard content.": "",
"Version": "Version", "Version": "",
"Web": "Web", "Web": "",
"WebUI Add-ons": "WebUI Add-ons", "WebUI Add-ons": "",
"WebUI Settings": "WebUI Settings", "WebUI Settings": "",
"WebUI will make requests to": "WebUI will make requests to", "WebUI will make requests to": "",
"Whats New in": "Whats New in", "Whats New in": "",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "When history is turned off, new chats on this browser won't appear in your history on any of your devices.", "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "",
"Whisper (Local)": "Whisper (Local)", "Whisper (Local)": "",
"Write a prompt suggestion (e.g. Who are you?)": "Write a prompt suggestion (e.g. Who are you?)", "Write a prompt suggestion (e.g. Who are you?)": "",
"Write a summary in 50 words that summarizes [topic or keyword].": "Write a summary in 50 words that summarizes [topic or keyword].", "Write a summary in 50 words that summarizes [topic or keyword].": "",
"You": "You", "You": "",
"You're a helpful assistant.": "You're a helpful assistant.", "You're a helpful assistant.": "",
"You're now logged in.": "You're now logged in." "You're now logged in.": ""
} }

View file

@ -360,4 +360,4 @@
"You": "Tu", "You": "Tu",
"You're a helpful assistant.": "Sei un assistente utile.", "You're a helpful assistant.": "Sei un assistente utile.",
"You're now logged in.": "Ora hai effettuato l'accesso." "You're now logged in.": "Ora hai effettuato l'accesso."
} }

View file

@ -360,4 +360,4 @@
"You": "あなた", "You": "あなた",
"You're a helpful assistant.": "あなたは役に立つアシスタントです。", "You're a helpful assistant.": "あなたは役に立つアシスタントです。",
"You're now logged in.": "ログインしました。" "You're now logged in.": "ログインしました。"
} }

View file

@ -0,0 +1,362 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'초', '분', '시간', '일', '주' 또는 만료 없음 '-1'",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(예: `sh webui.sh --api`)",
"(latest)": "(latest)",
"{{modelName}} is thinking...": "{{modelName}} 이(가) 생각중입니다....",
"{{webUIName}} Backend Required": "{{webUIName}} 백엔드가 필요합니다.",
"a user": "사용자",
"About": "소개",
"Account": "계정",
"Action": "액션",
"Add a model": "모델 추가",
"Add a model tag name": "모델 태그명 추가",
"Add a short description about what this modelfile does": "이 모델파일이 하는 일에 대한 간단한 설명 추가",
"Add a short title for this prompt": "이 프롬프트에 대한 간단한 제목 추가",
"Add a tag": "태그 추가",
"Add Docs": "문서 추가",
"Add Files": "파일 추가",
"Add message": "메시지 추가",
"add tags": "태그들 추가",
"Adjusting these settings will apply changes universally to all users.": "이 설정을 조정하면 모든 사용자에게 적용됩니다.",
"admin": "관리자",
"Admin Panel": "관리자 패널",
"Admin Settings": "관리자 설정",
"Advanced Parameters": "고급 매개변수",
"all": "모두",
"All Users": "모든 사용자",
"Allow": "허용",
"Allow Chat Deletion": "채팅 삭제 허용",
"alphanumeric characters and hyphens": "영문자,숫자 및 하이픈",
"Already have an account?": "이미 계정이 있으신가요?",
"an assistant": "어시스턴트",
"and": "그리고",
"API Base URL": "API 기본 URL",
"API Key": "API키",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "허용됩니다 - 이 명령을 활성화하려면 입력하세요.",
"Are you sure?": "확실합니까?",
"Audio": "오디오",
"Auto-playback response": "응답 자동 재생",
"Auto-send input after 3 sec.": "3초 후 입력 자동 전송",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Base URL이 필요합니다.",
"available!": "사용 가능!",
"Back": "뒤로가기",
"Builder Mode": "빌더 모드",
"Cancel": "취소",
"Categories": "분류",
"Change Password": "비밀번호 변경",
"Chat": "채팅",
"Chat History": "채팅 기록",
"Chat History is off for this browser.": "이 브라우저에서 채팅 기록이 꺼져 있습니다.",
"Chats": "채팅",
"Check Again": "다시 확인",
"Check for updates": "업데이트 확인",
"Checking for updates...": "업데이트 확인중...",
"Choose a model before saving...": "저장하기 전에 모델을 선택하세요...",
"Chunk Overlap": "Chunk Overlap",
"Chunk Params": "Chunk Params",
"Chunk Size": "Chunk Size",
"Click here for help.": "도움말을 보려면 여기를 클릭하세요.",
"Click here to check other modelfiles.": "다른 모델파일을 확인하려면 여기를 클릭하세요.",
"Click here to select": "선택하려면 여기를 클릭하세요.",
"Click here to select documents.": "문서를 선택하려면 여기를 클릭하세요.",
"click here.": "여기를 클릭하세요.",
"Click on the user role button to change a user's role.": "사용자 역할 버튼을 클릭하여 사용자의 역할을 변경하세요.",
"Close": "닫기",
"Collection": "컬렉션",
"Command": "명령",
"Confirm Password": "비밀번호 확인",
"Connections": "연결",
"Content": "내용",
"Context Length": "내용 길이",
"Conversation Mode": "대화 모드",
"Copy last code block": "마지막 코드 블록 복사",
"Copy last response": "마지막 응답 복사",
"Copying to clipboard was successful!": "클립보드에 복사되었습니다!",
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "다음 질문에 대한 제목으로 간결한 3-5 단어 문구를 만드되 3-5 단어 제한을 엄격히 준수하고 'title' 단어 사용을 피하세요:",
"Create a modelfile": "모델파일 만들기",
"Create Account": "계정 만들기",
"Created at": "생성일",
"Created by": "생성자",
"Current Model": "현재 모델",
"Current Password": "현재 비밀번호",
"Custom": "사용자 정의",
"Customize Ollama models for a specific purpose": "특정 목적으로 Ollama 모델 사용자 정의",
"Dark": "어두운",
"Database": "데이터베이스",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "기본값",
"Default (Automatic1111)": "기본값 (Automatic1111)",
"Default (Web API)": "기본값 (Web API)",
"Default model updated": "기본 모델이 업데이트되었습니다.",
"Default Prompt Suggestions": "기본 프롬프트 제안",
"Default User Role": "기본 사용자 역할",
"delete": "삭제",
"Delete a model": "모델 삭제",
"Delete chat": "채팅 삭제",
"Delete Chats": "채팅들 삭제",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨",
"Deleted {tagName}": "{tagName} 삭제됨",
"Description": "설명",
"Desktop Notifications": "알림",
"Disabled": "비활성화",
"Discover a modelfile": "모델파일 검색",
"Discover a prompt": "프롬프트 검색",
"Discover, download, and explore custom prompts": "사용자 정의 프롬프트 검색, 다운로드 및 탐색",
"Discover, download, and explore model presets": "모델 사전 설정 검색, 다운로드 및 탐색",
"Display the username instead of You in the Chat": "채팅에서 'You' 대신 사용자 이름 표시",
"Document": "문서",
"Document Settings": "문서 설정",
"Documents": "문서들",
"does not make any external connections, and your data stays securely on your locally hosted server.": "어떠한 외부 연결도 하지 않으며, 데이터는 로컬에서 호스팅되는 서버에 안전하게 유지됩니다.",
"Don't Allow": "허용 안 함",
"Don't have an account?": "계정이 없으신가요?",
"Download as a File": "파일로 다운로드",
"Download Database": "데이터베이스 다운로드",
"Drop any files here to add to the conversation": "대화에 추가할 파일을 여기에 드롭하세요.",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "예: '30s','10m'. 유효한 시간 단위는 's', 'm', 'h'입니다.",
"Edit Doc": "문서 편집",
"Edit User": "사용자 편집",
"Email": "이메일",
"Enable Chat History": "채팅 기록 활성화",
"Enable New Sign Ups": "새 회원가입 활성화",
"Enabled": "활성화",
"Enter {{role}} message here": "여기에 {{role}} 메시지 입력",
"Enter API Key": "API 키 입력",
"Enter Chunk Overlap": "청크 오버랩 입력",
"Enter Chunk Size": "청크 크기 입력",
"Enter Image Size (e.g. 512x512)": "이미지 크기 입력(예: 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "LiteLLM API 기본 URL 입력(litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "LiteLLM API 키 입력(litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM 입력(litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "LiteLLM 모델 입력(litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "최대 토큰 수 입력(litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "모델 태그 입력(예: {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "단계 수 입력(예: 50)",
"Enter stop sequence": "중지 시퀀스 입력",
"Enter Top K": "Top K 입력",
"Enter URL (e.g. http://127.0.0.1:7860/)": "URL 입력(예: http://127.0.0.1:7860/)",
"Enter Your Email": "이메일 입력",
"Enter Your Full Name": "전체 이름 입력",
"Enter Your Password": "비밀번호 입력",
"Experimental": "실험적",
"Export All Chats (All Users)": "모든 채팅 내보내기 (모든 사용자)",
"Export Chats": "채팅 내보내기",
"Export Documents Mapping": "문서 매핑 내보내기",
"Export Modelfiles": "모델파일 내보내기",
"Export Prompts": "프롬프트 내보내기",
"Failed to read clipboard contents": "클립보드 내용을 읽는 데 실패했습니다.",
"File Mode": "파일 모드",
"File not found.": "파일을 찾을 수 없습니다.",
"Focus chat input": "채팅 입력 포커스",
"Format your variables using square brackets like this:": "이렇게 대괄호를 사용하여 변수를 형식화하세요:",
"From (Base Model)": "출처(기본 모델)",
"Full Screen Mode": "전체 화면 모드",
"General": "일반",
"General Settings": "일반 설정",
"Hello, {{name}}": "안녕하세요, {{name}}",
"Hide": "숨기기",
"Hide Additional Params": "추가 매개변수 숨기기",
"How can I help you today?": "오늘 어떻게 도와드릴까요?",
"Image Generation (Experimental)": "이미지 생성(실험적)",
"Image Generation Engine": "이미지 생성 엔진",
"Image Settings": "이미지 설정",
"Images": "이미지",
"Import Chats": "채팅 가져오기",
"Import Documents Mapping": "문서 매핑 가져오기",
"Import Modelfiles": "모델파일 가져오기",
"Import Prompts": "프롬프트 가져오기",
"Include --api flag when running stable-diffusion-webui": "stable-diffusion-webui를 실행할 때 --api 플래그 포함",
"Interface": "인터페이스",
"join our Discord for help.": "도움말을 보려면 Discord에 가입하세요.",
"JSON": "JSON",
"JWT Expiration": "JWT 만료",
"JWT Token": "JWT 토큰",
"Keep Alive": "계속 유지하기",
"Keyboard shortcuts": "키보드 단축키",
"Language": "언어",
"Light": "밝음",
"Listening...": "청취 중...",
"LLMs can make mistakes. Verify important information.": "LLM은 실수를 할 수 있습니다. 중요한 정보를 확인하세요.",
"Made by OpenWebUI Community": "OpenWebUI 커뮤니티에서 제작",
"Make sure to enclose them with": "다음으로 묶는 것을 잊지 마세요:",
"Manage LiteLLM Models": "LiteLLM 모델 관리",
"Manage Models": "모델 관리",
"Manage Ollama Models": "Ollama 모델 관리",
"Max Tokens": "최대 토큰 수",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "최대 3개의 모델을 동시에 다운로드할 수 있습니다. 나중에 다시 시도하세요.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"Model '{{modelName}}' has been successfully downloaded.": "모델 '{{modelName}}'이(가) 성공적으로 다운로드되었습니다.",
"Model '{{modelTag}}' is already in queue for downloading.": "모델 '{{modelTag}}'이(가) 이미 다운로드 대기열에 있습니다.",
"Model {{modelId}} not found": "모델 {{modelId}}를 찾을 수 없습니다.",
"Model {{modelName}} already exists.": "모델 {{modelName}}이(가) 이미 존재합니다.",
"Model Name": "모델 이름",
"Model not selected": "모델이 선택되지 않았습니다.",
"Model Tag Name": "모델 태그 이름",
"Model Whitelisting": "모델 허용 목록",
"Model(s) Whitelisted": "허용된 모델",
"Modelfile": "모델파일",
"Modelfile Advanced Settings": "모델파일 고급 설정",
"Modelfile Content": "모델파일 내용",
"Modelfiles": "모델파일",
"Models": "모델",
"My Documents": "내 문서",
"My Modelfiles": "내 모델파일",
"My Prompts": "내 프롬프트",
"Name": "이름",
"Name Tag": "이름 태그",
"Name your modelfile": "모델파일 이름 지정",
"New Chat": "새 채팅",
"New Password": "새 비밀번호",
"Not sure what to add?": "추가할 것이 궁금하세요?",
"Not sure what to write? Switch to": "무엇을 쓸지 모르겠나요? 전환하세요.",
"Off": "끄기",
"Okay, Let's Go!": "그렇습니다, 시작합시다!",
"Ollama Base URL": "Ollama 기본 URL",
"Ollama Version": "Ollama 버전",
"On": "켜기",
"Only": "오직",
"Only alphanumeric characters and hyphens are allowed in the command string.": "명령어 문자열에는 영문자, 숫자 및 하이픈만 허용됩니다.",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "이런! 잠시만 기다려 주세요! 파일이 아직 처리 중입니다. 완벽하게 준비하고 있습니다. 잠시만 기다려주시면 준비가 되면 알려드리겠습니다.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "이런! URL이 잘못된 것 같습니다. 다시 한번 확인하고 다시 시도해주세요.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "이런! 지원되지 않는 방식(프론트엔드 전용)을 사용하고 계십니다. 백엔드에서 WebUI를 제공해주세요.",
"Open": "열기",
"Open AI": "Open AI",
"Open AI (Dall-E)": "OpenAI (Dall-E)",
"Open new chat": "새 채팅 열기",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "OpenAI API 키",
"OpenAI API Key is required.": "OpenAI API 키가 필요합니다.",
"or": "또는",
"Parameters": "매개변수",
"Password": "비밀번호",
"PDF Extract Images (OCR)": "PDF에서 이미지 추출 (OCR)",
"pending": "보류 중",
"Permission denied when accessing microphone: {{error}}": "마이크 액세스가 거부되었습니다: {{error}}",
"Playground": "놀이터",
"Profile": "프로필",
"Prompt Content": "프롬프트 내용",
"Prompt suggestions": "프롬프트 제안",
"Prompts": "프롬프트",
"Pull a model from Ollama.com": "Ollama.com에서 모델 가져오기",
"Pull Progress": "가져오기 진행 상황",
"Query Params": "쿼리 매개변수",
"RAG Template": "RAG 템플릿",
"Raw Format": "Raw 형식",
"Record voice": "음성 녹음",
"Redirecting you to OpenWebUI Community": "OpenWebUI 커뮤니티로 리디렉션하는 중",
"Release Notes": "릴리스 노트",
"Repeat Last N": "마지막 N 반복",
"Repeat Penalty": "반복 패널티",
"Request Mode": "요청 모드",
"Reset Vector Storage": "벡터 스토리지 초기화",
"Response AutoCopy to Clipboard": "응답 자동 클립보드 복사",
"Role": "역할",
"Rosé Pine": "로제 파인",
"Rosé Pine Dawn": "로제 파인 던",
"Save": "저장",
"Save & Create": "저장 및 생성",
"Save & Submit": "저장 및 제출",
"Save & Update": "저장 및 업데이트",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "브라우저의 저장소에 채팅 로그를 직접 저장하는 것은 더 이상 지원되지 않습니다. 아래 버튼을 클릭하여 채팅 로그를 다운로드하고 삭제하세요. 걱정 마세요. 백엔드를 통해 채팅 로그를 쉽게 다시 가져올 수 있습니다.",
"Scan": "스캔",
"Scan complete!": "스캔 완료!",
"Scan for documents from {{path}}": "{{path}}에서 문서 스캔",
"Search": "검색",
"Search Documents": "문서 검색",
"Search Prompts": "프롬프트 검색",
"See readme.md for instructions": "설명은 readme.md를 참조하세요.",
"See what's new": "새로운 기능 보기",
"Seed": "시드",
"Select a mode": "모드 선택",
"Select a model": "모델 선택",
"Select an Ollama instance": "Ollama 인스턴스 선택",
"Send a Message": "메시지 보내기",
"Send message": "메시지 보내기",
"Server connection verified": "서버 연결 확인됨",
"Set as default": "기본값으로 설정",
"Set Default Model": "기본 모델 설정",
"Set Image Size": "이미지 크기 설정",
"Set Steps": "단계 설정",
"Set Title Auto-Generation Model": "제목 자동 생성 모델 설정",
"Set Voice": "음성 설정",
"Settings": "설정",
"Settings saved successfully!": "설정이 성공적으로 저장되었습니다!",
"Share to OpenWebUI Community": "OpenWebUI 커뮤니티에 공유",
"short-summary": "간단한 요약",
"Show": "보이기",
"Show Additional Params": "추가 매개변수 보기",
"Show shortcuts": "단축키 보기",
"sidebar": "사이드바",
"Sign in": "로그인",
"Sign Out": "로그아웃",
"Sign up": "가입",
"Speech recognition error: {{error}}": "음성 인식 오류: {{error}}",
"Speech-to-Text Engine": "음성-텍스트 엔진",
"SpeechRecognition API is not supported in this browser.": "이 브라우저에서는 SpeechRecognition API를 지원하지 않습니다.",
"Stop Sequence": "중지 시퀀스",
"STT Settings": "STT 설정",
"Submit": "제출",
"Success": "성공",
"Successfully updated.": "성공적으로 업데이트되었습니다.",
"Sync All": "모두 동기화",
"System": "시스템",
"System Prompt": "시스템 프롬프트",
"Tags": "Tags",
"Temperature": "Temperature",
"Template": "Template",
"Text Completion": "텍스트 완성",
"Text-to-Speech Engine": "텍스트-음성 엔진",
"Tfs Z": "Tfs Z",
"Theme": "테마",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "이렇게 하면 소중한 대화 내용이 백엔드 데이터베이스에 안전하게 저장됩니다. 감사합니다!",
"This setting does not sync across browsers or devices.": "이 설정은 브라우저 또는 장치 간에 동기화되지 않습니다.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "팁: 각 대체 후 채팅 입력에서 탭 키를 눌러 여러 개의 변수 슬롯을 연속적으로 업데이트하세요.",
"Title": "제목",
"Title Auto-Generation": "제목 자동 생성",
"Title Generation Prompt": "제목 생성 프롬프트",
"to": "~까지",
"To access the available model names for downloading,": "다운로드 가능한 모델명을 확인하려면,",
"To access the GGUF models available for downloading,": "다운로드 가능한 GGUF 모델을 확인하려면,",
"to chat input.": "채팅 입력으로.",
"Toggle settings": "설정 전환",
"Toggle sidebar": "사이드바 전환",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Ollama에 접근하는 데 문제가 있나요?",
"TTS Settings": "TTS 설정",
"Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (다운로드) URL 입력",
"Uh-oh! There was an issue connecting to {{provider}}.": "앗! {{provider}}에 연결하는 데 문제가 있었습니다.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "알 수 없는 파일 유형 '{{file_type}}', 하지만 일반 텍스트로 허용하고 처리합니다.",
"Update password": "비밀번호 업데이트",
"Upload a GGUF model": "GGUF 모델 업로드",
"Upload files": "파일 업로드",
"Upload Progress": "업로드 진행 상황",
"URL Mode": "URL 모드",
"Use '#' in the prompt input to load and select your documents.": "프롬프트 입력에서 '#'를 사용하여 문서를 로드하고 선택하세요.",
"Use Gravatar": "Gravatar 사용",
"user": "사용자",
"User Permissions": "사용자 권한",
"Users": "사용자",
"Utilize": "활용",
"Valid time units:": "유효한 시간 단위:",
"variable": "변수",
"variable to have them replaced with clipboard content.": "변수를 사용하여 클립보드 내용으로 바꾸세요.",
"Version": "버전",
"Web": "웹",
"WebUI Add-ons": "WebUI 애드온",
"WebUI Settings": "WebUI 설정",
"WebUI will make requests to": "WebUI가 요청할 대상:",
"What's New in": "새로운 기능",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "기록 기능이 꺼져 있으면 이 브라우저의 새 채팅이 다른 장치의 채팅 기록에 나타나지 않습니다.",
"Whisper (Local)": "위스퍼 (Local)",
"Write a prompt suggestion (e.g. Who are you?)": "프롬프트 제안 작성 (예: 당신은 누구인가요?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "[주제 또는 키워드]에 대한 50단어 요약문 작성.",
"You": "당신",
"You're a helpful assistant.": "당신은 유용한 어시스턴트입니다.",
"You're now logged in.": "로그인되었습니다."
}

View file

@ -39,6 +39,10 @@
"code": "ja-JP", "code": "ja-JP",
"title": "Japanese" "title": "Japanese"
}, },
{
"code": "ko-KR",
"title": "Korean"
},
{ {
"code": "nl-NL", "code": "nl-NL",
"title": "Dutch (Netherlands)" "title": "Dutch (Netherlands)"

View file

@ -48,6 +48,7 @@
let messagesContainerElement: HTMLDivElement; let messagesContainerElement: HTMLDivElement;
let currentRequestId = null; let currentRequestId = null;
let showModelSelector = false;
let selectedModels = ['']; let selectedModels = [''];
let selectedModelfile = null; let selectedModelfile = null;
@ -533,6 +534,8 @@
console.log(docs); console.log(docs);
console.log(model);
const res = await generateOpenAIChatCompletion( const res = await generateOpenAIChatCompletion(
localStorage.token, localStorage.token,
{ {
@ -585,7 +588,9 @@
max_tokens: $settings?.options?.num_predict ?? undefined, max_tokens: $settings?.options?.num_predict ?? undefined,
docs: docs.length > 0 ? docs : undefined docs: docs.length > 0 ? docs : undefined
}, },
model.source === 'litellm' ? `${LITELLM_API_BASE_URL}/v1` : `${OPENAI_API_BASE_URL}` model?.source?.toLowerCase() === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
); );
if (res && res.ok) { if (res && res.ok) {
@ -776,7 +781,7 @@
titleModelId, titleModelId,
userPrompt, userPrompt,
titleModel?.external ?? false titleModel?.external ?? false
? titleModel.source === 'litellm' ? titleModel?.source?.toLowerCase() === 'litellm'
? `${LITELLM_API_BASE_URL}/v1` ? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}` : `${OPENAI_API_BASE_URL}`
: `${OLLAMA_API_BASE_URL}/v1` : `${OLLAMA_API_BASE_URL}/v1`
@ -837,7 +842,16 @@
</svelte:head> </svelte:head>
<div class="h-screen max-h-[100dvh] w-full flex flex-col"> <div class="h-screen max-h-[100dvh] w-full flex flex-col">
<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} {tags} {addTag} {deleteTag} /> <Navbar
{title}
bind:selectedModels
bind:showModelSelector
shareEnabled={messages.length > 0}
{initNewChat}
{tags}
{addTag}
{deleteTag}
/>
<div class="flex flex-col flex-auto"> <div class="flex flex-col flex-auto">
<div <div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
@ -849,15 +863,7 @@
messagesContainerElement.clientHeight + 5; messagesContainerElement.clientHeight + 5;
}} }}
> >
<div <div class=" h-full w-full flex flex-col pt-2 pb-4">
class="{$settings?.fullScreenMode ?? null
? 'max-w-full'
: 'max-w-2xl md:px-0'} mx-auto w-full px-4"
>
<ModelSelector bind:selectedModels />
</div>
<div class=" h-full w-full flex flex-col py-8">
<Messages <Messages
chatId={$chatId} chatId={$chatId}
{selectedModels} {selectedModels}

View file

@ -56,8 +56,10 @@
let currentRequestId = null; let currentRequestId = null;
// let chatId = $page.params.id; // let chatId = $page.params.id;
let showModelSelector = false;
let selectedModels = ['']; let selectedModels = [''];
let selectedModelfile = null; let selectedModelfile = null;
$: selectedModelfile = $: selectedModelfile =
selectedModels.length === 1 && selectedModels.length === 1 &&
$modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0 $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0
@ -600,7 +602,9 @@
max_tokens: $settings?.options?.num_predict ?? undefined, max_tokens: $settings?.options?.num_predict ?? undefined,
docs: docs.length > 0 ? docs : undefined docs: docs.length > 0 ? docs : undefined
}, },
model.source === 'litellm' ? `${LITELLM_API_BASE_URL}/v1` : `${OPENAI_API_BASE_URL}` model?.source?.toLowerCase() === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
); );
if (res && res.ok) { if (res && res.ok) {
@ -791,7 +795,7 @@
titleModelId, titleModelId,
userPrompt, userPrompt,
titleModel?.external ?? false titleModel?.external ?? false
? titleModel.source === 'litellm' ? titleModel?.source?.toLowerCase() === 'litellm'
? `${LITELLM_API_BASE_URL}/v1` ? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}` : `${OPENAI_API_BASE_URL}`
: `${OLLAMA_API_BASE_URL}/v1` : `${OLLAMA_API_BASE_URL}/v1`
@ -861,6 +865,8 @@
<div class="min-h-screen max-h-screen w-full flex flex-col"> <div class="min-h-screen max-h-screen w-full flex flex-col">
<Navbar <Navbar
{title} {title}
bind:selectedModels
bind:showModelSelector
shareEnabled={messages.length > 0} shareEnabled={messages.length > 0}
initNewChat={async () => { initNewChat={async () => {
if (currentRequestId !== null) { if (currentRequestId !== null) {
@ -885,15 +891,7 @@
messagesContainerElement.clientHeight + 5; messagesContainerElement.clientHeight + 5;
}} }}
> >
<div <div class=" h-full w-full flex flex-col py-4">
class="{$settings?.fullScreenMode ?? null
? 'max-w-full'
: 'max-w-2xl md:px-0'} mx-auto w-full px-4"
>
<ModelSelector bind:selectedModels />
</div>
<div class=" h-full w-full flex flex-col py-8">
<Messages <Messages
chatId={$chatId} chatId={$chatId}
{selectedModels} {selectedModels}

View file

@ -18,6 +18,7 @@
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import ChatCompletion from '$lib/components/playground/ChatCompletion.svelte'; import ChatCompletion from '$lib/components/playground/ChatCompletion.svelte';
import Selector from '$lib/components/chat/ModelSelector/Selector.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -315,27 +316,24 @@
</div> </div>
</div> </div>
<div class=" flex gap-1 px-1"> <div class="flex flex-col gap-1 px-1 w-full">
<select <div class="flex w-full">
id="models" <div class="overflow-hidden w-full">
class="outline-none bg-transparent text-sm font-medium rounded-lg w-full placeholder-gray-400" <div class="max-w-full">
<Selector
placeholder={$i18n.t('Select a model')}
items={$models
.filter((model) => model.name !== 'hr')
.map((model) => ({
value: model.id,
label: model.name,
info: model
}))}
bind:value={selectedModelId} bind:value={selectedModelId}
> />
<option class=" text-gray-800" value="" selected disabled </div>
>{$i18n.t('Select a model')}</option </div>
> </div>
{#each $models as model}
{#if model.name === 'hr'}
<hr />
{:else}
<option value={model.id} class="text-gray-800 text-lg"
>{model.name +
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
>
{/if}
{/each}
</select>
<!-- <button <!-- <button
class=" self-center dark:hover:text-gray-300" class=" self-center dark:hover:text-gray-300"

View file

@ -11,7 +11,7 @@
import '../tailwind.css'; import '../tailwind.css';
import 'tippy.js/dist/tippy.css'; import 'tippy.js/dist/tippy.css';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import i18n from '$lib/i18n'; import i18n, { initI18n } from '$lib/i18n';
setContext('i18n', i18n); setContext('i18n', i18n);
@ -25,6 +25,11 @@
if (backendConfig) { if (backendConfig) {
// Save Backend Status to Store // Save Backend Status to Store
await config.set(backendConfig); await config.set(backendConfig);
if ($config.default_locale) {
initI18n($config.default_locale);
} else {
initI18n();
}
await WEBUI_NAME.set(backendConfig.name); await WEBUI_NAME.set(backendConfig.name);
console.log(backendConfig); console.log(backendConfig);

View file

@ -3,7 +3,8 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
html, pre { html,
pre {
font-family: -apple-system, 'Arimo', ui-sans-serif, system-ui, 'Segoe UI', Roboto, Ubuntu, font-family: -apple-system, 'Arimo', ui-sans-serif, system-ui, 'Segoe UI', Roboto, Ubuntu,
Cantarell, 'Noto Sans', sans-serif, 'Helvetica Neue', Arial, 'Apple Color Emoji', Cantarell, 'Noto Sans', sans-serif, 'Helvetica Neue', Arial, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';

View file

@ -16,6 +16,12 @@ const config = {
assets: 'build', assets: 'build',
fallback: 'index.html' fallback: 'index.html'
}) })
},
onwarn: (warning, handler) => {
const { code, _ } = warning;
if (code === 'css-unused-selector') return;
handler(warning);
} }
}; };

View file

@ -16,9 +16,8 @@ export default {
700: '#4e4e4e', 700: '#4e4e4e',
800: '#333', 800: '#333',
850: '#262626', 850: '#262626',
900: 'var(--color-gray-900, #171717)',
900: '#171717', 950: 'var(--color-gray-950, #0d0d0d)'
950: '#0d0d0d'
} }
}, },
typography: { typography: {

View file

@ -1,6 +0,0 @@
{
"model_name": "string",
"litellm_params": {
"model": "ollama/mistral"
}
}