Merge branch 'dev' into feat/system-wide-theme

This commit is contained in:
Danny Liu 2024-03-16 23:23:56 -07:00 committed by GitHub
commit db0712aefd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 8000 additions and 1164 deletions

View file

@ -5,6 +5,42 @@ 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.113] - 2024-03-XX
### Added
- 🌍 **Localization**: You can now change the UI language in Settings -> General. We support Ukrainian, German, Farsi (Persian), Traditional and Simplified Chinese and French translations. You can help us to translate the UI into your language! More info in our [CONTRIBUTION.md](https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization).
### Fixed
- 🌑 **Dark background on select fields**: Added dark background to select fields, as this caused bad readability on some browsers/devices.
## [0.1.112] - 2024-03-15
### Fixed
- 🗨️ Resolved chat malfunction after image generation.
- 🎨 Fixed various RAG issues.
- 🧪 Rectified experimental broken GGUF upload logic.
## [0.1.111] - 2024-03-10
### Added
- 🛡️ **Model Whitelisting**: Admins now have the ability to whitelist models for users with the 'user' role.
- 🔄 **Update All Models**: Added a convenient button to update all models at once.
- 📄 **Toggle PDF OCR**: Users can now toggle PDF OCR option for improved parsing performance.
- 🎨 **DALL-E Integration**: Introduced DALL-E integration for image generation alongside automatic1111.
- 🛠️ **RAG API Refactoring**: Refactored RAG logic and exposed its API, with additional documentation to follow.
### Fixed
- 🔒 **Max Token Settings**: Added max token settings for anthropic/claude-3-sonnet-20240229 (Issue #1094).
- 🔧 **Misalignment Issue**: Corrected misalignment of Edit and Delete Icons when Chat Title is Empty (Issue #1104).
- 🔄 **Context Loss Fix**: Resolved RAG losing context on model response regeneration with Groq models via API key (Issue #1105).
- 📁 **File Handling Bug**: Addressed File Not Found Notification when Dropping a Conversation Element (Issue #1098).
- 🖱️ **Dragged File Styling**: Fixed dragged file layover styling issue.
## [0.1.110] - 2024-03-06 ## [0.1.110] - 2024-03-06
### Added ### Added

View file

@ -11,7 +11,7 @@
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s) [![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck) [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
User-friendly WebUI for LLMs, supported LLM runners include Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/). Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
![Open WebUI Demo](./demo.gif) ![Open WebUI Demo](./demo.gif)

View file

@ -293,6 +293,7 @@ def generate_image(
"size": form_data.size if form_data.size else app.state.IMAGE_SIZE, "size": form_data.size if form_data.size else app.state.IMAGE_SIZE,
"response_format": "b64_json", "response_format": "b64_json",
} }
r = requests.post( r = requests.post(
url=f"https://api.openai.com/v1/images/generations", url=f"https://api.openai.com/v1/images/generations",
json=data, json=data,
@ -300,7 +301,6 @@ def generate_image(
) )
r.raise_for_status() r.raise_for_status()
res = r.json() res = r.json()
images = [] images = []
@ -356,7 +356,10 @@ def generate_image(
return images return images
except Exception as e: except Exception as e:
print(e) error = e
if r:
print(r.json()) if r != None:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) data = r.json()
if "error" in data:
error = data["error"]["message"]
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))

View file

@ -15,7 +15,7 @@ import asyncio
from apps.web.models.users import Users from apps.web.models.users import Users
from constants import ERROR_MESSAGES 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 OLLAMA_BASE_URLS from config import OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST
from typing import Optional, List, Union from typing import Optional, List, Union
@ -29,6 +29,10 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
app.state.MODELS = {} app.state.MODELS = {}
@ -119,6 +123,7 @@ async def get_all_models():
map(lambda response: response["models"], responses) map(lambda response: response["models"], responses)
) )
} }
app.state.MODELS = {model["model"]: model for model in models["models"]} app.state.MODELS = {model["model"]: model for model in models["models"]}
return models return models
@ -129,9 +134,19 @@ async def get_all_models():
async def get_ollama_tags( async def get_ollama_tags(
url_idx: Optional[int] = None, user=Depends(get_current_user) url_idx: Optional[int] = None, user=Depends(get_current_user)
): ):
if url_idx == None: if url_idx == None:
return await get_all_models() models = await get_all_models()
if app.state.MODEL_FILTER_ENABLED:
if user.role == "user":
models["models"] = list(
filter(
lambda model: model["name"] in app.state.MODEL_FILTER_LIST,
models["models"],
)
)
return models
return models
else: else:
url = app.state.OLLAMA_BASE_URLS[url_idx] url = app.state.OLLAMA_BASE_URLS[url_idx]
try: try:
@ -167,11 +182,17 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
responses = await asyncio.gather(*tasks) responses = await asyncio.gather(*tasks)
responses = list(filter(lambda x: x is not None, responses)) responses = list(filter(lambda x: x is not None, responses))
if len(responses) > 0:
lowest_version = min( lowest_version = min(
responses, key=lambda x: tuple(map(int, x["version"].split("."))) responses, key=lambda x: tuple(map(int, x["version"].split(".")))
) )
return {"version": lowest_version["version"]} return {"version": lowest_version["version"]}
else:
raise HTTPException(
status_code=500,
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
)
else: else:
url = app.state.OLLAMA_BASE_URLS[url_idx] url = app.state.OLLAMA_BASE_URLS[url_idx]
try: try:

View file

@ -18,7 +18,13 @@ from utils.utils import (
get_verified_user, get_verified_user,
get_admin_user, get_admin_user,
) )
from config import OPENAI_API_BASE_URLS, OPENAI_API_KEYS, CACHE_DIR from config import (
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
CACHE_DIR,
MODEL_FILTER_ENABLED,
MODEL_FILTER_LIST,
)
from typing import List, Optional from typing import List, Optional
@ -34,6 +40,9 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
app.state.OPENAI_API_KEYS = OPENAI_API_KEYS app.state.OPENAI_API_KEYS = OPENAI_API_KEYS
@ -102,6 +111,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}" headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
headers["Content-Type"] = "application/json" headers["Content-Type"] = "application/json"
r = None
try: try:
r = requests.post( r = requests.post(
url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech", url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech",
@ -134,7 +144,9 @@ async def speech(request: Request, user=Depends(get_verified_user)):
except: except:
error_detail = f"External: {e}" error_detail = f"External: {e}"
raise HTTPException(status_code=r.status_code, detail=error_detail) raise HTTPException(
status_code=r.status_code if r else 500, detail=error_detail
)
except ValueError: except ValueError:
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND) raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
@ -170,12 +182,18 @@ def merge_models_lists(model_lists):
async def get_all_models(): async def get_all_models():
print("get_all_models") print("get_all_models")
if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "":
models = {"data": []}
else:
tasks = [ tasks = [
fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx]) fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx])
for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS) for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS)
] ]
responses = await asyncio.gather(*tasks) responses = await asyncio.gather(*tasks)
responses = list(filter(lambda x: x is not None and "error" not in x, responses)) responses = list(
filter(lambda x: x is not None and "error" not in x, responses)
)
models = { models = {
"data": merge_models_lists( "data": merge_models_lists(
list(map(lambda response: response["data"], responses)) list(map(lambda response: response["data"], responses))
@ -186,14 +204,26 @@ async def get_all_models():
return models return models
# , user=Depends(get_current_user)
@app.get("/models") @app.get("/models")
@app.get("/models/{url_idx}") @app.get("/models/{url_idx}")
async def get_models(url_idx: Optional[int] = None): async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
if url_idx == None: if url_idx == None:
return await get_all_models() models = await get_all_models()
if app.state.MODEL_FILTER_ENABLED:
if user.role == "user":
models["data"] = list(
filter(
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
models["data"],
)
)
return models
return models
else: else:
url = app.state.OPENAI_API_BASE_URLS[url_idx] url = app.state.OPENAI_API_BASE_URLS[url_idx]
r = None
try: try:
r = requests.request(method="GET", url=f"{url}/models") r = requests.request(method="GET", url=f"{url}/models")
r.raise_for_status() r.raise_for_status()
@ -266,6 +296,8 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
headers["Authorization"] = f"Bearer {key}" headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json" headers["Content-Type"] = "application/json"
r = None
try: try:
r = requests.request( r = requests.request(
method=request.method, method=request.method,
@ -298,4 +330,6 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
except: except:
error_detail = f"External: {e}" error_detail = f"External: {e}"
raise HTTPException(status_code=r.status_code, detail=error_detail) raise HTTPException(
status_code=r.status_code if r else 500, detail=error_detail
)

View file

@ -77,6 +77,7 @@ from constants import ERROR_MESSAGES
app = FastAPI() app = FastAPI()
app.state.PDF_EXTRACT_IMAGES = False
app.state.CHUNK_SIZE = CHUNK_SIZE app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.RAG_TEMPLATE = RAG_TEMPLATE app.state.RAG_TEMPLATE = RAG_TEMPLATE
@ -184,12 +185,15 @@ async def update_embedding_model(
} }
@app.get("/chunk") @app.get("/config")
async def get_chunk_params(user=Depends(get_admin_user)): async def get_rag_config(user=Depends(get_admin_user)):
return { return {
"status": True, "status": True,
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
"chunk": {
"chunk_size": app.state.CHUNK_SIZE, "chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP, "chunk_overlap": app.state.CHUNK_OVERLAP,
},
} }
@ -198,17 +202,24 @@ class ChunkParamUpdateForm(BaseModel):
chunk_overlap: int chunk_overlap: int
@app.post("/chunk/update") class ConfigUpdateForm(BaseModel):
async def update_chunk_params( pdf_extract_images: bool
form_data: ChunkParamUpdateForm, user=Depends(get_admin_user) chunk: ChunkParamUpdateForm
):
app.state.CHUNK_SIZE = form_data.chunk_size
app.state.CHUNK_OVERLAP = form_data.chunk_overlap @app.post("/config/update")
async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
app.state.PDF_EXTRACT_IMAGES = form_data.pdf_extract_images
app.state.CHUNK_SIZE = form_data.chunk.chunk_size
app.state.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
return { return {
"status": True, "status": True,
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
"chunk": {
"chunk_size": app.state.CHUNK_SIZE, "chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP, "chunk_overlap": app.state.CHUNK_OVERLAP,
},
} }
@ -364,7 +375,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
] ]
if file_ext == "pdf": if file_ext == "pdf":
loader = PyPDFLoader(file_path, extract_images=True) loader = PyPDFLoader(file_path, extract_images=app.state.PDF_EXTRACT_IMAGES)
elif file_ext == "csv": elif file_ext == "csv":
loader = CSVLoader(file_path) loader = CSVLoader(file_path)
elif file_ext == "rst": elif file_ext == "rst":
@ -389,9 +400,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
elif file_ext in known_source_ext or ( elif file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0 file_content_type and file_content_type.find("text/") >= 0
): ):
loader = TextLoader(file_path) loader = TextLoader(file_path, autodetect_encoding=True)
else: else:
loader = TextLoader(file_path) loader = TextLoader(file_path, autodetect_encoding=True)
known_type = False known_type = False
return loader, known_type return loader, known_type

View file

@ -91,7 +91,92 @@ def query_collection(
def rag_template(template: str, context: str, query: str): def rag_template(template: str, context: str, query: str):
template = re.sub(r"\[context\]", context, template) template = template.replace("[context]", context)
template = re.sub(r"\[query\]", query, template) template = template.replace("[query]", query)
return template return template
def rag_messages(docs, messages, template, k, embedding_function):
print(docs)
last_user_message_idx = None
for i in range(len(messages) - 1, -1, -1):
if messages[i]["role"] == "user":
last_user_message_idx = i
break
user_message = messages[last_user_message_idx]
if isinstance(user_message["content"], list):
# Handle list content input
content_type = "list"
query = ""
for content_item in user_message["content"]:
if content_item["type"] == "text":
query = content_item["text"]
break
elif isinstance(user_message["content"], str):
# Handle text content input
content_type = "text"
query = user_message["content"]
else:
# Fallback in case the input does not match expected types
content_type = None
query = ""
relevant_contexts = []
for doc in docs:
context = None
try:
if doc["type"] == "collection":
context = query_collection(
collection_names=doc["collection_names"],
query=query,
k=k,
embedding_function=embedding_function,
)
else:
context = query_doc(
collection_name=doc["collection_name"],
query=query,
k=k,
embedding_function=embedding_function,
)
except Exception as e:
print(e)
context = None
relevant_contexts.append(context)
context_string = ""
for context in relevant_contexts:
if context:
context_string += " ".join(context["documents"][0]) + "\n"
ra_content = rag_template(
template=template,
context=context_string,
query=query,
)
if content_type == "list":
new_content = []
for content_item in user_message["content"]:
if content_item["type"] == "text":
# Update the text item's content with ra_content
new_content.append({"type": "text", "text": ra_content})
else:
# Keep other types of content as they are
new_content.append(content_item)
new_user_message = {**user_message, "content": new_content}
else:
new_user_message = {
**user_message,
"content": ra_content,
}
messages[last_user_message_idx] = new_user_message
return messages

View file

@ -75,7 +75,7 @@ async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024
hashed = calculate_sha256(file) hashed = calculate_sha256(file)
file.seek(0) file.seek(0)
url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}" url = f"{OLLAMA_BASE_URLS[0]}/api/blobs/sha256:{hashed}"
response = requests.post(url, data=file) response = requests.post(url, data=file)
if response.ok: if response.ok:

View file

@ -209,10 +209,6 @@ 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", "")
if ENV == "prod":
if OLLAMA_BASE_URL == "/ollama":
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
OLLAMA_BASE_URL = ( OLLAMA_BASE_URL = (
@ -221,6 +217,11 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
else OLLAMA_API_BASE_URL else OLLAMA_API_BASE_URL
) )
if ENV == "prod":
if OLLAMA_BASE_URL == "/ollama":
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
@ -234,8 +235,6 @@ OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")]
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
if OPENAI_API_KEY == "":
OPENAI_API_KEY = "none"
if OPENAI_API_BASE_URL == "": if OPENAI_API_BASE_URL == "":
OPENAI_API_BASE_URL = "https://api.openai.com/v1" OPENAI_API_BASE_URL = "https://api.openai.com/v1"
@ -292,6 +291,11 @@ DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
USER_PERMISSIONS = {"chat": {"deletion": True}} USER_PERMISSIONS = {"chat": {"deletion": True}}
MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", False)
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
#################################### ####################################
# WEBUI_VERSION # WEBUI_VERSION
#################################### ####################################

View file

@ -52,3 +52,4 @@ class ERROR_MESSAGES(str, Enum):
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found"
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"

View file

@ -1,4 +1,5 @@
{ {
"version": "0.0.1",
"ui": { "ui": {
"prompt_suggestions": [ "prompt_suggestions": [
{ {

View file

@ -23,10 +23,22 @@ from apps.images.main import app as images_app
from apps.rag.main import app as rag_app from apps.rag.main import app as rag_app
from apps.web.main import app as webui_app from apps.web.main import app as webui_app
from pydantic import BaseModel
from typing import List
from apps.rag.utils import query_doc, query_collection, rag_template
from config import WEBUI_NAME, ENV, VERSION, CHANGELOG, FRONTEND_BUILD_DIR from utils.utils import get_admin_user
from apps.rag.utils import rag_messages
from config import (
WEBUI_NAME,
ENV,
VERSION,
CHANGELOG,
FRONTEND_BUILD_DIR,
MODEL_FILTER_ENABLED,
MODEL_FILTER_LIST,
)
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
@ -43,21 +55,11 @@ class SPAStaticFiles(StaticFiles):
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
origins = ["*"] origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.on_event("startup")
async def on_startup():
await litellm_app_startup()
class RAGMiddleware(BaseHTTPMiddleware): class RAGMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next): async def dispatch(self, request: Request, call_next):
@ -76,96 +78,33 @@ 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:
docs = data["docs"]
print(docs)
last_user_message_idx = None data = {**data}
for i in range(len(data["messages"]) - 1, -1, -1): data["messages"] = rag_messages(
if data["messages"][i]["role"] == "user": data["docs"],
last_user_message_idx = i data["messages"],
break rag_app.state.RAG_TEMPLATE,
rag_app.state.TOP_K,
user_message = data["messages"][last_user_message_idx] rag_app.state.sentence_transformer_ef,
if isinstance(user_message["content"], list):
# Handle list content input
content_type = "list"
query = ""
for content_item in user_message["content"]:
if content_item["type"] == "text":
query = content_item["text"]
break
elif isinstance(user_message["content"], str):
# Handle text content input
content_type = "text"
query = user_message["content"]
else:
# Fallback in case the input does not match expected types
content_type = None
query = ""
relevant_contexts = []
for doc in docs:
context = None
try:
if doc["type"] == "collection":
context = query_collection(
collection_names=doc["collection_names"],
query=query,
k=rag_app.state.TOP_K,
embedding_function=rag_app.state.sentence_transformer_ef,
) )
else:
context = query_doc(
collection_name=doc["collection_name"],
query=query,
k=rag_app.state.TOP_K,
embedding_function=rag_app.state.sentence_transformer_ef,
)
except Exception as e:
print(e)
context = None
relevant_contexts.append(context)
context_string = ""
for context in relevant_contexts:
if context:
context_string += " ".join(context["documents"][0]) + "\n"
ra_content = rag_template(
template=rag_app.state.RAG_TEMPLATE,
context=context_string,
query=query,
)
if content_type == "list":
new_content = []
for content_item in user_message["content"]:
if content_item["type"] == "text":
# Update the text item's content with ra_content
new_content.append({"type": "text", "text": ra_content})
else:
# Keep other types of content as they are
new_content.append(content_item)
new_user_message = {**user_message, "content": new_content}
else:
new_user_message = {
**user_message,
"content": ra_content,
}
data["messages"][last_user_message_idx] = new_user_message
del data["docs"] del data["docs"]
print(data["messages"])
modified_body_bytes = json.dumps(data).encode("utf-8") modified_body_bytes = json.dumps(data).encode("utf-8")
# Create a new request with the modified body # Replace the request body with the modified one
scope = request.scope request._body = modified_body_bytes
scope["body"] = modified_body_bytes
request = Request(scope, receive=lambda: self._receive(modified_body_bytes)) # Set custom header to ensure content-length matches new body length
request.headers.__dict__["_list"] = [
(b"content-length", str(len(modified_body_bytes)).encode("utf-8")),
*[
(k, v)
for k, v in request.headers.raw
if k.lower() != b"content-length"
],
]
response = await call_next(request) response = await call_next(request)
return response return response
@ -177,6 +116,15 @@ class RAGMiddleware(BaseHTTPMiddleware):
app.add_middleware(RAGMiddleware) app.add_middleware(RAGMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http") @app.middleware("http")
async def check_url(request: Request, call_next): async def check_url(request: Request, call_next):
start_time = int(time.time()) start_time = int(time.time())
@ -187,6 +135,11 @@ async def check_url(request: Request, call_next):
return response return response
@app.on_event("startup")
async def on_startup():
await litellm_app_startup()
app.mount("/api/v1", webui_app) app.mount("/api/v1", webui_app)
app.mount("/litellm/api", litellm_app) app.mount("/litellm/api", litellm_app)
@ -211,6 +164,39 @@ async def get_app_config():
} }
@app.get("/api/config/model/filter")
async def get_model_filter_config(user=Depends(get_admin_user)):
return {
"enabled": app.state.MODEL_FILTER_ENABLED,
"models": app.state.MODEL_FILTER_LIST,
}
class ModelFilterConfigForm(BaseModel):
enabled: bool
models: List[str]
@app.post("/api/config/model/filter")
async def get_model_filter_config(
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
):
app.state.MODEL_FILTER_ENABLED = form_data.enabled
app.state.MODEL_FILTER_LIST = form_data.models
ollama_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
ollama_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
return {
"enabled": app.state.MODEL_FILTER_ENABLED,
"models": app.state.MODEL_FILTER_LIST,
}
@app.get("/api/version") @app.get("/api/version")
async def get_app_config(): async def get_app_config():

View file

@ -16,7 +16,8 @@ aiohttp
peewee peewee
bcrypt bcrypt
litellm litellm==1.30.7
argon2-cffi
apscheduler apscheduler
google-generativeai google-generativeai

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 MiB

After

Width:  |  Height:  |  Size: 5 MiB

View file

@ -50,6 +50,18 @@ We welcome pull requests. Before submitting one, please:
Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI. Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI.
### 🌐 Translations and Internationalization
Help us make Open WebUI available to a wider audience. In this section, we'll guide you through the process of adding new translations to the project.
We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes][http://www.lingoes.net/en/translator/langcode.htm] to find the appropriate code for a specific language.
To add a new language:
- Create a new directory in the `src/lib/i18n/locales` path with the appropriate language code as its name. For instance, if you're adding translations for Spanish (Spain), create a new directory named `es-ES`.
- Copy the American English translation file(s) (from `en-US` directory in `src/lib/i18n/locale`) to this new directory and update the string values in JSON format according to your language. Make sure to preserve the structure of the JSON object.
- Add the language code and its respective title to languages file at `src/lib/i18n/locales/languages.json`.
### 🤔 Questions & Feedback ### 🤔 Questions & Feedback
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help! Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help!

38
i18next-parser.config.ts Normal file
View file

@ -0,0 +1,38 @@
// i18next-parser.config.ts
import { getLanguages } from './src/lib/i18n/index.ts';
const getLangCodes = async () => {
const languages = await getLanguages();
return languages.map((l) => l.code);
};
export default {
contextSeparator: '_',
createOldCatalogs: false,
defaultNamespace: 'translation',
defaultValue: '',
indentation: 2,
keepRemoved: false,
keySeparator: false,
lexers: {
svelte: ['JavascriptLexer'],
js: ['JavascriptLexer'],
ts: ['JavascriptLexer'],
default: ['JavascriptLexer']
},
lineEnding: 'auto',
locales: await getLangCodes(),
namespaceSeparator: false,
output: 'src/lib/i18n/locales/$LOCALE/$NAMESPACE.json',
pluralSeparator: '_',
input: 'src/**/*.{js,svelte}',
sort: true,
verbose: true,
failOnWarnings: false,
failOnUpdate: false,
customValueTemplate: null,
resetDefaultValueLocale: null,
i18nextOptions: null,
yamlOptions: null
};

View file

@ -4,6 +4,9 @@ metadata:
name: {{ include "open-webui.name" . }} name: {{ include "open-webui.name" . }}
labels: labels:
{{- include "open-webui.labels" . | nindent 4 }} {{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.service.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.webui.service.annotations }} {{- with .Values.webui.service.annotations }}
annotations: annotations:
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}
@ -11,14 +14,16 @@ metadata:
spec: spec:
selector: selector:
{{- include "open-webui.selectorLabels" . | nindent 4 }} {{- include "open-webui.selectorLabels" . | nindent 4 }}
{{- with .Values.webui.service }} type: {{ .Values.webui.service.type | default "ClusterIP" }}
type: {{ .type }}
ports: ports:
- protocol: TCP - protocol: TCP
name: http name: http
port: {{ .port }} port: {{ .Values.webui.service.port }}
targetPort: http targetPort: http
{{- if .nodePort }} {{- if .Values.webui.service.nodePort }}
nodePort: {{ .nodePort | int }} nodePort: {{ .Values.webui.service.nodePort | int }}
{{- end }} {{- end }}
{{- end }} {{- if .Values.webui.service.loadBalancerClass }}
loadBalancerClass: {{ .Values.webui.service.loadBalancerClass | quote }}
{{- end }}

View file

@ -70,3 +70,5 @@ webui:
port: 80 port: 80
containerPort: 8080 containerPort: 8080
nodePort: "" nodePort: ""
labels: {}
loadBalancerClass: ""

3398
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.1.110", "version": "0.1.112",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev --host", "dev": "vite dev --host",
@ -13,7 +13,8 @@
"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" "format:backend": "yapf --recursive backend -p -i",
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-auto": "^2.0.0",
@ -27,6 +28,7 @@
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.30.0", "eslint-plugin-svelte": "^2.30.0",
"i18next-parser": "^8.13.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1", "prettier-plugin-svelte": "^2.10.1",
@ -42,9 +44,13 @@
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^1.3.1", "@sveltejs/adapter-node": "^1.3.1",
"async": "^3.2.5", "async": "^3.2.5",
"bits-ui": "^0.19.7",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1", "idb": "^7.1.1",
"js-sha256": "^0.10.1", "js-sha256": "^0.10.1",
"katex": "^0.16.9", "katex": "^0.16.9",

View file

@ -77,3 +77,65 @@ export const getVersionUpdates = async () => {
return res; return res;
}; };
export const getModelFilterConfig = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const updateModelFilterConfig = async (
token: string,
enabled: boolean,
models: string[]
) => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
enabled: enabled,
models: models
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};

View file

@ -1,9 +1,9 @@
import { RAG_API_BASE_URL } from '$lib/constants'; import { RAG_API_BASE_URL } from '$lib/constants';
export const getChunkParams = async (token: string) => { export const getRAGConfig = async (token: string) => {
let error = null; let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/chunk`, { const res = await fetch(`${RAG_API_BASE_URL}/config`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -27,18 +27,27 @@ export const getChunkParams = async (token: string) => {
return res; return res;
}; };
export const updateChunkParams = async (token: string, size: number, overlap: number) => { type ChunkConfigForm = {
chunk_size: number;
chunk_overlap: number;
};
type RAGConfigForm = {
pdf_extract_images: boolean;
chunk: ChunkConfigForm;
};
export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => {
let error = null; let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/chunk/update`, { const res = await fetch(`${RAG_API_BASE_URL}/config/update`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
chunk_size: size, ...payload
chunk_overlap: overlap
}) })
}) })
.then(async (res) => { .then(async (res) => {

View file

@ -1,8 +1,13 @@
<script>
import { getContext } from 'svelte';
const i18n = getContext('i18n');
</script>
<div class=" text-center text-6xl mb-3">📄</div> <div class=" text-center text-6xl mb-3">📄</div>
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div> <div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
<slot <slot
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full"> ><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to the conversation {$i18n.t('Drop any files here to add to the conversation')}
</div> </div>
</slot> </slot>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { Confetti } from 'svelte-confetti'; import { Confetti } from 'svelte-confetti';
import { WEBUI_NAME, config } from '$lib/stores'; import { WEBUI_NAME, config } from '$lib/stores';
@ -9,6 +9,8 @@
import Modal from './common/Modal.svelte'; import Modal from './common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false; export let show = false;
let changelog = null; let changelog = null;
@ -23,7 +25,8 @@
<div class="px-5 py-4 dark:text-gray-300"> <div class="px-5 py-4 dark:text-gray-300">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div class="text-xl font-bold"> <div class="text-xl font-bold">
Whats New in {$WEBUI_NAME} {$i18n.t('Whats New in')}
{$WEBUI_NAME}
<Confetti x={[-1, -0.25]} y={[0, 0.5]} /> <Confetti x={[-1, -0.25]} y={[0, 0.5]} />
</div> </div>
<button <button
@ -45,7 +48,7 @@
</button> </button>
</div> </div>
<div class="flex items-center mt-1"> <div class="flex items-center mt-1">
<div class="text-sm dark:text-gray-200">Release Notes</div> <div class="text-sm dark:text-gray-200">{$i18n.t('Release Notes')}</div>
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" /> <div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
<div class="text-sm dark:text-gray-200"> <div class="text-sm dark:text-gray-200">
v{WEBUI_VERSION} v{WEBUI_VERSION}
@ -108,7 +111,7 @@
}} }}
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
> >
<span class="relative">Okay, Let's Go!</span> <span class="relative">{$i18n.t("Okay, Let's Go!")}</span>
</button> </button>
</div> </div>
</div> </div>

View file

@ -2,11 +2,12 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { updateUserById } from '$lib/apis/users'; import { updateUserById } from '$lib/apis/users';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let show = false; export let show = false;
@ -42,7 +43,7 @@
<Modal size="sm" bind:show> <Modal size="sm" bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Edit User</div> <div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
@ -84,7 +85,8 @@
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div> <div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
Created at {dayjs(selectedUser.timestamp * 1000).format('MMMM DD, YYYY')} {$i18n.t('Created at')}
{dayjs(selectedUser.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
</div> </div>
</div> </div>
</div> </div>
@ -93,7 +95,7 @@
<div class=" flex flex-col space-y-1.5"> <div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Email</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
@ -108,7 +110,7 @@
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Name</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
@ -122,7 +124,7 @@
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">New Password</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
@ -140,7 +142,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { downloadDatabase } from '$lib/apis/utils'; import { downloadDatabase } from '$lib/apis/utils';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
@ -17,10 +19,10 @@
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div> <div>
<div class=" mb-2 text-sm font-medium">Database</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<!-- <div class=" self-center text-xs font-medium">Allow Chat Deletion</div> --> <!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
<button <button
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
@ -46,7 +48,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center text-sm font-medium">Download Database</div> <div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
</button> </button>
</div> </div>
</div> </div>

View file

@ -7,7 +7,9 @@
updateDefaultUserRole, updateDefaultUserRole,
updateJWTExpiresDuration updateJWTExpiresDuration
} from '$lib/apis/auths'; } from '$lib/apis/auths';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let signUpEnabled = true; let signUpEnabled = true;
@ -43,10 +45,10 @@
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div> <div>
<div class=" mb-2 text-sm font-medium">General Settings</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Enable New Sign Ups</div> <div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -66,7 +68,7 @@
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
/> />
</svg> </svg>
<span class="ml-2 self-center">Enabled</span> <span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
{:else} {:else}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -81,25 +83,25 @@
/> />
</svg> </svg>
<span class="ml-2 self-center">Disabled</span> <span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
{/if} {/if}
</button> </button>
</div> </div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Default User Role</div> <div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={defaultUserRole} bind:value={defaultUserRole}
placeholder="Select a theme" placeholder="Select a theme"
on:change={(e) => { on:change={(e) => {
updateDefaultUserRoleHandler(e.target.value); updateDefaultUserRoleHandler(e.target.value);
}} }}
> >
<option value="pending">Pending</option> <option value="pending">{$i18n.t('pending')}</option>
<option value="user">User</option> <option value="user">{$i18n.t('user')}</option>
<option value="admin">Admin</option> <option value="admin">{$i18n.t('admin')}</option>
</select> </select>
</div> </div>
</div> </div>
@ -108,7 +110,7 @@
<div class=" w-full justify-between"> <div class=" w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">JWT Expiration</div> <div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
</div> </div>
<div class="flex mt-2 space-x-2"> <div class="flex mt-2 space-x-2">
@ -121,8 +123,9 @@
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Valid time units: <span class=" text-gray-300 font-medium" {$i18n.t('Valid time units:')}
>'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.</span <span class=" text-gray-300 font-medium"
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
> >
</div> </div>
</div> </div>
@ -134,7 +137,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -1,10 +1,17 @@
<script lang="ts"> <script lang="ts">
import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users'; import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { models } from '$lib/stores';
const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let whitelistEnabled = false;
let whitelistModels = [''];
let permissions = { let permissions = {
chat: { chat: {
deletion: true deletion: true
@ -13,6 +20,13 @@
onMount(async () => { onMount(async () => {
permissions = await getUserPermissions(localStorage.token); permissions = await getUserPermissions(localStorage.token);
const res = await getModelFilterConfig(localStorage.token);
if (res) {
whitelistEnabled = res.enabled;
whitelistModels = res.models.length > 0 ? res.models : [''];
}
}); });
</script> </script>
@ -21,15 +35,17 @@
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
// console.log('submit'); // console.log('submit');
await updateUserPermissions(localStorage.token, permissions); await updateUserPermissions(localStorage.token, permissions);
await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
saveHandler(); saveHandler();
}} }}
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div> <div>
<div class=" mb-2 text-sm font-medium">User Permissions</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Allow Chat Deletion</div> <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -49,7 +65,7 @@
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
/> />
</svg> </svg>
<span class="ml-2 self-center">Allow</span> <span class="ml-2 self-center">{$i18n.t('Allow')}</span>
{:else} {:else}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -64,11 +80,112 @@
/> />
</svg> </svg>
<span class="ml-2 self-center">Don't Allow</span> <span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span>
{/if} {/if}
</button> </button>
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-2" />
<div class="mt-2 space-y-3 pr-1.5">
<div>
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
</div>
</div>
<div class=" space-y-3">
<div>
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
whitelistEnabled = !whitelistEnabled;
}}>{whitelistEnabled ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div>
</div>
{#if whitelistEnabled}
<div>
<div class=" space-y-1.5">
{#each whitelistModels as modelId, modelIdx}
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={modelId}
placeholder="Select a model"
>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"
>{model.name}</option
>
{/each}
</select>
</div>
{#if modelIdx === 0}
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
type="button"
on:click={() => {
if (whitelistModels.at(-1) !== '') {
whitelistModels = [...whitelistModels, ''];
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
{:else}
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
type="button"
on:click={() => {
whitelistModels.splice(modelIdx, 1);
whitelistModels = whitelistModels;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</button>
{/if}
</div>
{/each}
</div>
<div class="flex justify-end items-center text-xs mt-1.5 text-right">
<div class=" text-xs font-medium">
{whitelistModels.length}
{$i18n.t('Model(s) Whitelisted')}
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
@ -76,7 +193,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -1,10 +1,13 @@
<script> <script>
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import Database from './Settings/Database.svelte'; import Database from './Settings/Database.svelte';
import General from './Settings/General.svelte'; import General from './Settings/General.svelte';
import Users from './Settings/Users.svelte'; import Users from './Settings/Users.svelte';
const i18n = getContext('i18n');
export let show = false; export let show = false;
let selectedTab = 'general'; let selectedTab = 'general';
@ -13,7 +16,7 @@
<Modal bind:show> <Modal bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Admin Settings</div> <div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
@ -61,7 +64,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">General</div> <div class=" self-center">{$i18n.t('General')}</div>
</button> </button>
<button <button
@ -85,7 +88,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Users</div> <div class=" self-center">{$i18n.t('Users')}</div>
</button> </button>
<button <button
@ -113,7 +116,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Database</div> <div class=" self-center">{$i18n.t('Database')}</div>
</button> </button>
</div> </div>
<div class="flex-1 md:min-h-[380px]"> <div class="flex-1 md:min-h-[380px]">

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { settings } from '$lib/stores'; import { settings } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
@ -14,12 +14,14 @@
import { transcribeAudio } from '$lib/apis/audio'; import { transcribeAudio } from '$lib/apis/audio';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
const i18n = getContext('i18n');
export let submitPrompt: Function; export let submitPrompt: Function;
export let stopResponse: Function; export let stopResponse: Function;
export let suggestionPrompts = []; export let suggestionPrompts = [];
export let autoScroll = true; export let autoScroll = true;
let chatTextAreaElement:HTMLTextAreaElement let chatTextAreaElement: HTMLTextAreaElement;
let filesInputElement; let filesInputElement;
let promptsElement; let promptsElement;
@ -209,11 +211,11 @@
// Event triggered when an error occurs // Event triggered when an error occurs
speechRecognition.onerror = function (event) { speechRecognition.onerror = function (event) {
console.log(event); console.log(event);
toast.error(`Speech recognition error: ${event.error}`); toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
isRecording = false; isRecording = false;
}; };
} else { } else {
toast.error('SpeechRecognition API is not supported in this browser.'); toast.error($i18n.t('SpeechRecognition API is not supported in this browser.'));
} }
} }
} }
@ -333,12 +335,15 @@
uploadDoc(file); uploadDoc(file);
} else { } else {
toast.error( toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text` $i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
); );
uploadDoc(file); uploadDoc(file);
} }
} else { } else {
toast.error(`File not found.`); toast.error($i18n.t(`File not found.`));
} }
} }
@ -359,12 +364,12 @@
{#if dragged} {#if dragged}
<div <div
class="fixed w-full h-full flex z-50 touch-none pointer-events-none" class="fixed lg:w-[calc(100%-260px)] w-full h-full flex z-50 touch-none pointer-events-none"
id="dropzone" id="dropzone"
role="region" role="region"
aria-label="Drag and Drop Container" aria-label="Drag and Drop Container"
> >
<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center"> <div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center"> <div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md"> <div class="max-w-md">
<AddFilesPlaceholder /> <AddFilesPlaceholder />
@ -477,13 +482,16 @@
filesInputElement.value = ''; filesInputElement.value = '';
} else { } else {
toast.error( toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text` $i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
); );
uploadDoc(file); uploadDoc(file);
filesInputElement.value = ''; filesInputElement.value = '';
} }
} else { } else {
toast.error(`File not found.`); toast.error($i18n.t(`File not found.`));
} }
}} }}
/> />
@ -570,7 +578,7 @@
{file.name} {file.name}
</div> </div>
<div class=" text-gray-500 text-sm">Document</div> <div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div> </div>
</div> </div>
{:else if file.type === 'collection'} {:else if file.type === 'collection'}
@ -598,7 +606,7 @@
{file?.title ?? `#${file.name}`} {file?.title ?? `#${file.name}`}
</div> </div>
<div class=" text-gray-500 text-sm">Collection</div> <div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div> </div>
</div> </div>
{/if} {/if}
@ -632,7 +640,7 @@
<div class=" flex"> <div class=" flex">
{#if fileUploadEnabled} {#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1"> <div class=" self-end mb-2 ml-1">
<Tooltip content="Upload files"> <Tooltip content={$i18n.t('Upload files')}>
<button <button
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5" class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
type="button" type="button"
@ -664,8 +672,8 @@
placeholder={chatInputPlaceholder !== '' placeholder={chatInputPlaceholder !== ''
? chatInputPlaceholder ? chatInputPlaceholder
: isRecording : isRecording
? 'Listening...' ? $i18n.t('Listening...')
: 'Send a message'} : $i18n.t('Send a Messsage')}
bind:value={prompt} bind:value={prompt}
on:keypress={(e) => { on:keypress={(e) => {
if (e.keyCode == 13 && !e.shiftKey) { if (e.keyCode == 13 && !e.shiftKey) {
@ -804,7 +812,7 @@
<div class="self-end mb-2 flex space-x-1 mr-1"> <div class="self-end mb-2 flex space-x-1 mr-1">
{#if messages.length == 0 || messages.at(-1).done == true} {#if messages.length == 0 || messages.at(-1).done == true}
<Tooltip content="Record voice"> <Tooltip content={$i18n.t('Record voice')}>
{#if speechRecognitionEnabled} {#if speechRecognitionEnabled}
<button <button
id="voice-input-button" id="voice-input-button"
@ -873,7 +881,7 @@
{/if} {/if}
</Tooltip> </Tooltip>
<Tooltip content="Send message"> <Tooltip content={$i18n.t('Send message')}>
<button <button
class="{prompt !== '' class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 ' ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
@ -919,7 +927,7 @@
</form> </form>
<div class="mt-1.5 text-xs text-gray-500 text-center"> <div class="mt-1.5 text-xs text-gray-500 text-center">
LLMs can make mistakes. Verify important information. {$i18n.t('LLMs can make mistakes. Verify important information.')}
</div> </div>
</div> </div>
</div> </div>

View file

@ -3,9 +3,11 @@
import { documents } from '$lib/stores'; import { documents } from '$lib/stores';
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils'; import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
import { tick } from 'svelte'; import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = ''; export let prompt = '';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -117,7 +119,7 @@
{doc?.title ?? `#${doc.name}`} {doc?.title ?? `#${doc.name}`}
</div> </div>
<div class=" text-xs text-gray-600 line-clamp-1">Collection</div> <div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Collection')}</div>
{:else} {:else}
<div class=" font-medium text-black line-clamp-1"> <div class=" font-medium text-black line-clamp-1">
#{doc.name} ({doc.filename}) #{doc.name} ({doc.filename})
@ -140,7 +142,9 @@
confirmSelectWeb(url); confirmSelectWeb(url);
} else { } else {
toast.error( toast.error(
$i18n.t(
'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.'
)
); );
} }
}} }}
@ -149,7 +153,7 @@
{prompt.split(' ')?.at(0)?.substring(1)} {prompt.split(' ')?.at(0)?.substring(1)}
</div> </div>
<div class=" text-xs text-gray-600 line-clamp-1">Web</div> <div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
</button> </button>
{/if} {/if}
</div> </div>

View file

@ -2,9 +2,11 @@
import { generatePrompt } from '$lib/apis/ollama'; import { generatePrompt } from '$lib/apis/ollama';
import { models } from '$lib/stores'; import { models } from '$lib/stores';
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import { tick } from 'svelte'; import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = ''; export let prompt = '';
export let user = null; export let user = null;
@ -41,7 +43,7 @@
user = JSON.parse(JSON.stringify(model.name)); user = JSON.parse(JSON.stringify(model.name));
await tick(); await tick();
chatInputPlaceholder = `'${model.name}' is thinking...`; chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name });
const chatInputElement = document.getElementById('chat-textarea'); const chatInputElement = document.getElementById('chat-textarea');
@ -113,7 +115,9 @@
toast.error(error.error); toast.error(error.error);
} }
} else { } else {
toast.error(`Uh-oh! There was an issue connecting to Ollama.`); toast.error(
$i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' })
);
} }
} }

View file

@ -1,9 +1,11 @@
<script lang="ts"> <script lang="ts">
import { prompts } from '$lib/stores'; import { prompts } from '$lib/stores';
import { findWordIndices } from '$lib/utils'; import { findWordIndices } from '$lib/utils';
import { tick } from 'svelte'; import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = ''; export let prompt = '';
let selectedCommandIdx = 0; let selectedCommandIdx = 0;
let filteredPromptCommands = []; let filteredPromptCommands = [];
@ -29,7 +31,7 @@
if (command.content.includes('{{CLIPBOARD}}')) { if (command.content.includes('{{CLIPBOARD}}')) {
const clipboardText = await navigator.clipboard.readText().catch((err) => { const clipboardText = await navigator.clipboard.readText().catch((err) => {
toast.error('Failed to read clipboard contents'); toast.error($i18n.t('Failed to read clipboard contents'));
return '{{CLIPBOARD}}'; return '{{CLIPBOARD}}';
}); });
@ -113,8 +115,9 @@
</div> </div>
<div class="line-clamp-1"> <div class="line-clamp-1">
Tip: Update multiple variable slots consecutively by pressing the tab key in the chat {$i18n.t(
input after each replacement. 'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,7 +2,7 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { chats, config, modelfiles, settings, user } from '$lib/stores'; import { chats, config, modelfiles, settings, user } from '$lib/stores';
import { tick } from 'svelte'; import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats'; import { getChatList, updateChatById } from '$lib/apis/chats';
@ -13,6 +13,8 @@
import Spinner from '../common/Spinner.svelte'; import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images'; import { imageGenerations } from '$lib/apis/images';
const i18n = getContext('i18n');
export let chatId = ''; export let chatId = '';
export let sendPrompt: Function; export let sendPrompt: Function;
export let continueGeneration: Function; export let continueGeneration: Function;
@ -67,7 +69,7 @@
navigator.clipboard.writeText(text).then( navigator.clipboard.writeText(text).then(
function () { function () {
console.log('Async: Copying to clipboard was successful!'); console.log('Async: Copying to clipboard was successful!');
toast.success('Copying to clipboard was successful!'); toast.success($i18n.t('Copying to clipboard was successful!'));
}, },
function (err) { function (err) {
console.error('Async: Could not copy text: ', err); console.error('Async: Could not copy text: ', err);

View file

@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import { user } from '$lib/stores'; import { user } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let models = []; export let models = [];
export let modelfiles = []; export let modelfiles = [];
@ -64,9 +66,9 @@
</div> </div>
{/if} {/if}
{:else} {:else}
<div class=" line-clamp-1">Hello, {$user.name}</div> <div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
<div>How can I help you today?</div> <div>{$i18n.t('How can I help you today?')}</div>
{/if} {/if}
</div> </div>
</div> </div>

View file

@ -8,7 +8,9 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -316,7 +318,7 @@
{#if message.timestamp} {#if message.timestamp}
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium"> <span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')} {dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
</span> </span>
{/if} {/if}
</Name> </Name>
@ -360,7 +362,7 @@
editMessageConfirmHandler(); editMessageConfirmHandler();
}} }}
> >
Save {$i18n.t('Save')}
</button> </button>
<button <button
@ -369,7 +371,7 @@
cancelEditMessage(); cancelEditMessage();
}} }}
> >
Cancel {$i18n.t('Cancel')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { tick, createEventDispatcher } from 'svelte'; import { tick, createEventDispatcher, getContext } from 'svelte';
import Name from './Name.svelte'; import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte'; import ProfileImage from './ProfileImage.svelte';
import { modelfiles, settings } from '$lib/stores'; import { modelfiles, settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let user; export let user;
@ -65,17 +67,18 @@
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)} {#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title} {$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
{:else} {:else}
You <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span> {$i18n.t('You')}
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
{/if} {/if}
{:else if $settings.showUsername} {:else if $settings.showUsername}
{user.name} {user.name}
{:else} {:else}
You {$i18n.t('You')}
{/if} {/if}
{#if message.timestamp} {#if message.timestamp}
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium"> <span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')} {dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
</span> </span>
{/if} {/if}
</Name> </Name>
@ -123,7 +126,7 @@
{file.name} {file.name}
</div> </div>
<div class=" text-gray-500 text-sm">Document</div> <div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div> </div>
</button> </button>
{:else if file.type === 'collection'} {:else if file.type === 'collection'}
@ -152,7 +155,7 @@
{file?.title ?? `#${file.name}`} {file?.title ?? `#${file.name}`}
</div> </div>
<div class=" text-gray-500 text-sm">Collection</div> <div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div> </div>
</button> </button>
{/if} {/if}
@ -181,7 +184,7 @@
editMessageConfirmHandler(); editMessageConfirmHandler();
}} }}
> >
Save & Submit {$i18n.t('Save & Submit')}
</button> </button>
<button <button
@ -190,7 +193,7 @@
cancelEditMessage(); cancelEditMessage();
}} }}
> >
Cancel {$i18n.t('Cancel')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,16 +1,18 @@
<script lang="ts"> <script lang="ts">
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 } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let selectedModels = ['']; export let selectedModels = [''];
export let disabled = false; export let disabled = false;
const saveDefaultModel = async () => { const saveDefaultModel = async () => {
const hasEmptyModel = selectedModels.filter((it) => it === ''); const hasEmptyModel = selectedModels.filter((it) => it === '');
if (hasEmptyModel.length) { if (hasEmptyModel.length) {
toast.error('Choose a model before saving...'); toast.error($i18n.t('Choose a model before saving...'));
return; return;
} }
settings.set({ ...$settings, models: selectedModels }); settings.set({ ...$settings, models: selectedModels });
@ -20,7 +22,7 @@
console.log('setting default models globally'); console.log('setting default models globally');
await setDefaultModels(localStorage.token, selectedModels.join(',')); await setDefaultModels(localStorage.token, selectedModels.join(','));
} }
toast.success('Default model updated'); toast.success($i18n.t('Default model updated'));
}; };
$: if (selectedModels.length > 0 && $models.length > 0) { $: if (selectedModels.length > 0 && $models.length > 0) {
@ -39,7 +41,9 @@
bind:value={selectedModel} bind:value={selectedModel}
{disabled} {disabled}
> >
<option class=" text-gray-700" value="" selected disabled>Select a model</option> <option class=" text-gray-700" value="" selected disabled
>{$i18n.t('Select a model')}</option
>
{#each $models as model} {#each $models as model}
{#if model.name === 'hr'} {#if model.name === 'hr'}
@ -133,5 +137,5 @@
</div> </div>
<div class="text-left mt-1.5 text-xs text-gray-500"> <div class="text-left mt-1.5 text-xs text-gray-500">
<button on:click={saveDefaultModel}> Set as default</button> <button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div> </div>

View file

@ -4,7 +4,9 @@
import { WEBUI_VERSION } from '$lib/constants'; import { WEBUI_VERSION } from '$lib/constants';
import { WEBUI_NAME, config, showChangelog } from '$lib/stores'; import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
import { compareVersion } from '$lib/utils'; import { compareVersion } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
let ollamaVersion = ''; let ollamaVersion = '';
@ -43,7 +45,8 @@
<div> <div>
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center"> <div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
<div> <div>
{$WEBUI_NAME} Version {$WEBUI_NAME}
{$i18n.t('Version')}
</div> </div>
</div> </div>
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
@ -56,10 +59,10 @@
target="_blank" target="_blank"
> >
{updateAvailable === null {updateAvailable === null
? 'Checking for updates...' ? $i18n.t('Checking for updates...')
: updateAvailable : updateAvailable
? `(v${version.latest} available!)` ? `(v${version.latest} ${$i18n.t('available!')})`
: '(latest)'} : $i18n.t('(latest)')}
</a> </a>
</div> </div>
@ -69,7 +72,7 @@
showChangelog.set(true); showChangelog.set(true);
}} }}
> >
<div>See what's new</div> <div>{$i18n.t("See what's new")}</div>
</button> </button>
</div> </div>
@ -79,7 +82,7 @@
checkForVersionUpdates(); checkForVersionUpdates();
}} }}
> >
Check for updates {$i18n.t('Check for updates')}
</button> </button>
</div> </div>
</div> </div>
@ -88,7 +91,7 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
{ollamaVersion ?? 'N/A'} {ollamaVersion ?? 'N/A'}
@ -123,7 +126,8 @@
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Created by <a {$i18n.t('Created by')}
<a
class=" text-gray-500 dark:text-gray-300 font-medium" class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://github.com/tjbck" href="https://github.com/tjbck"
target="_blank">Timothy J. Baek</a target="_blank">Timothy J. Baek</a

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { user } from '$lib/stores'; import { user } from '$lib/stores';
import { updateUserProfile } from '$lib/apis/auths'; import { updateUserProfile } from '$lib/apis/auths';
@ -9,6 +9,8 @@
import { getGravatarUrl } from '$lib/apis/utils'; import { getGravatarUrl } from '$lib/apis/utils';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let profileImageUrl = ''; let profileImageUrl = '';
@ -38,7 +40,7 @@
</script> </script>
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
<input <input
id="profile-image-input" id="profile-image-input"
bind:this={profileImageInputElement} bind:this={profileImageInputElement}
@ -101,7 +103,7 @@
}} }}
/> />
<div class=" mb-2.5 text-sm font-medium">Profile</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div>
<div class="flex space-x-5"> <div class="flex space-x-5">
<div class="flex flex-col"> <div class="flex flex-col">
@ -143,13 +145,13 @@
const url = await getGravatarUrl($user.email); const url = await getGravatarUrl($user.email);
profileImageUrl = url; profileImageUrl = url;
}}>Use Gravatar</button }}>{$i18n.t('Use Gravatar')}</button
> >
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Name</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
@ -170,7 +172,7 @@
<div class=" w-full justify-between"> <div class=" w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">JWT Token</div> <div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
</div> </div>
<div class="flex mt-2"> <div class="flex mt-2">
@ -271,7 +273,7 @@
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
on:click={async () => { on:click={async () => {
const res = await submitHandler(); const res = await submitHandler();
@ -280,7 +282,7 @@
} }
}} }}
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,7 +1,10 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { updateUserPassword } from '$lib/apis/auths'; import { updateUserPassword } from '$lib/apis/auths';
const i18n = getContext('i18n');
let show = false; let show = false;
let currentPassword = ''; let currentPassword = '';
let newPassword = ''; let newPassword = '';
@ -17,7 +20,7 @@
); );
if (res) { if (res) {
toast.success('Successfully updated.'); toast.success($i18n.t('Successfully updated.'));
} }
currentPassword = ''; currentPassword = '';
@ -40,20 +43,20 @@
}} }}
> >
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<div class=" font-medium">Change Password</div> <div class=" font-medium">{$i18n.t('Change Password')}</div>
<button <button
class=" text-xs font-medium text-gray-500" class=" text-xs font-medium text-gray-500"
type="button" type="button"
on:click={() => { on:click={() => {
show = !show; show = !show;
}}>{show ? 'Hide' : 'Show'}</button }}>{show ? $i18n.t('Hide') : $i18n.t('Show')}</button
> >
</div> </div>
{#if show} {#if show}
<div class=" py-2.5 space-y-1.5"> <div class=" py-2.5 space-y-1.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Current Password</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Current Password')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
@ -67,7 +70,7 @@
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">New Password</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
@ -81,7 +84,7 @@
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Confirm Password</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Confirm Password')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
@ -99,7 +102,7 @@
<button <button
class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium" class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
> >
Update password {$i18n.t('Update password')}
</button> </button>
</div> </div>
{/if} {/if}

View file

@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import AdvancedParams from './Advanced/AdvancedParams.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import AdvancedParams from './Advanced/AdvancedParams.svelte';
export let saveSettings: Function; export let saveSettings: Function;
// Advanced // Advanced
@ -55,14 +57,14 @@
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div class=" text-sm font-medium">Parameters</div> <div class=" text-sm font-medium">{$i18n.t('Parameters')}</div>
<AdvancedParams bind:options /> <AdvancedParams bind:options />
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div class=" py-1 w-full justify-between"> <div class=" py-1 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Keep Alive</div> <div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -72,9 +74,9 @@
}} }}
> >
{#if keepAlive === null} {#if keepAlive === null}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -84,7 +86,7 @@
<input <input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text" type="text"
placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`} placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
bind:value={keepAlive} bind:value={keepAlive}
/> />
</div> </div>
@ -93,7 +95,7 @@
<div> <div>
<div class=" py-1 flex w-full justify-between"> <div class=" py-1 flex w-full justify-between">
<div class=" self-center text-sm font-medium">Request Mode</div> <div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -102,7 +104,7 @@
}} }}
> >
{#if requestFormat === ''} {#if requestFormat === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else if requestFormat === 'json'} {:else if requestFormat === 'json'}
<!-- <svg <!-- <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -114,7 +116,7 @@
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/> />
</svg> --> </svg> -->
<span class="ml-2 self-center"> JSON </span> <span class="ml-2 self-center">{$i18n.t('JSON')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -147,7 +149,7 @@
dispatch('save'); dispatch('save');
}} }}
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,4 +1,8 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
export let options = { export let options = {
// Advanced // Advanced
seed: 0, seed: 0,
@ -20,7 +24,7 @@
<div class=" space-y-3 text-xs"> <div class=" space-y-3 text-xs">
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">Seed</div> <div class=" w-20 text-xs font-medium self-center">{$i18n.t('Seed')}</div>
<div class=" flex-1 self-center"> <div class=" flex-1 self-center">
<input <input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
@ -36,12 +40,12 @@
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div> <div class=" w-20 text-xs font-medium self-center">{$i18n.t('Stop Sequence')}</div>
<div class=" flex-1 self-center"> <div class=" flex-1 self-center">
<input <input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text" type="text"
placeholder="Enter Stop Sequence" placeholder={$i18n.t('Enter stop sequence')}
bind:value={options.stop} bind:value={options.stop}
autocomplete="off" autocomplete="off"
/> />
@ -51,7 +55,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Temperature</div> <div class=" self-center text-xs font-medium">{$i18n.t('Temperature')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -61,9 +65,9 @@
}} }}
> >
{#if options.temperature === ''} {#if options.temperature === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if} {/if}
</button> </button>
</div> </div>
@ -97,7 +101,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat</div> <div class=" self-center text-xs font-medium">{$i18n.t('Mirostat')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -107,9 +111,9 @@
}} }}
> >
{#if options.mirostat === ''} {#if options.mirostat === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -143,7 +147,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat Eta</div> <div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Eta')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -153,9 +157,9 @@
}} }}
> >
{#if options.mirostat_eta === ''} {#if options.mirostat_eta === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -189,7 +193,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat Tau</div> <div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Tau')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -199,9 +203,9 @@
}} }}
> >
{#if options.mirostat_tau === ''} {#if options.mirostat_tau === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -235,7 +239,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Top K</div> <div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -245,9 +249,9 @@
}} }}
> >
{#if options.top_k === ''} {#if options.top_k === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -281,7 +285,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Top P</div> <div class=" self-center text-xs font-medium">{$i18n.t('Top P')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -291,9 +295,9 @@
}} }}
> >
{#if options.top_p === ''} {#if options.top_p === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -327,7 +331,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Repeat Penalty</div> <div class=" self-center text-xs font-medium">{$i18n.t('Repeat Penalty')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -337,9 +341,9 @@
}} }}
> >
{#if options.repeat_penalty === ''} {#if options.repeat_penalty === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -373,7 +377,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Repeat Last N</div> <div class=" self-center text-xs font-medium">{$i18n.t('Repeat Last N')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -383,9 +387,9 @@
}} }}
> >
{#if options.repeat_last_n === ''} {#if options.repeat_last_n === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -419,7 +423,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Tfs Z</div> <div class=" self-center text-xs font-medium">{$i18n.t('Tfs Z')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -429,9 +433,9 @@
}} }}
> >
{#if options.tfs_z === ''} {#if options.tfs_z === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -465,7 +469,7 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Context Length</div> <div class=" self-center text-xs font-medium">{$i18n.t('Context Length')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -475,9 +479,9 @@
}} }}
> >
{#if options.num_ctx === ''} {#if options.num_ctx === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -510,7 +514,7 @@
</div> </div>
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Max Tokens</div> <div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -520,9 +524,9 @@
}} }}
> >
{#if options.num_predict === ''} {#if options.num_predict === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>

View file

@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function; export let saveSettings: Function;
// Audio // Audio
@ -101,32 +103,36 @@
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div> <div>
<div class=" mb-1 text-sm font-medium">STT Settings</div> <div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Speech-to-Text Engine</div> <div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={STTEngine} bind:value={STTEngine}
placeholder="Select a mode" placeholder="Select a mode"
on:change={(e) => { on:change={(e) => {
if (e.target.value !== '') { if (e.target.value !== '') {
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) { navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
toast.error(`Permission denied when accessing microphone: ${err}`); toast.error(
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
error: err
})
);
STTEngine = ''; STTEngine = '';
}); });
} }
}} }}
> >
<option value="">Default (Web API)</option> <option value="">{$i18n.t('Default (Web API)')}</option>
<option value="whisper-local">Whisper (Local)</option> <option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
</select> </select>
</div> </div>
</div> </div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Conversation Mode</div> <div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -136,15 +142,17 @@
type="button" type="button"
> >
{#if conversationMode === true} {#if conversationMode === true}
<span class="ml-2 self-center">On</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Auto-send input after 3 sec.</div> <div class=" self-center text-xs font-medium">
{$i18n.t('Auto-send input after 3 sec.')}
</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -154,22 +162,22 @@
type="button" type="button"
> >
{#if speechAutoSend === true} {#if speechAutoSend === true}
<span class="ml-2 self-center">On</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
</div> </div>
<div> <div>
<div class=" mb-1 text-sm font-medium">TTS Settings</div> <div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Text-to-Speech Engine</div> <div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={TTSEngine} bind:value={TTSEngine}
placeholder="Select a mode" placeholder="Select a mode"
on:change={(e) => { on:change={(e) => {
@ -182,14 +190,14 @@
} }
}} }}
> >
<option value="">Default (Web API)</option> <option value="">{$i18n.t('Default (Web API)')}</option>
<option value="openai">Open AI</option> <option value="openai">{$i18n.t('Open AI')}</option>
</select> </select>
</div> </div>
</div> </div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Auto-playback response</div> <div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -199,9 +207,9 @@
type="button" type="button"
> >
{#if responseAutoPlayback === true} {#if responseAutoPlayback === true}
<span class="ml-2 self-center">On</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -211,7 +219,7 @@
{#if TTSEngine === ''} {#if TTSEngine === ''}
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Set Voice</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<select <select
@ -219,7 +227,7 @@
bind:value={speaker} bind:value={speaker}
placeholder="Select a voice" placeholder="Select a voice"
> >
<option value="" selected>Default</option> <option value="" selected>{$i18n.t('Default')}</option>
{#each voices.filter((v) => v.localService === true) as voice} {#each voices.filter((v) => v.localService === true) as voice}
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option <option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
> >
@ -230,7 +238,7 @@
</div> </div>
{:else if TTSEngine === 'openai'} {:else if TTSEngine === 'openai'}
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Set Voice</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<select <select
@ -251,10 +259,10 @@
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -13,10 +13,12 @@
getChatList getChatList
} from '$lib/apis/chats'; } from '$lib/apis/chats';
import { getImportOrigin, convertOpenAIChats } from '$lib/utils'; import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let saveSettings: Function; export let saveSettings: Function;
// Chats // Chats
let saveChatHistory = true; let saveChatHistory = true;
@ -99,13 +101,13 @@
}); });
</script> </script>
<div class="flex flex-col h-full justify-between space-y-3 text-sm"> <div class="flex flex-col h-full justify-between space-y-3 text-sm max-h-[22rem]">
<div class=" space-y-2"> <div class=" space-y-2">
<div <div
class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition" class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
> >
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-sm font-medium">Chat History</div> <div class=" self-center text-sm font-medium">{$i18n.t('Chat History')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -129,7 +131,7 @@
/> />
</svg> </svg>
<span class="ml-2 self-center"> On </span> <span class="ml-2 self-center"> {$i18n.t('On')} </span>
{:else} {:else}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -147,13 +149,13 @@
/> />
</svg> </svg>
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
<div class="text-xs text-left w-full font-medium mt-0.5"> <div class="text-xs text-left w-full font-medium mt-0.5">
This setting does not sync across browsers or devices. {$i18n.t('This setting does not sync across browsers or devices.')}
</div> </div>
</div> </div>
@ -188,7 +190,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center text-sm font-medium">Import Chats</div> <div class=" self-center text-sm font-medium">{$i18n.t('Import Chats')}</div>
</button> </button>
<button <button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
@ -210,7 +212,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center text-sm font-medium">Export Chats</div> <div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div>
</button> </button>
</div> </div>
@ -232,7 +234,7 @@
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
<span>Are you sure?</span> <span>{$i18n.t('Are you sure?')}</span>
</div> </div>
<div class="flex space-x-1.5 items-center"> <div class="flex space-x-1.5 items-center">
@ -296,7 +298,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center text-sm font-medium">Delete Chats</div> <div class=" self-center text-sm font-medium">{$i18n.t('Delete Chats')}</div>
</button> </button>
{/if} {/if}
@ -324,7 +326,9 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center text-sm font-medium">Export All Chats (All Users)</div> <div class=" self-center text-sm font-medium">
{$i18n.t('Export All Chats (All Users)')}
</div>
</button> </button>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
@ -338,7 +342,7 @@
}); });
if (res) { if (res) {
toast.success('Success'); toast.success($i18n.t('Success'));
} }
}} }}
> >
@ -356,7 +360,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center text-sm font-medium">Reset Vector Storage</div> <div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
</button> </button>
{/if} {/if}
</div> </div>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { models, user } from '$lib/stores'; import { models, user } from '$lib/stores';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama'; import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
@ -12,6 +12,8 @@
} from '$lib/apis/openai'; } from '$lib/apis/openai';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let getModels: Function; export let getModels: Function;
// External // External
@ -42,7 +44,7 @@
}); });
if (ollamaVersion) { if (ollamaVersion) {
toast.success('Server connection verified'); toast.success($i18n.t('Server connection verified'));
await models.set(await getModels()); await models.set(await getModels());
} }
}; };
@ -63,17 +65,17 @@
dispatch('save'); dispatch('save');
}} }}
> >
<div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3"> <div class=" pr-1.5 overflow-y-scroll max-h-[22rem] space-y-3">
<div class=" space-y-3"> <div class=" space-y-3">
<div class="mt-2 space-y-2 pr-1.5"> <div class="mt-2 space-y-2 pr-1.5">
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<div class=" font-medium">OpenAI API</div> <div class=" font-medium">{$i18n.t('OpenAI API')}</div>
<button <button
class=" text-xs font-medium text-gray-500" class=" text-xs font-medium text-gray-500"
type="button" type="button"
on:click={() => { on:click={() => {
showOpenAI = !showOpenAI; showOpenAI = !showOpenAI;
}}>{showOpenAI ? 'Hide' : 'Show'}</button }}>{showOpenAI ? $i18n.t('Hide') : $i18n.t('Show')}</button
> >
</div> </div>
@ -84,7 +86,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="API Base URL" placeholder={$i18n.t('API Base URL')}
bind:value={url} bind:value={url}
autocomplete="off" autocomplete="off"
/> />
@ -93,7 +95,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="API Key" placeholder={$i18n.t('API Key')}
bind:value={OPENAI_API_KEYS[idx]} bind:value={OPENAI_API_KEYS[idx]}
autocomplete="off" autocomplete="off"
/> />
@ -143,7 +145,8 @@
</div> </div>
</div> </div>
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500"> <div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
WebUI will make requests to <span class=" text-gray-200">'{url}/models'</span> {$i18n.t('WebUI will make requests to')}
<span class=" text-gray-200">'{url}/models'</span>
</div> </div>
{/each} {/each}
</div> </div>
@ -154,7 +157,7 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Base URL')}</div>
<div class="flex w-full gap-1.5"> <div class="flex w-full gap-1.5">
<div class="flex-1 flex flex-col gap-2"> <div class="flex-1 flex flex-col gap-2">
{#each OLLAMA_BASE_URLS as url, idx} {#each OLLAMA_BASE_URLS as url, idx}
@ -233,13 +236,13 @@
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Trouble accessing Ollama? {$i18n.t('Trouble accessing Ollama?')}
<a <a
class=" text-gray-300 font-medium" class=" text-gray-300 font-medium underline"
href="https://github.com/open-webui/open-webui#troubleshooting" href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank" target="_blank"
> >
Click here for help. {$i18n.t('Click here for help.')}
</a> </a>
</div> </div>
</div> </div>
@ -247,10 +250,10 @@
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -1,10 +1,13 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { theme, setTheme } from '../../../stores/index'; import { getLanguages } from '$lib/i18n';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { models, user } from '$lib/stores'; import { models, user } from '$lib/stores';
import { theme, setTheme } from '../../../stores/index';
const i18n = getContext('i18n');
import AdvancedParams from './Advanced/AdvancedParams.svelte'; import AdvancedParams from './Advanced/AdvancedParams.svelte';
@ -16,6 +19,9 @@
let selectedTheme = 'system'; let selectedTheme = 'system';
let actualTheme: string; let actualTheme: string;
$: actualTheme = $theme; $: actualTheme = $theme;
let languages = [];
let lang = $i18n.language;
let notificationEnabled = false; let notificationEnabled = false;
let system = ''; let system = '';
@ -91,6 +97,7 @@
applyTheme(selectedTheme); applyTheme(selectedTheme);
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
languages = await getLanguages();
notificationEnabled = settings.notificationEnabled ?? false; notificationEnabled = settings.notificationEnabled ?? false;
system = settings.system ?? ''; system = settings.system ?? '';
@ -117,59 +124,61 @@
</script> </script>
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem]"> <div class=" pr-1.5 overflow-y-scroll max-h-[22rem]">
<div class=""> <div class="">
<div class=" mb-1 text-sm font-medium">WebUI Settings</div> <div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div>
<div class=" py-0.5 flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Theme</div> <div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<div class=" absolute right-16">
{#if actualTheme === 'dark'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
clip-rule="evenodd"
/>
</svg>
{:else if actualTheme === 'light'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4 self-center"
>
<path
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/>
</svg>
{/if}
</div>
<select <select
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={selectedTheme} bind:value={theme}
placeholder="Select a theme" placeholder="Select a theme"
on:change="{() => handleThemeChange(selectedTheme)}" on:change="{() => handleThemeChange(selectedTheme)}"
> >
<option value="system">System</option> <option value="system">System</option>
<option value="dark">Dark</option> <option value="dark">🌑 {$i18n.t('Dark')}</option>
<option value="light">Light</option> <option value="light">☀️ {$i18n.t('Light')}</option>
<option value="rose-pine dark">Rosé Pine</option> <option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
<option value="rose-pine-dawn light">Rosé Pine Dawn</option> <option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
</select> </select>
</div> </div>
</div> </div>
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Language')}</div>
<div class="flex items-center relative">
<select
class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={lang}
placeholder="Select a language"
on:change={(e) => {
$i18n.changeLanguage(lang);
}}
>
{#each languages as language}
<option value={language['code']}>{language['title']}</option>
{/each}
</select>
</div>
</div>
{#if $i18n.language === 'en-US'}
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
Couldn't find your language?
<a
class=" text-gray-300 font-medium underline"
href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization"
target="_blank"
>
Help us translate Open WebUI!
</a>
</div>
{/if}
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Notification</div> <div class=" self-center text-xs font-medium">{$i18n.t('Desktop Notifications')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -179,9 +188,9 @@
type="button" type="button"
> >
{#if notificationEnabled === true} {#if notificationEnabled === true}
<span class="ml-2 self-center">On</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -191,7 +200,7 @@
<hr class=" dark:border-gray-700 my-3" /> <hr class=" dark:border-gray-700 my-3" />
<div> <div>
<div class=" my-2.5 text-sm font-medium">System Prompt</div> <div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
<textarea <textarea
bind:value={system} bind:value={system}
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
@ -201,13 +210,13 @@
<div class="mt-2 space-y-3 pr-1.5"> <div class="mt-2 space-y-3 pr-1.5">
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<div class=" font-medium">Advanced Parameters</div> <div class=" font-medium">{$i18n.t('Advanced Parameters')}</div>
<button <button
class=" text-xs font-medium text-gray-500" class=" text-xs font-medium text-gray-500"
type="button" type="button"
on:click={() => { on:click={() => {
showAdvanced = !showAdvanced; showAdvanced = !showAdvanced;
}}>{showAdvanced ? 'Hide' : 'Show'}</button }}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button
> >
</div> </div>
@ -217,7 +226,7 @@
<div class=" py-1 w-full justify-between"> <div class=" py-1 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Keep Alive</div> <div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -227,9 +236,9 @@
}} }}
> >
{#if keepAlive === null} {#if keepAlive === null}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else} {:else}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if} {/if}
</button> </button>
</div> </div>
@ -239,7 +248,7 @@
<input <input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text" type="text"
placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`} placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
bind:value={keepAlive} bind:value={keepAlive}
/> />
</div> </div>
@ -248,7 +257,7 @@
<div> <div>
<div class=" py-1 flex w-full justify-between"> <div class=" py-1 flex w-full justify-between">
<div class=" self-center text-sm font-medium">Request Mode</div> <div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -257,7 +266,7 @@
}} }}
> >
{#if requestFormat === ''} {#if requestFormat === ''}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else if requestFormat === 'json'} {:else if requestFormat === 'json'}
<!-- <svg <!-- <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -269,7 +278,7 @@
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/> />
</svg> --> </svg> -->
<span class="ml-2 self-center"> JSON </span> <span class="ml-2 self-center"> {$i18n.t('JSON')} </span>
{/if} {/if}
</button> </button>
</div> </div>
@ -280,7 +289,7 @@
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
on:click={() => { on:click={() => {
saveSettings({ saveSettings({
system: system !== '' ? system : undefined, system: system !== '' ? system : undefined,
@ -304,7 +313,7 @@
dispatch('save'); dispatch('save');
}} }}
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { config, user } from '$lib/stores'; import { config, user } from '$lib/stores';
import { import {
getAUTOMATIC1111Url, getAUTOMATIC1111Url,
@ -21,6 +21,8 @@
import { getBackendConfig } from '$lib/apis'; import { getBackendConfig } from '$lib/apis';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function; export let saveSettings: Function;
let loading = false; let loading = false;
@ -61,7 +63,7 @@
await getModels(); await getModels();
if (models) { if (models) {
toast.success('Server connection verified'); toast.success($i18n.t('Server connection verified'));
} }
} else { } else {
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token); AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
@ -116,11 +118,13 @@
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
loading = true; loading = true;
if (imageGenerationEngine === 'openai') {
await updateOpenAIKey(localStorage.token, OPENAI_API_KEY); await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
}
await updateDefaultImageGenerationModel(localStorage.token, selectedModel); await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
await updateImageSize(localStorage.token, imageSize).catch((error) => { await updateImageSize(localStorage.token, imageSize).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
@ -134,39 +138,41 @@
loading = false; loading = false;
}} }}
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[20.5rem]"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[24rem]">
<div> <div>
<div class=" mb-1 text-sm font-medium">Image Settings</div> <div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Image Generation Engine</div> <div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={imageGenerationEngine} bind:value={imageGenerationEngine}
placeholder="Select a mode" placeholder={$i18n.t('Select a mode')}
on:change={async () => { on:change={async () => {
await updateImageGeneration(); await updateImageGeneration();
}} }}
> >
<option value="">Default (Automatic1111)</option> <option value="">{$i18n.t('Default (Automatic1111)')}</option>
<option value="openai">Open AI (Dall-E)</option> <option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
</select> </select>
</div> </div>
</div> </div>
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Image Generation (Experimental)</div> <div class=" self-center text-xs font-medium">
{$i18n.t('Image Generation (Experimental)')}
</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
on:click={() => { on:click={() => {
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') { if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
toast.error('AUTOMATIC1111 Base URL is required.'); toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
enableImageGeneration = false; enableImageGeneration = false;
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') { } else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
toast.error('OpenAI API Key is required.'); toast.error($i18n.t('OpenAI API Key is required.'));
enableImageGeneration = false; enableImageGeneration = false;
} else { } else {
enableImageGeneration = !enableImageGeneration; enableImageGeneration = !enableImageGeneration;
@ -177,9 +183,9 @@
type="button" type="button"
> >
{#if enableImageGeneration === true} {#if enableImageGeneration === true}
<span class="ml-2 self-center">On</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -188,12 +194,12 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
{#if imageGenerationEngine === ''} {#if imageGenerationEngine === ''}
<div class=" mb-2.5 text-sm font-medium">AUTOMATIC1111 Base URL</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter URL (e.g. http://127.0.0.1:7860/)" placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={AUTOMATIC1111_BASE_URL} bind:value={AUTOMATIC1111_BASE_URL}
/> />
</div> </div>
@ -222,22 +228,22 @@
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Include `--api` flag when running stable-diffusion-webui {$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
<a <a
class=" text-gray-300 font-medium" class=" text-gray-300 font-medium"
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734" href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
target="_blank" target="_blank"
> >
(e.g. `sh webui.sh --api`) {$i18n.t('(e.g. `sh webui.sh --api`)')}
</a> </a>
</div> </div>
{:else if imageGenerationEngine === 'openai'} {:else if imageGenerationEngine === 'openai'}
<div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter API Key" placeholder={$i18n.t('Enter API Key')}
bind:value={OPENAI_API_KEY} bind:value={OPENAI_API_KEY}
/> />
</div> </div>
@ -248,16 +254,16 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Set Default Model</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedModel} bind:value={selectedModel}
placeholder="Select a model" placeholder={$i18n.t('Select a model')}
> >
{#if !selectedModel} {#if !selectedModel}
<option value="" disabled selected>Select a model</option> <option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if} {/if}
{#each models ?? [] as model} {#each models ?? [] as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option> <option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
@ -268,12 +274,12 @@
</div> </div>
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Set Image Size</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter Image Size (e.g. 512x512)" placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
bind:value={imageSize} bind:value={imageSize}
/> />
</div> </div>
@ -281,12 +287,12 @@
</div> </div>
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Set Steps</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter Number of Steps (e.g. 50)" placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
bind:value={steps} bind:value={steps}
/> />
</div> </div>
@ -297,13 +303,13 @@
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded flex flex-row space-x-1 items-center {loading class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading
? ' cursor-not-allowed' ? ' cursor-not-allowed'
: ''}" : ''}"
type="submit" type="submit"
disabled={loading} disabled={loading}
> >
Save {$i18n.t('Save')}
{#if loading} {#if loading}
<div class="ml-2 self-center"> <div class="ml-2 self-center">

View file

@ -2,10 +2,12 @@
import { getBackendConfig } from '$lib/apis'; import { getBackendConfig } from '$lib/apis';
import { setDefaultPromptSuggestions } from '$lib/apis/configs'; import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import { config, models, user } from '$lib/stores'; import { config, models, user } from '$lib/stores';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function; export let saveSettings: Function;
// Addons // Addons
@ -63,6 +65,7 @@
} }
saveSettings({ saveSettings({
titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
}); });
}; };
@ -81,7 +84,9 @@
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? ''; titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
titleGenerationPrompt = titleGenerationPrompt =
settings.titleGenerationPrompt ?? settings.titleGenerationPrompt ??
`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': {{prompt}}`; $i18n.t(
"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':"
) + ' {{prompt}}';
}); });
</script> </script>
@ -92,13 +97,13 @@
dispatch('save'); dispatch('save');
}} }}
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
<div> <div>
<div class=" mb-1 text-sm font-medium">WebUI Add-ons</div> <div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Title Auto-Generation</div> <div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -108,9 +113,9 @@
type="button" type="button"
> >
{#if titleAutoGenerate === true} {#if titleAutoGenerate === true}
<span class="ml-2 self-center">On</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -118,7 +123,9 @@
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Response AutoCopy to Clipboard</div> <div class=" self-center text-xs font-medium">
{$i18n.t('Response AutoCopy to Clipboard')}
</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -128,9 +135,9 @@
type="button" type="button"
> >
{#if responseAutoCopy === true} {#if responseAutoCopy === true}
<span class="ml-2 self-center">On</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -138,7 +145,7 @@
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Full Screen Mode</div> <div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -148,9 +155,9 @@
type="button" type="button"
> >
{#if fullScreenMode === true} {#if fullScreenMode === true}
<span class="ml-2 self-center">On</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -159,7 +166,7 @@
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
Display the username instead of "You" in the Chat {$i18n.t('Display the username instead of You in the Chat')}
</div> </div>
<button <button
@ -170,9 +177,9 @@
type="button" type="button"
> >
{#if showUsername === true} {#if showUsername === true}
<span class="ml-2 self-center">On</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">Off</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -182,15 +189,15 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Set Title Auto-Generation Model</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={titleAutoGenerateModel} bind:value={titleAutoGenerateModel}
placeholder="Select a model" placeholder={$i18n.t('Select a model')}
> >
<option value="" selected>Current Model</option> <option value="" selected>{$i18n.t('Current Model')}</option>
{#each $models as model} {#each $models as model}
{#if model.size != null} {#if model.size != null}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"> <option value={model.name} class="bg-gray-100 dark:bg-gray-700">
@ -200,35 +207,13 @@
{/each} {/each}
</select> </select>
</div> </div>
<button
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
on:click={() => {
saveSettings({
titleAutoGenerateModel:
titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined
});
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3.5 h-3.5"
>
<path
fill-rule="evenodd"
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div> </div>
<div class="mt-3">
<div class=" mb-2.5 text-sm font-medium">Title Generation Prompt</div> <div class="mt-3 mr-2">
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
<textarea <textarea
bind:value={titleGenerationPrompt} bind:value={titleGenerationPrompt}
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="3" rows="3"
/> />
</div> </div>
@ -239,7 +224,9 @@
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div class="flex w-full justify-between mb-2"> <div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">Default Prompt Suggestions</div> <div class=" self-center text-sm font-semibold">
{$i18n.t('Default Prompt Suggestions')}
</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -312,19 +299,19 @@
{#if promptSuggestions.length > 0} {#if promptSuggestions.length > 0}
<div class="text-xs text-left w-full mt-2"> <div class="text-xs text-left w-full mt-2">
Adjusting these settings will apply changes universally to all users. {$i18n.t('Adjusting these settings will apply changes universally to all users.')}
</div> </div>
{/if} {/if}
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end text-sm font-medium">
<button <button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -12,8 +12,11 @@
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, models, user } from '$lib/stores'; import { WEBUI_NAME, models, user } from '$lib/stores';
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm'; import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let getModels: Function; export let getModels: Function;
@ -37,6 +40,10 @@
let OLLAMA_URLS = []; let OLLAMA_URLS = [];
let selectedOllamaUrlIdx: string | null = null; let selectedOllamaUrlIdx: string | null = null;
let updateModelId = null;
let updateProgress = null;
let showExperimentalOllama = false; let showExperimentalOllama = false;
let ollamaVersion = ''; let ollamaVersion = '';
const MAX_PARALLEL_DOWNLOADS = 3; const MAX_PARALLEL_DOWNLOADS = 3;
@ -61,14 +68,85 @@
let deleteModelTag = ''; let deleteModelTag = '';
const updateModelsHandler = async () => {
for (const model of $models.filter(
(m) =>
m.size != null &&
(selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))
)) {
console.log(model);
updateModelId = model.id;
const res = await pullModel(localStorage.token, model.id, selectedOllamaUrlIdx).catch(
(error) => {
toast.error(error);
return null;
}
);
if (res) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
try {
const { value, done } = await reader.read();
if (done) break;
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
let data = JSON.parse(line);
console.log(data);
if (data.error) {
throw data.error;
}
if (data.detail) {
throw data.detail;
}
if (data.status) {
if (data.digest) {
updateProgress = 0;
if (data.completed) {
updateProgress = Math.round((data.completed / data.total) * 1000) / 10;
} else {
updateProgress = 100;
}
} else {
toast.success(data.status);
}
}
}
}
} catch (error) {
console.log(error);
}
}
}
}
updateModelId = null;
updateProgress = null;
};
const pullModelHandler = async () => { const pullModelHandler = async () => {
const sanitizedModelTag = modelTag.trim(); const sanitizedModelTag = modelTag.trim();
if (modelDownloadStatus[sanitizedModelTag]) { if (modelDownloadStatus[sanitizedModelTag]) {
toast.error(`Model '${sanitizedModelTag}' is already in queue for downloading.`); toast.error(
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
modelTag: sanitizedModelTag
})
);
return; return;
} }
if (Object.keys(modelDownloadStatus).length === 3) { if (Object.keys(modelDownloadStatus).length === 3) {
toast.error('Maximum of 3 models can be downloaded simultaneously. Please try again later.'); toast.error(
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
);
return; return;
} }
@ -86,10 +164,12 @@
if (!data.success) { if (!data.success) {
toast.error(data.error); toast.error(data.error);
} else { } else {
toast.success(`Model '${modelName}' has been successfully downloaded.`); toast.success(
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName })
);
const notification = new Notification($WEBUI_NAME, { const notification = new Notification($WEBUI_NAME, {
body: `Model '${modelName}' has been successfully downloaded.`, body: $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName }),
icon: `${WEBUI_BASE_URL}/static/favicon.png` icon: `${WEBUI_BASE_URL}/static/favicon.png`
}); });
@ -253,7 +333,7 @@
); );
if (res) { if (res) {
toast.success(`Deleted ${deleteModelTag}`); toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
} }
deleteModelTag = ''; deleteModelTag = '';
@ -340,7 +420,7 @@
} }
} }
} else { } else {
toast.error(`Model ${liteLLMModelName} already exists.`); toast.error($i18n.t(`Model {{modelName}} already exists.`, { modelName: liteLLMModelName }));
} }
liteLLMModelName = ''; liteLLMModelName = '';
@ -379,7 +459,7 @@
return []; return [];
}); });
if (OLLAMA_URLS.length > 1) { if (OLLAMA_URLS.length > 0) {
selectedOllamaUrlIdx = 0; selectedOllamaUrlIdx = 0;
} }
@ -389,33 +469,68 @@
</script> </script>
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[23rem]"> <div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
{#if ollamaVersion} {#if ollamaVersion}
<div class="space-y-2 pr-1.5"> <div class="space-y-2 pr-1.5">
<div class="text-sm font-medium">Manage Ollama Models</div> <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
{#if OLLAMA_URLS.length > 1} {#if OLLAMA_URLS.length > 0}
<div class="flex gap-2">
<div class="flex-1 pb-1"> <div class="flex-1 pb-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedOllamaUrlIdx} bind:value={selectedOllamaUrlIdx}
placeholder="Select an Ollama instance" placeholder={$i18n.t('Select an Ollama instance')}
> >
{#each OLLAMA_URLS as url, idx} {#each OLLAMA_URLS as url, idx}
<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option> <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
{/each} {/each}
</select> </select>
</div> </div>
<div>
<div class="flex w-full justify-end">
<Tooltip content="Update All Models" placement="top">
<button
class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
updateModelsHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z"
/>
<path
d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z"
/>
</svg>
</button>
</Tooltip>
</div>
</div>
</div>
{#if updateModelId}
Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
{/if}
{/if} {/if}
<div class="space-y-2"> <div class="space-y-2">
<div> <div>
<div class=" mb-2 text-sm font-medium">Pull a model from Ollama.com</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter model tag (e.g. mistral:7b)" placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
modelTag: 'mistral:7b'
})}
bind:value={modelTag} bind:value={modelTag}
/> />
</div> </div>
@ -471,10 +586,11 @@
</div> </div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
To access the available model names for downloading, <a {$i18n.t('To access the available model names for downloading,')}
<a
class=" text-gray-500 dark:text-gray-300 font-medium underline" class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://ollama.com/library" href="https://ollama.com/library"
target="_blank">click here.</a target="_blank">{$i18n.t('click here.')}</a
> >
</div> </div>
@ -499,16 +615,16 @@
</div> </div>
<div> <div>
<div class=" mb-2 text-sm font-medium">Delete a model</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteModelTag} bind:value={deleteModelTag}
placeholder="Select a model" placeholder={$i18n.t('Select a model')}
> >
{#if !deleteModelTag} {#if !deleteModelTag}
<option value="" disabled selected>Select a model</option> <option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if} {/if}
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model} {#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700" <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
@ -541,13 +657,13 @@
<div class="pt-1"> <div class="pt-1">
<div class="flex justify-between items-center text-xs"> <div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">Experimental</div> <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
<button <button
class=" text-xs font-medium text-gray-500" class=" text-xs font-medium text-gray-500"
type="button" type="button"
on:click={() => { on:click={() => {
showExperimentalOllama = !showExperimentalOllama; showExperimentalOllama = !showExperimentalOllama;
}}>{showExperimentalOllama ? 'Hide' : 'Show'}</button }}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
> >
</div> </div>
</div> </div>
@ -559,7 +675,7 @@
}} }}
> >
<div class=" mb-2 flex w-full justify-between"> <div class=" mb-2 flex w-full justify-between">
<div class=" text-sm font-medium">Upload a GGUF model</div> <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -573,9 +689,9 @@
type="button" type="button"
> >
{#if modelUploadMode === 'file'} {#if modelUploadMode === 'file'}
<span class="ml-2 self-center">File Mode</span> <span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
{:else} {:else}
<span class="ml-2 self-center">URL Mode</span> <span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
{/if} {/if}
</button> </button>
</div> </div>
@ -607,7 +723,7 @@
{#if modelInputFile && modelInputFile.length > 0} {#if modelInputFile && modelInputFile.length > 0}
{modelInputFile[0].name} {modelInputFile[0].name}
{:else} {:else}
Click here to select {$i18n.t('Click here to select')}
{/if} {/if}
</button> </button>
</div> </div>
@ -621,7 +737,7 @@
type="url" type="url"
required required
bind:value={modelFileUrl} bind:value={modelFileUrl}
placeholder="Type Hugging Face Resolve (Download) URL" placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
/> />
</div> </div>
{/if} {/if}
@ -681,7 +797,7 @@
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<div> <div>
<div> <div>
<div class=" my-2.5 text-sm font-medium">Modelfile Content</div> <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
<textarea <textarea
bind:value={modelFileContent} bind:value={modelFileContent}
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
@ -691,16 +807,17 @@
</div> </div>
{/if} {/if}
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500"> <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
To access the GGUF models available for downloading, <a {$i18n.t('To access the GGUF models available for downloading,')}
<a
class=" text-gray-500 dark:text-gray-300 font-medium underline" class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://huggingface.co/models?search=gguf" href="https://huggingface.co/models?search=gguf"
target="_blank">click here.</a target="_blank">{$i18n.t('click here.')}</a
> >
</div> </div>
{#if uploadProgress !== null} {#if uploadProgress !== null}
<div class="mt-2"> <div class="mt-2">
<div class=" mb-2 text-xs">Upload Progress</div> <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
<div class="w-full rounded-full dark:bg-gray-800"> <div class="w-full rounded-full dark:bg-gray-800">
<div <div
@ -727,13 +844,13 @@
<div> <div>
<div class="mb-2"> <div class="mb-2">
<div class="flex justify-between items-center text-xs"> <div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">Manage LiteLLM Models</div> <div class=" text-sm font-medium">{$i18n.t('Manage LiteLLM Models')}</div>
<button <button
class=" text-xs font-medium text-gray-500" class=" text-xs font-medium text-gray-500"
type="button" type="button"
on:click={() => { on:click={() => {
showLiteLLM = !showLiteLLM; showLiteLLM = !showLiteLLM;
}}>{showLiteLLM ? 'Hide' : 'Show'}</button }}>{showLiteLLM ? $i18n.t('Hide') : $i18n.t('Show')}</button
> >
</div> </div>
</div> </div>
@ -741,14 +858,16 @@
{#if showLiteLLM} {#if showLiteLLM}
<div> <div>
<div class="flex justify-between items-center text-xs"> <div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">Add a model</div> <div class=" text-sm font-medium">{$i18n.t('Add a model')}</div>
<button <button
class=" text-xs font-medium text-gray-500" class=" text-xs font-medium text-gray-500"
type="button" type="button"
on:click={() => { on:click={() => {
showLiteLLMParams = !showLiteLLMParams; showLiteLLMParams = !showLiteLLMParams;
}} }}
>{showLiteLLMParams ? 'Hide Additional Params' : 'Show Additional Params'}</button >{showLiteLLMParams
? $i18n.t('Hide Additional Params')
: $i18n.t('Show Additional Params')}</button
> >
</div> </div>
</div> </div>
@ -758,7 +877,7 @@
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM Model (litellm_params.model)" placeholder={$i18n.t('Enter LiteLLM Model (litellm_params.model)')}
bind:value={liteLLMModel} bind:value={liteLLMModel}
autocomplete="off" autocomplete="off"
/> />
@ -785,7 +904,7 @@
{#if showLiteLLMParams} {#if showLiteLLMParams}
<div> <div>
<div class=" mb-1.5 text-sm font-medium">Model Name</div> <div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Name')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
@ -799,12 +918,14 @@
</div> </div>
<div> <div>
<div class=" mb-1.5 text-sm font-medium">API Base URL</div> <div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Base URL')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM API Base URL (litellm_params.api_base)" placeholder={$i18n.t(
'Enter LiteLLM API Base URL (litellm_params.api_base)'
)}
bind:value={liteLLMAPIBase} bind:value={liteLLMAPIBase}
autocomplete="off" autocomplete="off"
/> />
@ -813,12 +934,12 @@
</div> </div>
<div> <div>
<div class=" mb-1.5 text-sm font-medium">API Key</div> <div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Key')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM API Key (litellm_params.api_key)" placeholder={$i18n.t('Enter LiteLLM API Key (litellm_params.api_key)')}
bind:value={liteLLMAPIKey} bind:value={liteLLMAPIKey}
autocomplete="off" autocomplete="off"
/> />
@ -827,12 +948,12 @@
</div> </div>
<div> <div>
<div class="mb-1.5 text-sm font-medium">API RPM</div> <div class="mb-1.5 text-sm font-medium">{$i18n.t('API RPM')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM API RPM (litellm_params.rpm)" placeholder={$i18n.t('Enter LiteLLM API RPM (litellm_params.rpm)')}
bind:value={liteLLMRPM} bind:value={liteLLMRPM}
autocomplete="off" autocomplete="off"
/> />
@ -841,12 +962,12 @@
</div> </div>
<div> <div>
<div class="mb-1.5 text-sm font-medium">Max Tokens</div> <div class="mb-1.5 text-sm font-medium">{$i18n.t('Max Tokens')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter Max Tokens (litellm_params.max_tokens)" placeholder={$i18n.t('Enter Max Tokens (litellm_params.max_tokens)')}
bind:value={liteLLMMaxTokens} bind:value={liteLLMMaxTokens}
type="number" type="number"
min="1" min="1"
@ -859,27 +980,27 @@
</div> </div>
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
Not sure what to add? {$i18n.t('Not sure what to add?')}
<a <a
class=" text-gray-300 font-medium underline" class=" text-gray-300 font-medium underline"
href="https://litellm.vercel.app/docs/proxy/configs#quick-start" href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
target="_blank" target="_blank"
> >
Click here for help. {$i18n.t('Click here for help.')}
</a> </a>
</div> </div>
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Delete a model</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Delete a model')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteLiteLLMModelId} bind:value={deleteLiteLLMModelId}
placeholder="Select a model" placeholder={$i18n.t('Select a model')}
> >
{#if !deleteLiteLLMModelId} {#if !deleteLiteLLMModelId}
<option value="" disabled selected>Select a model</option> <option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if} {/if}
{#each liteLLMModelInfo as model} {#each liteLLMModelInfo as model}
<option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700" <option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"
@ -912,88 +1033,6 @@
{/if} {/if}
</div> </div>
</div> </div>
<!-- <div class="mt-2 space-y-3 pr-1.5">
<div>
<div class=" mb-2.5 text-sm font-medium">Add LiteLLM Model</div>
<div class="flex w-full mb-2">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter LiteLLM Model (e.g. ollama/mistral)"
bind:value={liteLLMModel}
autocomplete="off"
/>
</div>
</div>
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">Advanced Model Params</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showLiteLLMParams = !showLiteLLMParams;
}}>{showLiteLLMParams ? 'Hide' : 'Show'}</button
>
</div>
{#if showLiteLLMParams}
<div>
<div class=" mb-2.5 text-sm font-medium">LiteLLM API Key</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter LiteLLM API Key (e.g. os.environ/AZURE_API_KEY_CA)"
bind:value={liteLLMAPIKey}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">LiteLLM API Base URL</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter LiteLLM API Base URL"
bind:value={liteLLMAPIBase}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">LiteLLM API RPM</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter LiteLLM API RPM"
bind:value={liteLLMRPM}
autocomplete="off"
/>
</div>
</div>
</div>
{/if}
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Not sure what to add?
<a
class=" text-gray-300 font-medium underline"
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
target="_blank"
>
Click here for help.
</a>
</div>
</div>
</div> -->
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { models, settings, user } from '$lib/stores'; import { models, settings, user } from '$lib/stores';
@ -17,6 +18,8 @@
import Connections from './Settings/Connections.svelte'; import Connections from './Settings/Connections.svelte';
import Images from './Settings/Images.svelte'; import Images from './Settings/Images.svelte';
const i18n = getContext('i18n');
export let show = false; export let show = false;
const saveSettings = async (updated) => { const saveSettings = async (updated) => {
@ -58,7 +61,7 @@
<Modal bind:show> <Modal bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Settings</div> <div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
@ -106,7 +109,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">General</div> <div class=" self-center">{$i18n.t('General')}</div>
</button> </button>
{#if $user?.role === 'admin'} {#if $user?.role === 'admin'}
@ -131,7 +134,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Connections</div> <div class=" self-center">{$i18n.t('Connections')}</div>
</button> </button>
<button <button
@ -157,7 +160,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Models</div> <div class=" self-center">{$i18n.t('Models')}</div>
</button> </button>
{/if} {/if}
@ -184,7 +187,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Interface</div> <div class=" self-center">{$i18n.t('Interface')}</div>
</button> </button>
<button <button
@ -211,7 +214,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Audio</div> <div class=" self-center">{$i18n.t('Audio')}</div>
</button> </button>
{#if $user.role === 'admin'} {#if $user.role === 'admin'}
@ -238,7 +241,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Images</div> <div class=" self-center">{$i18n.t('Images')}</div>
</button> </button>
{/if} {/if}
@ -265,7 +268,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Chats</div> <div class=" self-center">{$i18n.t('Chats')}</div>
</button> </button>
<button <button
@ -291,7 +294,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Account</div> <div class=" self-center">{$i18n.t('Account')}</div>
</button> </button>
<button <button
@ -317,16 +320,16 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">About</div> <div class=" self-center">{$i18n.t('About')}</div>
</button> </button>
</div> </div>
<div class="flex-1 md:min-h-[380px]"> <div class="flex-1 md:min-h-[25rem]">
{#if selectedTab === 'general'} {#if selectedTab === 'general'}
<General <General
{getModels} {getModels}
{saveSettings} {saveSettings}
on:save={() => { on:save={() => {
show = false; toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'models'} {:else if selectedTab === 'models'}
@ -335,28 +338,28 @@
<Connections <Connections
{getModels} {getModels}
on:save={() => { on:save={() => {
show = false; toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'interface'} {:else if selectedTab === 'interface'}
<Interface <Interface
{saveSettings} {saveSettings}
on:save={() => { on:save={() => {
show = false; toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'audio'} {:else if selectedTab === 'audio'}
<Audio <Audio
{saveSettings} {saveSettings}
on:save={() => { on:save={() => {
show = false; toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'images'} {:else if selectedTab === 'images'}
<Images <Images
{saveSettings} {saveSettings}
on:save={() => { on:save={() => {
show = false; toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'chats'} {:else if selectedTab === 'chats'}
@ -364,7 +367,7 @@
{:else if selectedTab === 'account'} {:else if selectedTab === 'account'}
<Account <Account
saveHandler={() => { saveHandler={() => {
show = false; toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'about'} {:else if selectedTab === 'about'}

View file

@ -1,6 +1,9 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
const i18n = getContext('i18n');
export let downloadChat: Function; export let downloadChat: Function;
export let shareChat: Function; export let shareChat: Function;
@ -17,11 +20,11 @@
show = false; show = false;
}} }}
> >
Share to OpenWebUI Community {$i18n.t('Share to OpenWebUI Community')}
</button> </button>
<div class="flex justify-center space-x-1 mt-1.5"> <div class="flex justify-center space-x-1 mt-1.5">
<div class=" self-center text-gray-400 text-xs font-medium">or</div> <div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
<button <button
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline" class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
@ -31,7 +34,7 @@
show = false; show = false;
}} }}
> >
Download as a File {$i18n.t('Download as a File')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,13 +1,16 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false; export let show = false;
</script> </script>
<Modal bind:show> <Modal bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Keyboard shortcuts</div> <div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
@ -32,7 +35,7 @@
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<div class="flex flex-col space-y-3 w-full self-start"> <div class="flex flex-col space-y-3 w-full self-start">
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm">Open new chat</div> <div class=" text-sm">{$i18n.t('Open new chat')}</div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
<div <div
@ -56,7 +59,7 @@
</div> </div>
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm">Focus chat input</div> <div class=" text-sm">{$i18n.t('Focus chat input')}</div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
<div <div
@ -74,7 +77,7 @@
</div> </div>
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm">Copy last code block</div> <div class=" text-sm">{$i18n.t('Copy last code block')}</div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
<div <div
@ -98,7 +101,7 @@
</div> </div>
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm">Copy last response</div> <div class=" text-sm">{$i18n.t('Copy last response')}</div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
<div <div
@ -124,7 +127,7 @@
<div class="flex flex-col space-y-3 w-full self-start"> <div class="flex flex-col space-y-3 w-full self-start">
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm">Toggle settings</div> <div class=" text-sm">{$i18n.t('Toggle settings')}</div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
<div <div
@ -141,7 +144,7 @@
</div> </div>
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm">Toggle sidebar</div> <div class=" text-sm">{$i18n.t('Toggle sidebar')}</div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
<div <div
@ -165,7 +168,7 @@
</div> </div>
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm">Delete chat</div> <div class=" text-sm">{$i18n.t('Delete chat')}</div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
<div <div
@ -188,7 +191,7 @@
</div> </div>
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm">Show shortcuts</div> <div class=" text-sm">{$i18n.t('Show shortcuts')}</div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
<div <div

View file

@ -0,0 +1,37 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>
<DropdownMenu.Root
onOpenChange={(state) => {
dispatch('change', state);
}}
>
<DropdownMenu.Trigger>
<slot />
</DropdownMenu.Trigger>
<slot name="content">
<DropdownMenu.Content
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700 z-50 bg-gray-850 text-white"
sideOffset={8}
side="bottom"
align="start"
>
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
<div class="flex items-center">Profile</div>
</DropdownMenu.Item>
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
<div class="flex items-center">Profile</div>
</DropdownMenu.Item>
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
<div class="flex items-center">Profile</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</slot>
</DropdownMenu.Root>

View file

@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher, getContext } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
let showTagInput = false; let showTagInput = false;
let tagName = ''; let tagName = '';
</script> </script>
@ -9,12 +11,6 @@
<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">
<input
bind:value={tagName}
class=" cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[4rem]"
placeholder="Add a tag"
/>
<button <button
type="button" type="button"
on:click={() => { on:click={() => {
@ -36,6 +32,11 @@
/> />
</svg> </svg>
</button> </button>
<input
bind:value={tagName}
class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[8rem]"
placeholder={$i18n.t('Add a tag')}
/>
</div> </div>
<!-- TODO: Tag Suggestions --> <!-- TODO: Tag Suggestions -->

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents'; import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
@ -13,6 +13,8 @@
import { transformFileName } from '$lib/utils'; import { transformFileName } from '$lib/utils';
import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants'; import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants';
const i18n = getContext('i18n');
export let show = false; export let show = false;
export let selectedDoc; export let selectedDoc;
let uploadDocInputElement: HTMLInputElement; let uploadDocInputElement: HTMLInputElement;
@ -71,7 +73,7 @@
inputFiles = null; inputFiles = null;
uploadDocInputElement.value = ''; uploadDocInputElement.value = '';
} else { } else {
toast.error(`File not found.`); toast.error($i18n.t(`File not found.`));
} }
show = false; show = false;
@ -96,7 +98,7 @@
<Modal size="sm" bind:show> <Modal size="sm" bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Add Docs</div> <div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
@ -145,14 +147,14 @@
{#if inputFiles} {#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected. {inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
{:else} {:else}
Click here to select documents. {$i18n.t('Click here to select documents.')}
{/if} {/if}
</button> </button>
</div> </div>
<div class=" flex flex-col space-y-1.5"> <div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1.5 text-xs text-gray-500">Tags</div> <div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} /> <Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
</div> </div>
@ -163,7 +165,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents'; import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
@ -10,6 +10,8 @@
import Tags from '../common/Tags.svelte'; import Tags from '../common/Tags.svelte';
import { addTagById } from '$lib/apis/chats'; import { addTagById } from '$lib/apis/chats';
const i18n = getContext('i18n');
export let show = false; export let show = false;
export let selectedDoc; export let selectedDoc;
@ -74,7 +76,7 @@
<Modal size="sm" bind:show> <Modal size="sm" bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Edit Doc</div> <div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
@ -105,7 +107,7 @@
> >
<div class=" flex flex-col space-y-1.5"> <div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Name Tag</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name Tag')}</div>
<div class="flex flex-1"> <div class="flex flex-1">
<div <div
@ -134,7 +136,7 @@
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Title</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Title')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
@ -148,7 +150,7 @@
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1.5 text-xs text-gray-500">Tags</div> <div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} /> <Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
</div> </div>
@ -159,7 +161,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -1,22 +1,25 @@
<script lang="ts"> <script lang="ts">
import { getDocs } from '$lib/apis/documents'; import { getDocs } from '$lib/apis/documents';
import { import {
getChunkParams, getRAGConfig,
updateRAGConfig,
getQuerySettings, getQuerySettings,
scanDocs, scanDocs,
updateChunkParams,
updateQuerySettings updateQuerySettings
} from '$lib/apis/rag'; } from '$lib/apis/rag';
import { documents } from '$lib/stores'; import { documents } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let loading = false; let loading = false;
let chunkSize = 0; let chunkSize = 0;
let chunkOverlap = 0; let chunkOverlap = 0;
let pdfExtractImages = true;
let querySettings = { let querySettings = {
template: '', template: '',
@ -30,21 +33,29 @@
if (res) { if (res) {
await documents.set(await getDocs(localStorage.token)); await documents.set(await getDocs(localStorage.token));
toast.success('Scan complete!'); toast.success($i18n.t('Scan complete!'));
} }
}; };
const submitHandler = async () => { const submitHandler = async () => {
const res = await updateChunkParams(localStorage.token, chunkSize, chunkOverlap); const res = await updateRAGConfig(localStorage.token, {
pdf_extract_images: pdfExtractImages,
chunk: {
chunk_overlap: chunkOverlap,
chunk_size: chunkSize
}
});
querySettings = await updateQuerySettings(localStorage.token, querySettings); querySettings = await updateQuerySettings(localStorage.token, querySettings);
}; };
onMount(async () => { onMount(async () => {
const res = await getChunkParams(localStorage.token); const res = await getRAGConfig(localStorage.token);
if (res) { if (res) {
chunkSize = res.chunk_size; pdfExtractImages = res.pdf_extract_images;
chunkOverlap = res.chunk_overlap;
chunkSize = res.chunk.chunk_size;
chunkOverlap = res.chunk.chunk_overlap;
} }
querySettings = await getQuerySettings(localStorage.token); querySettings = await getQuerySettings(localStorage.token);
@ -60,10 +71,12 @@
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div> <div>
<div class=" mb-2 text-sm font-medium">General Settings</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Scan for documents from '/data/docs'</div> <div class=" self-center text-xs font-medium">
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
</div>
<button <button
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded flex flex-row space-x-1 items-center {loading class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded flex flex-row space-x-1 items-center {loading
@ -76,7 +89,7 @@
type="button" type="button"
disabled={loading} disabled={loading}
> >
<div class="self-center font-medium">Scan</div> <div class="self-center font-medium">{$i18n.t('Scan')}</div>
<!-- <svg <!-- <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -125,17 +138,17 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div class=" "> <div class=" ">
<div class=" text-sm font-medium">Chunk Params</div> <div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
<div class=" flex"> <div class=" flex">
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">Chunk Size</div> <div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Size')}</div>
<div class="self-center p-3"> <div class="self-center p-3">
<input <input
class=" w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" class=" w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="number" type="number"
placeholder="Enter Chunk Size" placeholder={$i18n.t('Enter Chunk Size')}
bind:value={chunkSize} bind:value={chunkSize}
autocomplete="off" autocomplete="off"
min="0" min="0"
@ -144,13 +157,13 @@
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">Chunk Overlap</div> <div class=" self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Overlap')}</div>
<div class="self-center p-3"> <div class="self-center p-3">
<input <input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="number" type="number"
placeholder="Enter Chunk Overlap" placeholder={$i18n.t('Enter Chunk Overlap')}
bind:value={chunkOverlap} bind:value={chunkOverlap}
autocomplete="off" autocomplete="off"
min="0" min="0"
@ -159,17 +172,33 @@
</div> </div>
</div> </div>
<div class=" text-sm font-medium">Query Params</div> <div>
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
pdfExtractImages = !pdfExtractImages;
}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div>
</div>
</div>
<div>
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
<div class=" flex"> <div class=" flex">
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium flex-1">Top K</div> <div class="self-center text-xs font-medium flex-1">{$i18n.t('Top K')}</div>
<div class="self-center p-3"> <div class="self-center p-3">
<input <input
class=" w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" class=" w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="number" type="number"
placeholder="Enter Top K" placeholder={$i18n.t('Enter Top K')}
bind:value={querySettings.k} bind:value={querySettings.k}
autocomplete="off" autocomplete="off"
min="0" min="0"
@ -194,7 +223,7 @@
</div> </div>
<div> <div>
<div class=" mb-2.5 text-sm font-medium">RAG Template</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
<textarea <textarea
bind:value={querySettings.template} bind:value={querySettings.template}
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
@ -209,7 +238,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -1,7 +1,10 @@
<script> <script>
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import General from './Settings/General.svelte'; import General from './Settings/General.svelte';
const i18n = getContext('i18n');
export let show = false; export let show = false;
let selectedTab = 'general'; let selectedTab = 'general';
@ -10,7 +13,7 @@
<Modal bind:show> <Modal bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Document Settings</div> <div class=" text-lg font-medium self-center">{$i18n.t('Document Settings')}</div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
@ -58,7 +61,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">General</div> <div class=" self-center">{$i18n.t('General')}</div>
</button> </button>
</div> </div>
<div class="flex-1 md:min-h-[380px]"> <div class="flex-1 md:min-h-[380px]">

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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
@ -9,6 +10,8 @@
import TagInput from '../common/Tags/TagInput.svelte'; import TagInput from '../common/Tags/TagInput.svelte';
import Tags from '../common/Tags.svelte'; import Tags from '../common/Tags.svelte';
const i18n = getContext('i18n');
export let initNewChat: Function; export let initNewChat: Function;
export let title: string = $WEBUI_NAME; export let title: string = $WEBUI_NAME;
export let shareEnabled: boolean = false; export let shareEnabled: boolean = false;
@ -26,7 +29,7 @@
const chat = (await getChatById(localStorage.token, $chatId)).chat; const chat = (await getChatById(localStorage.token, $chatId)).chat;
console.log('share', chat); console.log('share', chat);
toast.success('Redirecting you to OpenWebUI Community'); toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com'; const url = 'https://openwebui.com';
// const url = 'http://localhost:5173'; // const url = 'http://localhost:5173';

View file

@ -7,7 +7,10 @@
import { goto, invalidateAll } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { user, chats, settings, showSettings, chatId, tags } from '$lib/stores'; import { user, chats, settings, showSettings, chatId, tags } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
import { import {
deleteChatById, deleteChatById,
getChatList, getChatList,
@ -20,6 +23,8 @@
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import Dropdown from '../common/Dropdown.svelte';
import ChatMenu from './Sidebar/ChatMenu.svelte';
let show = false; let show = false;
let navElement; let navElement;
@ -27,6 +32,8 @@
let title: string = 'UI'; let title: string = 'UI';
let search = ''; let search = '';
let selectedChatId = null;
let chatDeleteId = null; let chatDeleteId = null;
let chatTitleEditId = null; let chatTitleEditId = null;
let chatTitle = ''; let chatTitle = '';
@ -82,7 +89,10 @@
}); });
if (res) { if (res) {
if ($chatId === id) {
goto('/'); goto('/');
}
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
} }
}; };
@ -128,7 +138,7 @@
/> />
</div> </div>
<div class=" self-center font-medium text-sm">New Chat</div> <div class=" self-center font-medium text-sm">{$i18n.t('New Chat')}</div>
</div> </div>
<div class="self-center"> <div class="self-center">
@ -173,7 +183,7 @@
</div> </div>
<div class="flex self-center"> <div class="flex self-center">
<div class=" self-center font-medium text-sm">Modelfiles</div> <div class=" self-center font-medium text-sm">{$i18n.t('Modelfiles')}</div>
</div> </div>
</a> </a>
</div> </div>
@ -201,7 +211,7 @@
</div> </div>
<div class="flex self-center"> <div class="flex self-center">
<div class=" self-center font-medium text-sm">Prompts</div> <div class=" self-center font-medium text-sm">{$i18n.t('Prompts')}</div>
</div> </div>
</a> </a>
</div> </div>
@ -229,7 +239,7 @@
</div> </div>
<div class="flex self-center"> <div class="flex self-center">
<div class=" self-center font-medium text-sm">Documents</div> <div class=" self-center font-medium text-sm">{$i18n.t('Documents')}</div>
</div> </div>
</a> </a>
</div> </div>
@ -239,11 +249,13 @@
{#if !($settings.saveChatHistory ?? true)} {#if !($settings.saveChatHistory ?? true)}
<div class="absolute z-40 w-full h-full bg-black/90 flex justify-center"> <div class="absolute z-40 w-full h-full bg-black/90 flex justify-center">
<div class=" text-left px-5 py-2"> <div class=" text-left px-5 py-2">
<div class=" font-medium">Chat History is off for this browser.</div> <div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
<div class="text-xs mt-2"> <div class="text-xs mt-2">
When history is turned off, new chats on this browser won't appear in your history on {$i18n.t(
any of your devices. <span class=" font-semibold" "When history is turned off, new chats on this browser won't appear in your history on any of your devices."
>This setting does not sync across browsers or devices.</span )}
<span class=" font-semibold"
>{$i18n.t('This setting does not sync across browsers or devices.')}</span
> >
</div> </div>
@ -270,7 +282,7 @@
/> />
</svg> </svg>
<div>Enable Chat History</div> <div>{$i18n.t('Enable Chat History')}</div>
</button> </button>
</div> </div>
</div> </div>
@ -296,7 +308,7 @@
<input <input
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none" class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
placeholder="Search" placeholder={$i18n.t('Search')}
bind:value={search} bind:value={search}
on:focus={() => { on:focus={() => {
enrichChatsWithContent($chats); enrichChatsWithContent($chats);
@ -370,24 +382,27 @@
return title.includes(query) || contentMatches; return title.includes(query) || contentMatches;
} }
}) as chat, i} }) as chat, i}
<div class=" w-full pr-2 relative"> <div class=" w-full pr-2 relative group">
{#if chatTitleEditId === chat.id} {#if chatTitleEditId === chat.id}
<div <div
class=" w-full flex justify-between rounded-xl px-3 py-2 hover:bg-gray-900 {chat.id === class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId
$chatId
? 'bg-gray-900' ? 'bg-gray-900'
: ''} transition whitespace-nowrap text-ellipsis" : chat.id === selectedChatId
? 'bg-gray-950'
: 'group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
> >
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" /> <input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
</div> </div>
{:else} {:else}
<a <a
class=" w-full flex justify-between rounded-xl px-3 py-2 hover:bg-gray-900 {chat.id === class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId
$chatId
? 'bg-gray-900' ? 'bg-gray-900'
: ''} transition whitespace-nowrap text-ellipsis" : chat.id === selectedChatId
? 'bg-gray-950'
: 'group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
href="/c/{chat.id}" href="/c/{chat.id}"
on:click={() => { on:click={() => {
selectedChatId = chat.id;
if (window.innerWidth < 1024) { if (window.innerWidth < 1024) {
show = false; show = false;
} }
@ -395,21 +410,27 @@
draggable="false" draggable="false"
> >
<div class=" flex self-center flex-1 w-full"> <div class=" flex self-center flex-1 w-full">
<div <div class=" text-left self-center overflow-hidden w-full h-[20px]">
class=" text-left self-center overflow-hidden {chat.id === $chatId
? 'w-[160px]'
: 'w-full'} h-[20px]"
>
{chat.title} {chat.title}
</div> </div>
</div> </div>
</a> </a>
{/if} {/if}
{#if chat.id === $chatId} <div
<div class=" absolute right-[22px] top-[10px]"> class="
{chat.id === $chatId
? ' from-gray-900'
: chat.id === selectedChatId
? 'from-gray-950'
: 'invisible group-hover:visible from-gray-950'}
absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
to-transparent"
>
{#if chatTitleEditId === chat.id} {#if chatTitleEditId === chat.id}
<div class="flex self-center space-x-1.5"> <div class="flex self-center space-x-1.5 z-10">
<button <button
class=" self-center hover:text-white transition" class=" self-center hover:text-white transition"
on:click={() => { on:click={() => {
@ -451,7 +472,7 @@
</button> </button>
</div> </div>
{:else if chatDeleteId === chat.id} {:else if chatDeleteId === chat.id}
<div class="flex self-center space-x-1.5"> <div class="flex self-center space-x-1.5 z-10">
<button <button
class=" self-center hover:text-white transition" class=" self-center hover:text-white transition"
on:click={() => { on:click={() => {
@ -490,61 +511,41 @@
</button> </button>
</div> </div>
{:else} {:else}
<div class="flex self-center space-x-1.5"> <div class="flex self-center space-x-1.5 z-10">
<button <ChatMenu
id="delete-chat-button" renameHandler={() => {
class=" hidden"
on:click={() => {
deleteChat(chat.id);
}}
/>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatTitle = chat.title; chatTitle = chat.title;
chatTitleEditId = chat.id; chatTitleEditId = chat.id;
}} }}
deleteHandler={() => {
chatDeleteId = chat.id;
}}
onClose={() => {
selectedChatId = null;
}}
> >
<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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
<button <button
aria-label="Chat Menu"
class=" self-center hover:text-white transition" class=" self-center hover:text-white transition"
on:click={() => { on:click={() => {
chatDeleteId = chat.id; selectedChatId = chat.id;
}} }}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 16 16"
viewBox="0 0 24 24" fill="currentColor"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
stroke-linecap="round" d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/> />
</svg> </svg>
</button> </button>
</ChatMenu>
</div> </div>
{/if} {/if}
</div> </div>
{/if}
</div> </div>
{/each} {/each}
</div> </div>
@ -602,7 +603,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center font-medium">Admin Panel</div> <div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
</button> </button>
<button <button
@ -628,7 +629,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center font-medium">Playground</div> <div class=" self-center font-medium">{$i18n.t('Playground')}</div>
</button> </button>
{/if} {/if}
@ -660,7 +661,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center font-medium">Settings</div> <div class=" self-center font-medium">{$i18n.t('Settings')}</div>
</button> </button>
</div> </div>
@ -694,7 +695,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center font-medium">Sign Out</div> <div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
</button> </button>
</div> </div>
</div> </div>
@ -707,7 +708,11 @@
<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] z-40 -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
> >
<Tooltip placement="right" content={`${show ? 'Close' : 'Open'} sidebar`} touch={false}> <Tooltip
placement="right"
content={`${show ? $i18n.t('Close') : $i18n.t('Open')} ${$i18n.t('sidebar')}`}
touch={false}
>
<button <button
id="sidebar-toggle-button" id="sidebar-toggle-button"
class=" group" class=" group"

View file

@ -0,0 +1,54 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
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';
export let renameHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
</script>
<Dropdown
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
>
<Tooltip content="More">
<slot />
</Tooltip>
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700/50 z-50 bg-gray-850 text-white"
sideOffset={8}
side="bottom"
align="start"
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
renameHandler();
}}
>
<Pencil strokeWidth="2" />
<div class="flex items-center">Rename</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
deleteHandler();
}}
>
<GarbageBin strokeWidth="2" />
<div class="flex items-center">Delete</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</div>
</Dropdown>

View file

@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let messages = []; export let messages = [];
let textAreaElement: HTMLTextAreaElement; let textAreaElement: HTMLTextAreaElement;
@ -19,16 +21,20 @@
class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left dark:group-hover:bg-gray-800 rounded-lg transition" class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left dark:group-hover:bg-gray-800 rounded-lg transition"
on:click={() => { on:click={() => {
message.role = message.role === 'user' ? 'assistant' : 'user'; message.role = message.role === 'user' ? 'assistant' : 'user';
}}>{message.role}</button }}>{$i18n.t(message.role)}</button
> >
</div> </div>
<div class="flex-1"> <div class="flex-1">
<!-- $i18n.t('a user') -->
<!-- $i18n.t('an assistant') -->
<textarea <textarea
id="{message.role}-{idx}-textarea" id="{message.role}-{idx}-textarea"
bind:this={textAreaElement} bind:this={textAreaElement}
class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden" class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
placeholder="Enter {message.role === 'user' ? 'a user' : 'an assistant'} message here" placeholder={$i18n.t(`Enter {{role}} message here`, {
role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
})}
rows="1" rows="1"
on:input={(e) => { on:input={(e) => {
textAreaElement.style.height = ''; textAreaElement.style.height = '';
@ -100,6 +106,6 @@
</svg> </svg>
</div> </div>
<div class=" text-sm font-medium">Add message</div> <div class=" text-sm font-medium">{$i18n.t('Add message')}</div>
</button> </button>
</div> </div>

73
src/lib/i18n/index.ts Normal file
View file

@ -0,0 +1,73 @@
import i18next from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import type { i18n as i18nType } from 'i18next';
import { writable } from 'svelte/store';
const createI18nStore = (i18n: i18nType) => {
const i18nWritable = writable(i18n);
i18n.on('initialized', () => {
i18nWritable.set(i18n);
});
i18n.on('loaded', () => {
i18nWritable.set(i18n);
});
i18n.on('added', () => i18nWritable.set(i18n));
i18n.on('languageChanged', () => {
i18nWritable.set(i18n);
});
return i18nWritable;
};
const createIsLoadingStore = (i18n: i18nType) => {
const isLoading = writable(false);
// if loaded resources are empty || {}, set loading to true
i18n.on('loaded', (resources) => {
// console.log('loaded:', resources);
Object.keys(resources).length !== 0 && isLoading.set(false);
});
// if resources failed loading, set loading to true
i18n.on('failedLoading', () => {
isLoading.set(true);
});
return isLoading;
};
i18next
.use(
resourcesToBackend(
(language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`)
)
)
.use(LanguageDetector)
.init({
debug: false,
detection: {
order: ['querystring', 'localStorage', 'navigator'],
caches: ['localStorage'],
lookupQuerystring: 'lang',
lookupLocalStorage: 'locale'
},
fallbackLng: {
default: ['en-US']
},
ns: 'translation',
returnEmptyString: false,
interpolation: {
escapeValue: false // not needed for svelte as it escapes by default
}
});
const i18n = createI18nStore(i18next);
const isLoadingStore = createIsLoadingStore(i18next);
export const getLanguages = async () => {
const languages = (await import(`./locales/languages.json`)).default;
return languages;
};
export default i18n;
export const isLoading = isLoadingStore;

View file

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' oder '-1' für kein Ablaufdatum.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(z.B. `sh webui.sh --api`)",
"(latest)": "(neueste)",
"{{modelName}} is thinking...": "{{modelName}} denkt nach...",
"{{webUIName}} Backend Required": "{{webUIName}}-Backend erforderlich",
"a user": "",
"About": "Über",
"Account": "Account",
"Action": "Aktion",
"Add a model": "Füge ein Modell hinzu",
"Add a model tag name": "Benenne dein Modell-Tag",
"Add a short description about what this modelfile does": "Füge eine kurze Beschreibung hinzu, was dieses Modelfile kann",
"Add a short title for this prompt": "Füge einen kurzen Titel für diesen Prompt hinzu",
"Add a tag": "Tag hinzugügen",
"Add Docs": "Dokumente hinzufügen",
"Add Files": "Dateien hinzufügen",
"Add message": "Nachricht eingeben",
"add tags": "Tags hinzufügen",
"Adjusting these settings will apply changes universally to all users.": "Das Anpassen dieser Einstellungen wirkt sich universell auf alle Benutzer aus.",
"admin": "Administrator",
"Admin Panel": "Admin Panel",
"Admin Settings": "Admin Einstellungen",
"Advanced Parameters": "Erweiterte Parameter",
"all": "Alle",
"All Users": "Alle Benutzer",
"Allow": "Erlauben",
"Allow Chat Deletion": "Chat Löschung erlauben",
"alphanumeric characters and hyphens": "alphanumerische Zeichen und Bindestriche",
"Already have an account?": "Hast du vielleicht schon ein Account?",
"an assistant": "",
"and": "und",
"API Base URL": "API Basis URL",
"API Key": "API Key",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "sind erlaubt - Aktiviere diesen Befehl, indem du",
"Are you sure?": "",
"Audio": "Audio",
"Auto-playback response": "Automatische Wiedergabe der Antwort",
"Auto-send input after 3 sec.": "Automatisches Senden der Eingabe nach 3 Sek",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Basis URL",
"AUTOMATIC1111 Base URL is required.": "",
"available!": "verfügbar!",
"Back": "Zurück",
"Builder Mode": "Builder Modus",
"Cancel": "Abbrechen",
"Categories": "Kategorien",
"Change Password": "Passwort ändern",
"Chat": "Chat",
"Chat History": "Chat Verlauf",
"Chat History is off for this browser.": "Chat Verlauf ist für diesen Browser ausgeschaltet.",
"Chats": "Chats",
"Check Again": "Erneut überprüfen",
"Check for updates": "Nach Updates suchen",
"Checking for updates...": "Nach Updates suchen...",
"Choose a model before saving...": "Wähle bitte zuerst ein Modell, bevor du speicherst...",
"Chunk Overlap": "Chunk Overlap",
"Chunk Params": "Chunk Parameter",
"Chunk Size": "Chunk Size",
"Click here for help.": "Klicke hier für Hilfe.",
"Click here to check other modelfiles.": "Klicke hier, um andere Modelfiles zu überprüfen.",
"Click here to select": "",
"Click here to select documents.": "",
"click here.": "hier klicken.",
"Click on the user role button to change a user's role.": "Klicke auf die Benutzerrollenschaltfläche, um die Rolle eines Benutzers zu ändern.",
"Close": "Schließe",
"Collection": "Kollektion",
"Command": "Befehl",
"Confirm Password": "Passwort bestätigen",
"Connections": "Verbindungen",
"Content": "Inhalt",
"Context Length": "Context Length",
"Conversation Mode": "Konversationsmodus",
"Copy last code block": "Letzten Codeblock kopieren",
"Copy last response": "Letzte Antwort kopieren",
"Copying to clipboard was successful!": "Das Kopieren in die Zwischenablage war erfolgreich!",
"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':": "Erstelle einen prägnanten Satz mit 3-5 Wörtern als Überschrift für die folgende Abfrage. Halte dich dabei strikt an die 3-5-Wort-Grenze und vermeide die Verwendung des Wortes Titel:",
"Create a modelfile": "Modelfiles erstellen",
"Create Account": "Konto erstellen",
"Created at": "Erstellt am",
"Created by": "Erstellt von",
"Current Model": "Aktuelles Modell",
"Current Password": "Aktuelles Passwort",
"Custom": "Benutzerdefiniert",
"Customize Ollama models for a specific purpose": "Ollama-Modelle für einen bestimmten Zweck anpassen",
"Dark": "Dunkel",
"Database": "Datenbank",
"DD/MM/YYYY HH:mm": "DD.MM.YYYY HH:mm",
"Default": "Standard",
"Default (Automatic1111)": "",
"Default (Web API)": "Standard (Web-API)",
"Default model updated": "Standardmodell aktualisiert",
"Default Prompt Suggestions": "Standard-Prompt-Vorschläge",
"Default User Role": "Standardbenutzerrolle",
"delete": "löschen",
"Delete a model": "Ein Modell löschen",
"Delete chat": "Chat löschen",
"Delete Chats": "Chats löschen",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht",
"Deleted {tagName}": "{tagName} gelöscht",
"Description": "Beschreibung",
"Desktop Notifications": "Desktop-Benachrichtigungen",
"Disabled": "Deaktiviert",
"Discover a modelfile": "Eine Modelfiles entdecken",
"Discover a prompt": "Einen Prompt entdecken",
"Discover, download, and explore custom prompts": "Benutzerdefinierte Prompts entdecken, herunterladen und erkunden",
"Discover, download, and explore model presets": "Modellvorgaben entdecken, herunterladen und erkunden",
"Display the username instead of You in the Chat": "Den Benutzernamen anstelle von 'Du' im Chat anzeigen",
"Document": "Dokument",
"Document Settings": "Dokumenteinstellungen",
"Documents": "Dokumente",
"does not make any external connections, and your data stays securely on your locally hosted server.": "stellt keine externen Verbindungen her, und Deine Daten bleiben sicher auf Deinen lokal gehosteten Server.",
"Don't Allow": "Nicht erlauben",
"Don't have an account?": "Hast du vielleicht noch kein Account?",
"Download as a File": "Als Datei herunterladen",
"Download Database": "Datenbank herunterladen",
"Drop any files here to add to the conversation": "Lasse Dateien hier fallen, um sie dem Chat anzuhängen",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "z.B. '30s','10m'. Gültige Zeiteinheiten sind 's', 'm', 'h'.",
"Edit Doc": "Dokument bearbeiten",
"Edit User": "Benutzer bearbeiten",
"Email": "E-Mail",
"Enable Chat History": "Chat-Verlauf aktivieren",
"Enable New Sign Ups": "Neue Anmeldungen aktivieren",
"Enabled": "Aktiviert",
"Enter {{role}} message here": "",
"Enter API Key": "",
"Enter Chunk Overlap": "",
"Enter Chunk Size": "",
"Enter Image Size (e.g. 512x512)": "",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "",
"Enter LiteLLM API Key (litellm_params.api_key)": "",
"Enter LiteLLM API RPM (litellm_params.rpm)": "",
"Enter LiteLLM Model (litellm_params.model)": "",
"Enter Max Tokens (litellm_params.max_tokens)": "",
"Enter model tag (e.g. {{modelTag}})": "",
"Enter Number of Steps (e.g. 50)": "",
"Enter stop sequence": "Stop-Sequenz eingeben",
"Enter Top K": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "",
"Enter Your Email": "Geben Deine E-Mail-Adresse ein",
"Enter Your Full Name": "Gebe Deinen vollständigen Namen ein",
"Enter Your Password": "Gebe Dein Passwort ein",
"Experimental": "Experimentell",
"Export All Chats (All Users)": "Alle Chats exportieren (alle Benutzer)",
"Export Chats": "Chats exportieren",
"Export Documents Mapping": "Dokumentenmapping exportieren",
"Export Modelfiles": "Modelfiles exportieren",
"Export Prompts": "Prompts exportieren",
"Failed to read clipboard contents": "Fehler beim Lesen des Zwischenablageninhalts",
"File Mode": "File Mode",
"File not found.": "Datei nicht gefunden.",
"Focus chat input": "Chat-Eingabe fokussieren",
"Format your variables using square brackets like this:": "Formatiere Deine Variablen mit eckigen Klammern wie folgt:",
"From (Base Model)": "Von (Basismodell)",
"Full Screen Mode": "Vollbildmodus",
"General": "Allgemein",
"General Settings": "Allgemeine Einstellungen",
"Hello, {{name}}": "Hallo, {{name}}",
"Hide": "Verbergen",
"Hide Additional Params": "Hide Additional Params",
"How can I help you today?": "Wie kann ich Dir heute helfen?",
"Image Generation (Experimental)": "Bildgenerierung (experimentell)",
"Image Generation Engine": "",
"Image Settings": "Bildeinstellungen",
"Images": "Bilder",
"Import Chats": "Chats importieren",
"Import Documents Mapping": "Dokumentenmapping importieren",
"Import Modelfiles": "Modelfiles importieren",
"Import Prompts": "Prompts importieren",
"Include `--api` flag when running stable-diffusion-webui": "Füge das `--api`-Flag hinzu, wenn Du stable-diffusion-webui nutzt",
"Interface": "Benutzeroberfläche",
"join our Discord for help.": "Trete unserem Discord bei, um Hilfe zu erhalten.",
"JSON": "JSON",
"JWT Expiration": "JWT-Ablauf",
"JWT Token": "JWT-Token",
"Keep Alive": "Keep Alive",
"Keyboard shortcuts": "Tastenkürzel",
"Language": "Sprache",
"Light": "Hell",
"Listening...": "Hören...",
"LLMs can make mistakes. Verify important information.": "LLMs können Fehler machen. Überprüfe wichtige Informationen.",
"Made by OpenWebUI Community": "Von der OpenWebUI-Community",
"Make sure to enclose them with": "Formatiere Deine Variablen mit:",
"Manage LiteLLM Models": "LiteLLM-Modelle verwalten",
"Manage Models": "Modelle verwalten",
"Manage Ollama Models": "Ollama-Modelle verwalten",
"Max Tokens": "Maximale Tokens",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Es können maximal 3 Modelle gleichzeitig heruntergeladen werden. Bitte versuche es später erneut.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "DD.MM.YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "Modell '{{modelName}}' wurde erfolgreich heruntergeladen.",
"Model '{{modelTag}}' is already in queue for downloading.": "Modell '{{modelTag}}' befindet sich bereits in der Warteschlange zum Herunterladen.",
"Model {{modelId}} not found": "Modell {{modelId}} nicht gefunden",
"Model {{modelName}} already exists.": "Modell {{modelName}} existiert bereits.",
"Model Name": "Modellname",
"Model not selected": "Modell nicht ausgewählt",
"Model Tag Name": "Modell-Tag-Name",
"Model Whitelisting": "Modell-Whitelisting",
"Model(s) Whitelisted": "Modell(e) auf der Whitelist",
"Modelfile": "Modelfiles",
"Modelfile Advanced Settings": "Erweiterte Modelfileseinstellungen",
"Modelfile Content": "Modelfile Content",
"Modelfiles": "Modelfiles",
"Models": "Modelle",
"My Documents": "Meine Dokumente",
"My Modelfiles": "Meine Modelfiles",
"My Prompts": "Meine Prompts",
"Name": "Name",
"Name Tag": "Namens-Tag",
"Name your modelfile": "Name your modelfile",
"New Chat": "Neuer Chat",
"New Password": "Neues Passwort",
"Not sure what to add?": "Nicht sicher, was hinzugefügt werden soll?",
"Not sure what to write? Switch to": "Nicht sicher, was Du schreiben sollst? Wechsel zu",
"Off": "Aus",
"Okay, Let's Go!": "Okay, los geht's!",
"Ollama Base URL": "",
"Ollama Version": "Ollama-Version",
"On": "Ein",
"Only": "Nur",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Nur alphanumerische Zeichen und Bindestriche sind im Befehlsstring erlaubt.",
"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.": "Hoppla! Warte noch einen Moment! Die Dateien sind noch im der Verarbeitung. Bitte habe etwas Geduld und wir informieren Dich, sobald sie bereit sind.",
"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.",
"Open": "Öffne",
"Open AI": "Open AI",
"Open AI (Dall-E)": "",
"Open new chat": "Neuen Chat öffnen",
"OpenAI API": "OpenAI-API",
"OpenAI API Key": "",
"OpenAI API Key is required.": "",
"or": "oder",
"Parameters": "Parameter",
"Password": "Passwort",
"PDF Extract Images (OCR)": "Text von Bilder aus PDFs extrahieren (OCR)",
"pending": "ausstehend",
"Permission denied when accessing microphone: {{error}}": "Zugriff auf das Mikrofon verweigert: {{error}}",
"Playground": "Playground",
"Profile": "Profil",
"Prompt Content": "Prompt-Inhalt",
"Prompt suggestions": "Prompt-Vorschläge",
"Prompts": "Prompts",
"Pull a model from Ollama.com": "Ein Modell von Ollama.com abrufen",
"Pull Progress": "Fortschritt abrufen",
"Query Params": "Query Parameter",
"RAG Template": "RAG-Vorlage",
"Raw Format": "Rohformat",
"Record voice": "Stimme aufnehmen",
"Redirecting you to OpenWebUI Community": "Du wirst zur OpenWebUI-Community weitergeleitet",
"Release Notes": "Versionshinweise",
"Repeat Last N": "Repeat Last N",
"Repeat Penalty": "Repeat Penalty",
"Request Mode": "Request-Modus",
"Reset Vector Storage": "Vektorspeicher zurücksetzen",
"Response AutoCopy to Clipboard": "Antwort automatisch in die Zwischenablage kopieren",
"Role": "Rolle",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Speichern",
"Save & Create": "Speichern und erstellen",
"Save & Submit": "Speichern und senden",
"Save & Update": "Speichern und aktualisieren",
"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": "Das direkte Speichern von Chat-Protokollen im Browser-Speicher wird nicht mehr unterstützt. Bitte nehme Dir einen Moment Zeit, um Deine Chat-Protokolle herunterzuladen und zu löschen, indem Du auf die Schaltfläche unten klickst. Keine Sorge, Du kannst Deine Chat-Protokolle problemlos über das Backend wieder importieren.",
"Scan": "Scannen",
"Scan complete!": "Scan abgeschlossen!",
"Scan for documents from {{path}}": "Dokumente von {{path}} scannen",
"Search": "Suchen",
"Search Documents": "Dokumente suchen",
"Search Prompts": "Prompts suchen",
"See readme.md for instructions": "Anleitung in readme.md anzeigen",
"See what's new": "Was gibt's Neues",
"Seed": "Seed",
"Select a mode": "",
"Select a model": "Ein Modell auswählen",
"Select an Ollama instance": "",
"Send a Messsage": "Eine Nachricht senden",
"Send message": "Nachricht senden",
"Server connection verified": "Serververbindung überprüft",
"Set as default": "Als Standard festlegen",
"Set Default Model": "Standardmodell festlegen",
"Set Image Size": "Bildgröße festlegen",
"Set Steps": "Schritte festlegen",
"Set Title Auto-Generation Model": "Modell für automatische Titelgenerierung festlegen",
"Set Voice": "Stimme festlegen",
"Settings": "Einstellungen",
"Settings saved successfully!": "Einstellungen erfolgreich gespeichert!",
"Share to OpenWebUI Community": "Mit OpenWebUI Community teilen",
"short-summary": "kurze-zusammenfassung",
"Show": "Anzeigen",
"Show Additional Params": "Show Additional Params",
"Show shortcuts": "Verknüpfungen anzeigen",
"sidebar": "Seitenleiste",
"Sign in": "Anmelden",
"Sign Out": "Abmelden",
"Sign up": "Registrieren",
"Speech recognition error: {{error}}": "Spracherkennungsfehler: {{error}}",
"Speech-to-Text Engine": "Sprache-zu-Text-Engine",
"SpeechRecognition API is not supported in this browser.": "Die SpeechRecognition-API wird in diesem Browser nicht unterstützt.",
"Stop Sequence": "Stop Sequence",
"STT Settings": "STT-Einstellungen",
"Submit": "Senden",
"Success": "Erfolg",
"Successfully updated.": "Erfolgreich aktualisiert.",
"Sync All": "Alles synchronisieren",
"System": "System",
"System Prompt": "System-Prompt",
"Tags": "Tags",
"Temperature": "Temperatur",
"Template": "Vorlage",
"Text Completion": "Textvervollständigung",
"Text-to-Speech Engine": "Text-zu-Sprache-Engine",
"Tfs Z": "Tfs Z",
"Theme": "Design",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Dadurch werden Deine wertvollen Unterhaltungen sicher in der Backend-Datenbank gespeichert. Vielen Dank!",
"This setting does not sync across browsers or devices.": "Diese Einstellung wird nicht zwischen Browsern oder Geräten synchronisiert.",
"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": "Titel",
"Title Auto-Generation": "Automatische Titelgenerierung",
"Title Generation Prompt": "Prompt für Titelgenerierung",
"to": "für",
"To access the available model names for downloading,": "Um auf die verfügbaren Modellnamen zum Herunterladen zuzugreifen,",
"To access the GGUF models available for downloading,": "To access the GGUF models available for downloading,",
"to chat input.": "to chat input.",
"Toggle settings": "Einstellungen umschalten",
"Toggle sidebar": "Seitenleiste umschalten",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Probleme beim Zugriff auf Ollama?",
"TTS Settings": "TTS-Einstellungen",
"Type Hugging Face Resolve (Download) URL": "",
"Uh-oh! There was an issue connecting to {{provider}}.": "Ups! Es gab ein Problem bei der Verbindung mit {{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",
"Update password": "Passwort aktualisieren",
"Upload a GGUF model": "Upload a GGUF model",
"Upload files": "Dateien hochladen",
"Upload Progress": "Upload Progress",
"URL Mode": "URL Mode",
"Use '#' in the prompt input to load and select your documents.": "Verwende '#' in der Prompt-Eingabe, um Deine Dokumente zu laden und auszuwählen.",
"Use Gravatar": "",
"user": "Benutzer",
"User Permissions": "Benutzerberechtigungen",
"Users": "Benutzer",
"Utilize": "Nutze die",
"Valid time units:": "Gültige Zeiteinheiten:",
"variable": "Variable",
"variable to have them replaced with clipboard content.": "Variable, um den Inhalt der Zwischenablage beim Nutzen des Prompts zu ersetzen.",
"Version": "Version",
"Web": "Web",
"WebUI Add-ons": "WebUI-Add-Ons",
"WebUI Settings": "WebUI-Einstellungen",
"WebUI will make requests to": "Wenn aktiviert sendet WebUI externe Anfragen an",
"Whats New in": "Was gibt's Neues in",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Wenn die Historie ausgeschaltet ist, werden neue Chats nicht in Deiner Historie auf Deine Geräte angezeigt.",
"Whisper (Local)": "Whisper (Lokal)",
"Write a prompt suggestion (e.g. Who are you?)": "Gebe einen Prompt-Vorschlag ein (z.B. Wer bist du?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "Schreibe eine kurze Zusammenfassung in 50 Wörtern, die [Thema oder Schlüsselwort] zusammenfasst.",
"You": "Du",
"You're a helpful assistant.": "Du bist ein hilfreicher Assistent.",
"You're now logged in.": "Du bist nun eingeloggt."
}

View file

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)",
"(latest)": "(latest)",
"{{modelName}} is thinking...": "{{modelName}} is thinking...",
"{{webUIName}} Backend Required": "{{webUIName}} Backend Required",
"a user": "",
"About": "About",
"Account": "Account",
"Action": "Action",
"Add a model": "Add a model",
"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 title for this prompt": "Add a short title for this prompt",
"Add a tag": "Add a tag",
"Add Docs": "Add Docs",
"Add Files": "Add Files",
"Add message": "Add message",
"add tags": "add tags",
"Adjusting these settings will apply changes universally to all users.": "Adjusting these settings will apply changes universally to all users.",
"admin": "admin",
"Admin Panel": "Admin Panel",
"Admin Settings": "Admin Settings",
"Advanced Parameters": "Advanced Parameters",
"all": "all",
"All Users": "All Users",
"Allow": "Allow",
"Allow Chat Deletion": "Allow Chat Deletion",
"alphanumeric characters and hyphens": "alphanumeric characters and hyphens",
"Already have an account?": "Already have an account?",
"an assistant": "",
"and": "and",
"API Base URL": "API Base URL",
"API Key": "API Key",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "are allowed - Activate this command by typing",
"Are you sure?": "",
"Audio": "Audio",
"Auto-playback response": "Auto-playback response",
"Auto-send input after 3 sec.": "Auto-send input after 3 sec.",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL",
"AUTOMATIC1111 Base URL is required.": "",
"available!": "available!",
"Back": "Back",
"Builder Mode": "Builder Mode",
"Cancel": "Cancel",
"Categories": "Categories",
"Change Password": "Change Password",
"Chat": "Chat",
"Chat History": "Chat History",
"Chat History is off for this browser.": "Chat History is off for this browser.",
"Chats": "Chats",
"Check Again": "Check Again",
"Check for updates": "Check for updates",
"Checking for updates...": "Checking for updates...",
"Choose a model before saving...": "Choose a model before saving...",
"Chunk Overlap": "Chunk Overlap",
"Chunk Params": "Chunk Params",
"Chunk Size": "Chunk Size",
"Click here for help.": "Click here for help.",
"Click here to check other modelfiles.": "Click here to check other modelfiles.",
"Click here to select": "",
"Click here to select documents.": "",
"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.",
"Close": "Close",
"Collection": "Collection",
"Command": "Command",
"Confirm Password": "Confirm Password",
"Connections": "Connections",
"Content": "Content",
"Context Length": "Context Length",
"Conversation Mode": "Conversation Mode",
"Copy last code block": "Copy last code block",
"Copy last response": "Copy last response",
"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 modelfile": "Create a modelfile",
"Create Account": "Create Account",
"Created at": "Created at",
"Created by": "Created by",
"Current Model": "Current Model",
"Current Password": "Current Password",
"Custom": "Custom",
"Customize Ollama models for a specific purpose": "Customize Ollama models for a specific purpose",
"Dark": "Dark",
"Database": "Database",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "Default",
"Default (Automatic1111)": "",
"Default (Web API)": "Default (Web API)",
"Default model updated": "Default model updated",
"Default Prompt Suggestions": "Default Prompt Suggestions",
"Default User Role": "Default User Role",
"delete": "delete",
"Delete a model": "Delete a model",
"Delete chat": "Delete chat",
"Delete Chats": "Delete Chats",
"Deleted {{deleteModelTag}}": "Deleted {{deleteModelTag}}",
"Deleted {tagName}": "Deleted {tagName}",
"Description": "Description",
"Desktop Notifications": "Notification",
"Disabled": "Disabled",
"Discover a modelfile": "Discover a modelfile",
"Discover a prompt": "Discover a prompt",
"Discover, download, and explore custom prompts": "Discover, download, and explore custom prompts",
"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",
"Document": "Document",
"Document Settings": "Document Settings",
"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.",
"Don't Allow": "Don't Allow",
"Don't have an account?": "Don't have an account?",
"Download as a File": "Download as a File",
"Download Database": "Download Database",
"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'.",
"Edit Doc": "Edit Doc",
"Edit User": "Edit User",
"Email": "Email",
"Enable Chat History": "Enable Chat History",
"Enable New Sign Ups": "Enable New Sign Ups",
"Enabled": "Enabled",
"Enter {{role}} message here": "",
"Enter API Key": "",
"Enter Chunk Overlap": "",
"Enter Chunk Size": "",
"Enter Image Size (e.g. 512x512)": "",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "",
"Enter LiteLLM API Key (litellm_params.api_key)": "",
"Enter LiteLLM API RPM (litellm_params.rpm)": "",
"Enter LiteLLM Model (litellm_params.model)": "",
"Enter Max Tokens (litellm_params.max_tokens)": "",
"Enter model tag (e.g. {{modelTag}})": "",
"Enter Number of Steps (e.g. 50)": "",
"Enter stop sequence": "Enter stop sequence",
"Enter Top K": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "",
"Enter Your Email": "Enter Your Email",
"Enter Your Full Name": "Enter Your Full Name",
"Enter Your Password": "Enter Your Password",
"Experimental": "Experimental",
"Export All Chats (All Users)": "Export All Chats (All Users)",
"Export Chats": "Export Chats",
"Export Documents Mapping": "Export Documents Mapping",
"Export Modelfiles": "Export Modelfiles",
"Export Prompts": "Export Prompts",
"Failed to read clipboard contents": "Failed to read clipboard contents",
"File Mode": "File Mode",
"File not found.": "File not found.",
"Focus chat input": "Focus chat input",
"Format your variables using square brackets like this:": "Format your variables using square brackets like this:",
"From (Base Model)": "From (Base Model)",
"Full Screen Mode": "Full Screen Mode",
"General": "General",
"General Settings": "General Settings",
"Hello, {{name}}": "Hello, {{name}}",
"Hide": "Hide",
"Hide Additional Params": "Hide Additional Params",
"How can I help you today?": "How can I help you today?",
"Image Generation (Experimental)": "Image Generation (Experimental)",
"Image Generation Engine": "",
"Image Settings": "Image Settings",
"Images": "Images",
"Import Chats": "Import Chats",
"Import Documents Mapping": "Import Documents Mapping",
"Import Modelfiles": "Import Modelfiles",
"Import Prompts": "Import Prompts",
"Include `--api` flag when running stable-diffusion-webui": "Include `--api` flag when running stable-diffusion-webui",
"Interface": "Interface",
"join our Discord for help.": "join our Discord for help.",
"JSON": "JSON",
"JWT Expiration": "JWT Expiration",
"JWT Token": "JWT Token",
"Keep Alive": "Keep Alive",
"Keyboard shortcuts": "Keyboard shortcuts",
"Language": "Language",
"Light": "Light",
"Listening...": "Listening...",
"LLMs can make mistakes. Verify important information.": "LLMs can make mistakes. Verify important information.",
"Made by OpenWebUI Community": "Made by OpenWebUI Community",
"Make sure to enclose them with": "Make sure to enclose them with",
"Manage LiteLLM Models": "Manage LiteLLM Models",
"Manage Models": "Manage Models",
"Manage Ollama Models": "Manage Ollama Models",
"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.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "MMMM DD, YYYY",
"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 {{modelId}} not found": "Model {{modelId}} not found",
"Model {{modelName}} already exists.": "Model {{modelName}} already exists.",
"Model Name": "Model Name",
"Model not selected": "Model not selected",
"Model Tag Name": "Model Tag Name",
"Model Whitelisting": "Model Whitelisting",
"Model(s) Whitelisted": "Model(s) Whitelisted",
"Modelfile": "Modelfile",
"Modelfile Advanced Settings": "Modelfile Advanced Settings",
"Modelfile Content": "Modelfile Content",
"Modelfiles": "Modelfiles",
"Models": "Models",
"My Documents": "My Documents",
"My Modelfiles": "My Modelfiles",
"My Prompts": "My Prompts",
"Name": "Name",
"Name Tag": "Name Tag",
"Name your modelfile": "Name your modelfile",
"New Chat": "New Chat",
"New Password": "New Password",
"Not sure what to add?": "Not sure what to add?",
"Not sure what to write? Switch to": "Not sure what to write? Switch to",
"Off": "Off",
"Okay, Let's Go!": "Okay, Let's Go!",
"Ollama Base URL": "",
"Ollama Version": "Ollama Version",
"On": "On",
"Only": "Only",
"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! 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.",
"Open": "Open",
"Open AI": "Open AI",
"Open AI (Dall-E)": "",
"Open new chat": "Open new chat",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "",
"OpenAI API Key is required.": "",
"or": "or",
"Parameters": "Parameters",
"Password": "Password",
"PDF Extract Images (OCR)": "PDF Extract Images (OCR)",
"pending": "pending",
"Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}",
"Playground": "Playground",
"Profile": "Profile",
"Prompt Content": "Prompt Content",
"Prompt suggestions": "Prompt suggestions",
"Prompts": "Prompts",
"Pull a model from Ollama.com": "Pull a model from Ollama.com",
"Pull Progress": "Pull Progress",
"Query Params": "Query Params",
"RAG Template": "RAG Template",
"Raw Format": "Raw Format",
"Record voice": "Record voice",
"Redirecting you to OpenWebUI Community": "Redirecting you to OpenWebUI Community",
"Release Notes": "Release Notes",
"Repeat Last N": "Repeat Last N",
"Repeat Penalty": "Repeat Penalty",
"Request Mode": "Request Mode",
"Reset Vector Storage": "Reset Vector Storage",
"Response AutoCopy to Clipboard": "Response AutoCopy to Clipboard",
"Role": "Role",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Save",
"Save & Create": "Save & Create",
"Save & Submit": "Save & Submit",
"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",
"Scan": "Scan",
"Scan complete!": "Scan complete!",
"Scan for documents from {{path}}": "Scan for documents from {{path}}",
"Search": "Search",
"Search Documents": "Search Documents",
"Search Prompts": "Search Prompts",
"See readme.md for instructions": "See readme.md for instructions",
"See what's new": "See what's new",
"Seed": "Seed",
"Select a mode": "",
"Select a model": "Select a model",
"Select an Ollama instance": "",
"Send a Messsage": "Send a Messsage",
"Send message": "Send message",
"Server connection verified": "Server connection verified",
"Set as default": "Set as default",
"Set Default Model": "Set Default Model",
"Set Image Size": "Set Image Size",
"Set Steps": "Set Steps",
"Set Title Auto-Generation Model": "Set Title Auto-Generation Model",
"Set Voice": "Set Voice",
"Settings": "Settings",
"Settings saved successfully!": "Settings saved successfully!",
"Share to OpenWebUI Community": "Share to OpenWebUI Community",
"short-summary": "short-summary",
"Show": "Show",
"Show Additional Params": "Show Additional Params",
"Show shortcuts": "Show shortcuts",
"sidebar": "sidebar",
"Sign in": "Sign in",
"Sign Out": "Sign Out",
"Sign up": "Sign up",
"Speech recognition error: {{error}}": "Speech recognition error: {{error}}",
"Speech-to-Text Engine": "Speech-to-Text Engine",
"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API is not supported in this browser.",
"Stop Sequence": "Stop Sequence",
"STT Settings": "STT Settings",
"Submit": "Submit",
"Success": "Success",
"Successfully updated.": "Successfully updated.",
"Sync All": "Sync All",
"System": "System",
"System Prompt": "System Prompt",
"Tags": "Tags",
"Temperature": "Temperature",
"Template": "Template",
"Text Completion": "Text Completion",
"Text-to-Speech Engine": "Text-to-Speech Engine",
"Tfs Z": "Tfs Z",
"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 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.",
"Title": "Title",
"Title Auto-Generation": "Title Auto-Generation",
"Title Generation Prompt": "Title Generation Prompt",
"to": "to",
"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 chat input.": "to chat input.",
"Toggle settings": "Toggle settings",
"Toggle sidebar": "Toggle sidebar",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Trouble accessing Ollama?",
"TTS Settings": "TTS Settings",
"Type Hugging Face Resolve (Download) URL": "",
"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",
"Update password": "Update password",
"Upload a GGUF model": "Upload a GGUF model",
"Upload files": "Upload files",
"Upload Progress": "Upload Progress",
"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 Gravatar": "",
"user": "user",
"User Permissions": "User Permissions",
"Users": "Users",
"Utilize": "Utilize",
"Valid time units:": "Valid time units:",
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable to have them replaced with clipboard content.",
"Version": "Version",
"Web": "Web",
"WebUI Add-ons": "WebUI Add-ons",
"WebUI Settings": "WebUI Settings",
"WebUI will make requests to": "WebUI will make requests to",
"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.",
"Whisper (Local)": "Whisper (Local)",
"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].",
"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,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' یا '-1' برای غیر فعال کردن انقضا.",
"(Beta)": "(بتا)",
"(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)",
"(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": "توضیح کوتاهی در مورد کاری که این فایل\u200cمدل انجام می دهد اضافه کنید",
"Add a short title for this prompt": "یک عنوان کوتاه برای این درخواست اضافه کنید",
"Add a tag": "اضافه کردن یک تگ",
"Add Docs": "اضافه کردن اسناد",
"Add Files": "اضافه کردن فایل\u200cها",
"Add message": "اضافه کردن پیغام",
"add tags": "اضافه کردن تگ\u200cها",
"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 Base URL",
"API Key": "API Key",
"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": "پایه URL AUTOMATIC1111 ",
"AUTOMATIC1111 Base URL is required.": "به URL پایه AUTOMATIC1111 مورد نیاز است.",
"available!": "در دسترس!",
"Back": "بازگشت",
"Builder Mode": "حالت سازنده",
"Cancel": "لغو",
"Categories": "دسته\u200cبندی\u200cها",
"Change Password": "تغییر رمز عبور",
"Chat": "گپ",
"Chat History": "تاریخچه\u200cی گفتگو",
"Chat History is off for this browser.": "سابقه گپ برای این مرورگر خاموش است.",
"Chats": "گپ\u200cها",
"Check Again": "چک مجدد",
"Check for updates": "بررسی به\u200cروزرسانی",
"Checking for updates...": "در حال بررسی برای به\u200cروزرسانی..",
"Choose a model before saving...": "قبل از ذخیره یک مدل را انتخاب کنید...",
"Chunk Overlap": "همپوشانی تکه",
"Chunk Params": "پارامترهای تکه",
"Chunk Size": "اندازه تکه",
"Click here for help.": "برای کمک اینجا را کلیک کنید.",
"Click here to check other modelfiles.": "برای بررسی سایر فایل\u200cهای مدل اینجا را کلیک کنید.",
"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':": "یک عبارت مختصر و ۳ تا ۵ کلمه ای را به عنوان سرفصل برای پرس و جو زیر ایجاد کنید، به شدت محدودیت ۳-۵ کلمه را رعایت کنید و از استفاده از کلمه 'عنوان' خودداری کنید:",
"Create a modelfile": "ایجاد یک فایل مدل",
"Create Account": "ساخت حساب کاربری",
"Created at": "ایجاد شده در",
"Created by": "ایجاد شده توسط",
"Current Model": "مدل فعلی",
"Current Password": "رمز عبور فعلی",
"Custom": "دلخواه",
"Customize Ollama models for a specific purpose": "مدل های اولاما را برای یک هدف خاص سفارشی کنید",
"Dark": "تیره",
"Database": "پایگاه داده",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "پیشفرض",
"Default (Automatic1111)": "پیشفرض (Automatic1111)",
"Default (Web API)": "پیشفرض (Web API)",
"Default model updated": "مدل پیشفرض به\u200cروزرسانی شد",
"Default Prompt Suggestions": "پیشنهادات پرامپت پیش فرض",
"Default User Role": "نقش کاربر پیش فرض",
"delete": "حذف",
"Delete a model": "حذف یک مدل",
"Delete chat": "حذف گپ",
"Delete Chats": "حذف گپ\u200cها",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} پاک شد",
"Deleted {tagName}": "{tagName} حذف شد",
"Description": "توضیحات",
"Desktop Notifications": "اعلان",
"Disabled": "غیرفعال",
"Discover a modelfile": "فایل مدل را کشف کنید",
"Discover a prompt": "یک اعلان را کشف کنید",
"Discover, download, and explore custom prompts": "پرامپت\u200cهای سفارشی را کشف، دانلود و کاوش کنید",
"Discover, download, and explore model presets": "پیش تنظیمات مدل را کشف، دانلود و کاوش کنید",
"Display the username instead of You in the Chat": "نمایش نام کاربری به جای «شما» در چت",
"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'. واحد\u200cهای زمانی معتبر 's', 'm', 'h' هستند.",
"Edit Doc": "ویرایش سند",
"Edit User": "ویرایش کاربر",
"Email": "ایمیل",
"Enable Chat History": "تاریخچه چت را فعال کنید",
"Enable New Sign Ups": "فعال کردن ثبت نام\u200cهای جدید",
"Enabled": "فعال",
"Enter {{role}} message here": "پیام {{role}} را اینجا وارد کنید",
"Enter API Key": "کلید API را وارد کنید",
"Enter Chunk Overlap": "مقدار Chunk Overlap را وارد کنید",
"Enter Chunk Size": "مقدار Chunk Size را وارد کنید",
"Enter Image Size (e.g. 512x512)": "اندازه تصویر را وارد کنید (مثال: 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "URL پایه مربوط به LiteLLM API را وارد کنید (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "کلید API مربوط به LiteLLM را وارد کنید (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "RPM API مربوط به LiteLLM را وارد کنید (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)": "اکسپورت از همه گپ\u200cها(همه کاربران)",
"Export Chats": "اکسپورت از گپ\u200cها",
"Export Documents Mapping": "اکسپورت از نگاشت اسناد",
"Export Modelfiles": "اکسپورت از فایل\u200cهای مدل",
"Export Prompts": "اکسپورت از پرامپت\u200cها",
"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": "ایمپورت گپ\u200cها",
"Import Documents Mapping": "ایمپورت نگاشت اسناد",
"Import Modelfiles": "ایمپورت فایل\u200cهای مدل",
"Import Prompts": "ایمپورت پرامپت\u200cها",
"Include `--api` flag when running stable-diffusion-webui": "فلگ `--api` را هنکام اجرای stable-diffusion-webui استفاده کنید.",
"Interface": "رابط",
"join our Discord for help.": "برای کمک به دیسکورد ما بپیوندید.",
"JSON": "JSON",
"JWT Expiration": "JWT انقضای",
"JWT Token": "JWT توکن",
"Keep Alive": "Keep Alive",
"Keyboard shortcuts": "میانبرهای صفحه کلید",
"Language": "زبان",
"Light": "روشن",
"Listening...": "در حال گوش دادن...",
"LLMs can make mistakes. Verify important information.": "مدل\u200cهای زبانی بزرگ می\u200cتوانند اشتباه کنند. اطلاعات مهم را راستی\u200cآزمایی کنید.",
"Made by OpenWebUI Community": "ساخته شده توسط OpenWebUI Community",
"Make sure to enclose them with": "مطمئن شوید که آنها را با این محصور کنید:",
"Manage LiteLLM Models": "Manage LiteLLM Models",
"Manage Models": "مدیریت مدل\u200cها",
"Manage Ollama Models": "مدیریت مدل\u200cهای اولاما",
"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",
"MMMM DD, YYYY": "MMMM DD, YYYY",
"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": "تنظیمات پیشرفته فایل\u200cمدل",
"Modelfile Content": "محتویات فایل مدل",
"Modelfiles": "فایل\u200cهای مدل",
"Models": "مدل\u200cها",
"My Documents": "اسناد من",
"My Modelfiles": "فایل\u200cهای مدل من",
"My Prompts": "پرامپت\u200cهای من",
"Name": "نام",
"Name Tag": "نام تگ",
"Name your modelfile": "فایل مدل را نام\u200cگذاری کنید",
"New Chat": "گپ جدید",
"New Password": "رمز عبور جدید",
"Not sure what to add?": "مطمئن نیستید چه چیزی را اضافه کنید؟",
"Not sure what to write? Switch to": "مطمئن نیستید چه بنویسید؟ تغییر به",
"Off": "خاموش",
"Okay, Let's Go!": "باشه، بزن بریم!",
"Ollama Base URL": "URL پایه اولاما",
"Ollama Version": "نسخه اولاما",
"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.": "اوه! شما از یک روش پشتیبانی نشده (فقط frontend) استفاده می کنید. لطفاً WebUI را از بکند اجرا کنید.",
"Open": "باز",
"Open AI": "Open AI",
"Open AI (Dall-E)": "Open AI (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": "پرامپت\u200cها",
"Pull a model from Ollama.com": "دریافت یک مدل از Ollama.com",
"Pull Progress": "پیشرفت دریافت",
"Query Params": "پارامترهای پرس و جو",
"RAG Template": "RAG الگوی",
"Raw Format": "فرمت خام",
"Record voice": "ضبط صدا",
"Redirecting you to OpenWebUI Community": "در حال هدایت به OpenWebUI Community",
"Release Notes": "یادداشت\u200cهای انتشار",
"Repeat Last N": "Repeat Last N",
"Repeat Penalty": "Repeat Penalty",
"Request Mode": "حالت درخواست",
"Reset Vector Storage": "بازنشانی ذخیره سازی برداری",
"Response AutoCopy to Clipboard": "کپی خودکار پاسخ به کلیپ بورد",
"Role": "نقش",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "ذخیره",
"Save & Create": "ذخیره و ایجاد",
"Save & Submit": "ذخیره و ارسال",
"Save & Update": "ذخیره و به\u200cروزرسانی",
"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": "ذخیره گزارش\u200cهای چت مستقیماً در حافظه مرورگر شما دیگر پشتیبانی نمی\u200cشود. لطفاً با کلیک بر روی دکمه زیر، چند لحظه برای دانلود و حذف گزارش های چت خود وقت بگذارید. نگران نباشید، شما به راحتی می توانید گزارش های چت خود را از طریق بکند دوباره وارد کنید",
"Scan": "اسکن",
"Scan complete!": "اسکن کامل شد!",
"Scan for documents from {{path}}": "اسکن اسناد از {{path}}",
"Search": "جستجو",
"Search Documents": "جستجوی اسناد",
"Search Prompts": "جستجوی پرامپت\u200cها",
"See readme.md for instructions": "برای مشاهده دستورالعمل\u200cها به readme.md مراجعه کنید",
"See what's new": "ببینید موارد جدید چه بوده",
"Seed": "Seed",
"Select a mode": "یک حالت انتخاب کنید",
"Select a model": "انتخاب یک مدل",
"Select an Ollama instance": "انتخاب یک نمونه از اولاما",
"Send a Messsage": "ارسال یک پیام",
"Send message": "ارسال پیام",
"Server connection verified": "اتصال سرور تأیید شد",
"Set as default": "تنظیم به عنوان پیشفرض",
"Set Default Model": "تنظیم مدل پیش فرض",
"Set Image Size": "تنظیم اندازه تصویر",
"Set Steps": "تنظیم گام\u200cها",
"Set Title Auto-Generation Model": "تنظیم مدل تولید خودکار عنوان",
"Set Voice": "تنظیم صدا",
"Settings": "تنظیمات",
"Settings saved successfully!": "تنظیمات با موفقیت ذخیره شد!",
"Share to OpenWebUI Community": "اشتراک گذاری با OpenWebUI Community",
"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.": "API تشخیص گفتار در این مرورگر پشتیبانی نمی شود.",
"Stop Sequence": "توالی توقف",
"STT Settings": "STT تنظیمات",
"Submit": "ارسال",
"Success": "موفقیت",
"Successfully updated.": "با موفقیت به روز شد",
"Sync All": "همگام سازی همه",
"System": "سیستم",
"System Prompt": "پرامپت سیستم",
"Tags": "تگ\u200cها",
"Temperature": "دما",
"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.": "این تنظیم در مرورگرها یا دستگاه\u200cها همگام\u200cسازی نمی\u200cشود.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "با فشردن کلید Tab در ورودی چت پس از هر بار تعویض، چندین متغیر را به صورت متوالی به روزرسانی کنید.",
"Title": "عنوان",
"Title Auto-Generation": "تولید خودکار عنوان",
"Title Generation Prompt": "پرامپت تولید عنوان",
"to": "به",
"To access the available model names for downloading,": "برای دسترسی به نام مدل های موجود برای دانلود،",
"To access the GGUF models available for downloading,": "برای دسترسی به مدل\u200cهای GGUF موجود برای دانلود،",
"to chat input.": "در ورودی گپ.",
"Toggle settings": "نمایش/عدم نمایش تنظیمات",
"Toggle sidebar": "نمایش/عدم نمایش نوار کناری",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "در دسترسی به اولاما مشکل دارید؟",
"TTS Settings": "تنظیمات TTS",
"Type Hugging Face Resolve (Download) URL": "مقدار URL دانلود (Resolve) Hugging Face را وارد کنید",
"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": "بارگذاری فایل\u200cها",
"Upload Progress": "پیشرفت آپلود",
"URL Mode": "حالت URL",
"Use '#' in the prompt input to load and select your documents.": "در پرامپت از '#' برای لود و انتخاب اسناد خود استفاده کنید.",
"Use Gravatar": "استفاده از گراواتار",
"user": "کاربر",
"User Permissions": "مجوزهای کاربر",
"Users": "کاربران",
"Utilize": "استفاده کنید",
"Valid time units:": "واحدهای زمانی معتبر:",
"variable": "متغیر",
"variable to have them replaced with clipboard content.": "متغیر برای جایگزینی آنها با محتوای کلیپ بورد.",
"Version": "نسخه",
"Web": "وب",
"WebUI Add-ons": "WebUI افزونه\u200cهای",
"WebUI Settings": "تنظیمات WebUI",
"WebUI will make requests to": "WebUI درخواست\u200cها را ارسال خواهد کرد به",
"Whats New in": "موارد جدید در",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "وقتی سابقه خاموش است، چت\u200cهای جدید در این مرورگر در سابقه شما در هیچ یک از دستگاه\u200cهایتان ظاهر نمی\u200cشوند.",
"Whisper (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.": "شما اکنون وارد شده\u200cاید."
}

View file

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' ou '-1' pour aucune expiration.",
"(Beta)": "(Bêta)",
"(e.g. `sh webui.sh --api`)": "(par ex. `sh webui.sh --api`)",
"(latest)": "",
"{{modelName}} is thinking...": "{{modelName}} réfléchit...",
"{{webUIName}} Backend Required": "Backend {{webUIName}} requis",
"a user": "",
"About": "À propos",
"Account": "Compte",
"Action": "Action",
"Add a model": "Ajouter un modèle",
"Add a model tag name": "Ajouter un nom de tag pour le modèle",
"Add a short description about what this modelfile does": "Ajouter une courte description de ce que fait ce fichier de modèle",
"Add a short title for this prompt": "Ajouter un court titre pour ce prompt",
"Add a tag": "",
"Add Docs": "Ajouter des documents",
"Add Files": "Ajouter des fichiers",
"Add message": "Ajouter un message",
"add tags": "ajouter des tags",
"Adjusting these settings will apply changes universally to all users.": "L'ajustement de ces paramètres appliquera les changements à tous les utilisateurs.",
"admin": "Administrateur",
"Admin Panel": "Panneau d'administration",
"Admin Settings": "Paramètres d'administration",
"Advanced Parameters": "Paramètres avancés",
"all": "tous",
"All Users": "Tous les utilisateurs",
"Allow": "Autoriser",
"Allow Chat Deletion": "Autoriser la suppression du chat",
"alphanumeric characters and hyphens": "caractères alphanumériques et tirets",
"Already have an account?": "Vous avez déjà un compte ?",
"an assistant": "",
"and": "et",
"API Base URL": "URL de base de l'API",
"API Key": "Clé API",
"API RPM": "RPM API",
"are allowed - Activate this command by typing": "sont autorisés - Activez cette commande en tapant",
"Are you sure?": "",
"Audio": "Audio",
"Auto-playback response": "Réponse en lecture automatique",
"Auto-send input after 3 sec.": "Envoyer automatiquement l'entrée après 3 sec.",
"AUTOMATIC1111 Base URL": "URL de base AUTOMATIC1111",
"AUTOMATIC1111 Base URL is required.": "",
"available!": "disponible !",
"Back": "Retour",
"Builder Mode": "Mode Constructeur",
"Cancel": "Annuler",
"Categories": "Catégories",
"Change Password": "Changer le mot de passe",
"Chat": "Chat",
"Chat History": "Historique du chat",
"Chat History is off for this browser.": "L'historique du chat est désactivé pour ce navigateur.",
"Chats": "Chats",
"Check Again": "Vérifier à nouveau",
"Check for updates": "Vérifier les mises à jour",
"Checking for updates...": "Vérification des mises à jour...",
"Choose a model before saving...": "Choisissez un modèle avant d'enregistrer...",
"Chunk Overlap": "Chevauchement de bloc",
"Chunk Params": "Paramètres de bloc",
"Chunk Size": "Taille de bloc",
"Click here for help.": "",
"Click here to check other modelfiles.": "Cliquez ici pour vérifier d'autres fichiers de modèle.",
"Click here to select": "",
"Click here to select documents.": "",
"click here.": "cliquez ici.",
"Click on the user role button to change a user's role.": "Cliquez sur le bouton de rôle d'utilisateur pour changer le rôle d'un utilisateur.",
"Close": "Fermer",
"Collection": "Collection",
"Command": "Commande",
"Confirm Password": "Confirmer le mot de passe",
"Connections": "Connexions",
"Content": "Contenu",
"Context Length": "Longueur du contexte",
"Conversation Mode": "Mode de conversation",
"Copy last code block": "Copier le dernier bloc de code",
"Copy last response": "Copier la dernière réponse",
"Copying to clipboard was successful!": "La copie dans le presse-papiers a réussi !",
"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':": "Créez une phrase concise de 3-5 mots comme en-tête pour la requête suivante, en respectant strictement la limite de 3-5 mots et en évitant l'utilisation du mot 'titre' :",
"Create a modelfile": "Créer un fichier de modèle",
"Create Account": "Créer un compte",
"Created at": "Créé le",
"Created by": "Créé par",
"Current Model": "Modèle actuel",
"Current Password": "Mot de passe actuel",
"Custom": "Personnalisé",
"Customize Ollama models for a specific purpose": "Personnaliser les modèles Ollama pour un objectif spécifique",
"Dark": "Sombre",
"Database": "Base de données",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "Par défaut",
"Default (Automatic1111)": "",
"Default (Web API)": "Par défaut (API Web)",
"Default model updated": "Modèle par défaut mis à jour",
"Default Prompt Suggestions": "Suggestions de prompt par défaut",
"Default User Role": "Rôle d'utilisateur par défaut",
"delete": "supprimer",
"Delete a model": "Supprimer un modèle",
"Delete chat": "Supprimer le chat",
"Delete Chats": "Supprimer les chats",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé",
"Deleted {tagName}": "{tagName} supprimé",
"Description": "Description",
"Desktop Notifications": "Notifications de bureau",
"Disabled": "Désactivé",
"Discover a modelfile": "Découvrir un fichier de modèle",
"Discover a prompt": "Découvrir un prompt",
"Discover, download, and explore custom prompts": "Découvrir, télécharger et explorer des prompts personnalisés",
"Discover, download, and explore model presets": "Découvrir, télécharger et explorer des préconfigurations de modèles",
"Display the username instead of You in the Chat": "Afficher le nom d'utilisateur au lieu de 'Vous' dans le Chat",
"Document": "Document",
"Document Settings": "Paramètres du document",
"Documents": "Documents",
"does not make any external connections, and your data stays securely on your locally hosted server.": "ne fait aucune connexion externe, et vos données restent en sécurité sur votre serveur hébergé localement.",
"Don't Allow": "Ne pas autoriser",
"Don't have an account?": "Vous n'avez pas de compte ?",
"Download as a File": "Télécharger en tant que fichier",
"Download Database": "Télécharger la base de données",
"Drop any files here to add to the conversation": "Déposez des fichiers ici pour les ajouter à la conversation",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "par ex. '30s', '10m'. Les unités de temps valides sont 's', 'm', 'h'.",
"Edit Doc": "Éditer le document",
"Edit User": "Éditer l'utilisateur",
"Email": "Email",
"Enable Chat History": "Activer l'historique du chat",
"Enable New Sign Ups": "Activer les nouvelles inscriptions",
"Enabled": "Activé",
"Enter {{role}} message here": "",
"Enter API Key": "",
"Enter Chunk Overlap": "",
"Enter Chunk Size": "",
"Enter Image Size (e.g. 512x512)": "",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "",
"Enter LiteLLM API Key (litellm_params.api_key)": "",
"Enter LiteLLM API RPM (litellm_params.rpm)": "",
"Enter LiteLLM Model (litellm_params.model)": "",
"Enter Max Tokens (litellm_params.max_tokens)": "",
"Enter model tag (e.g. {{modelTag}})": "",
"Enter Number of Steps (e.g. 50)": "",
"Enter stop sequence": "Entrez la séquence d'arrêt",
"Enter Top K": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "",
"Enter Your Email": "Entrez votre email",
"Enter Your Full Name": "Entrez votre nom complet",
"Enter Your Password": "Entrez votre mot de passe",
"Experimental": "Expérimental",
"Export All Chats (All Users)": "Exporter tous les chats (tous les utilisateurs)",
"Export Chats": "Exporter les chats",
"Export Documents Mapping": "Exporter la correspondance des documents",
"Export Modelfiles": "Exporter les fichiers de modèle",
"Export Prompts": "Exporter les prompts",
"Failed to read clipboard contents": "Échec de la lecture du contenu du presse-papiers",
"File Mode": "Mode fichier",
"File not found.": "Fichier non trouvé.",
"Focus chat input": "Concentrer sur l'entrée du chat",
"Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme ceci :",
"From (Base Model)": "De (Modèle de base)",
"Full Screen Mode": "Mode plein écran",
"General": "Général",
"General Settings": "Paramètres généraux",
"Hello, {{name}}": "Bonjour, {{name}}",
"Hide": "Cacher",
"Hide Additional Params": "Hide Additional Params",
"How can I help you today?": "Comment puis-je vous aider aujourd'hui ?",
"Image Generation (Experimental)": "Génération d'image (Expérimental)",
"Image Generation Engine": "",
"Image Settings": "Paramètres d'image",
"Images": "Images",
"Import Chats": "Importer les chats",
"Import Documents Mapping": "Importer la correspondance des documents",
"Import Modelfiles": "Importer les fichiers de modèle",
"Import Prompts": "Importer les prompts",
"Include `--api` flag when running stable-diffusion-webui": "Inclure le drapeau `--api` lors de l'exécution de stable-diffusion-webui",
"Interface": "Interface",
"join our Discord for help.": "rejoignez notre Discord pour obtenir de l'aide.",
"JSON": "JSON",
"JWT Expiration": "Expiration JWT",
"JWT Token": "Jeton JWT",
"Keep Alive": "Garder en vie",
"Keyboard shortcuts": "Raccourcis clavier",
"Language": "Langue",
"Light": "Clair",
"Listening...": "Écoute...",
"LLMs can make mistakes. Verify important information.": "Les LLMs peuvent faire des erreurs. Vérifiez les informations importantes.",
"Made by OpenWebUI Community": "Réalisé par la communauté OpenWebUI",
"Make sure to enclose them with": "Assurez-vous de les entourer avec",
"Manage LiteLLM Models": "Gérer les modèles LiteLLM",
"Manage Models": "",
"Manage Ollama Models": "Gérer les modèles Ollama",
"Max Tokens": "Tokens maximaux",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Un maximum de 3 modèles peut être téléchargé simultanément. Veuillez réessayer plus tard.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "MMMM DD, YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "Le modèle '{{modelName}}' a été téléchargé avec succès.",
"Model '{{modelTag}}' is already in queue for downloading.": "Le modèle '{{modelTag}}' est déjà dans la file d'attente pour le téléchargement.",
"Model {{modelId}} not found": "Modèle {{modelId}} non trouvé",
"Model {{modelName}} already exists.": "Le modèle {{modelName}} existe déjà.",
"Model Name": "Nom du modèle",
"Model not selected": "Modèle non sélectionné",
"Model Tag Name": "Nom de tag du modèle",
"Model Whitelisting": "",
"Model(s) Whitelisted": "",
"Modelfile": "Fichier de modèle",
"Modelfile Advanced Settings": "Paramètres avancés du fichier de modèle",
"Modelfile Content": "Contenu du fichier de modèle",
"Modelfiles": "Fichiers de modèle",
"Models": "Modèles",
"My Documents": "Mes documents",
"My Modelfiles": "Mes fichiers de modèle",
"My Prompts": "Mes prompts",
"Name": "Nom",
"Name Tag": "Tag de nom",
"Name your modelfile": "Nommez votre fichier de modèle",
"New Chat": "Nouveau chat",
"New Password": "Nouveau mot de passe",
"Not sure what to add?": "Vous ne savez pas quoi ajouter ?",
"Not sure what to write? Switch to": "Vous ne savez pas quoi écrire ? Basculer vers",
"Off": "Désactivé",
"Okay, Let's Go!": "D'accord, allons-y !",
"Ollama Base URL": "",
"Ollama Version": "Version Ollama",
"On": "Activé",
"Only": "Seulement",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Seuls les caractères alphanumériques et les tirets sont autorisés dans la chaîne de commande.",
"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.": "Oups ! Tenez bon ! Vos fichiers sont encore dans le four. Nous les cuisinons à la perfection. Soyez patient et nous vous informerons dès qu'ils seront prêts.",
"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.",
"Open": "Ouvrir",
"Open AI": "Open AI",
"Open AI (Dall-E)": "",
"Open new chat": "Ouvrir un nouveau chat",
"OpenAI API": "API OpenAI",
"OpenAI API Key": "",
"OpenAI API Key is required.": "",
"or": "ou",
"Parameters": "Paramètres",
"Password": "Mot de passe",
"PDF Extract Images (OCR)": "",
"pending": "en attente",
"Permission denied when accessing microphone: {{error}}": "Permission refusée lors de l'accès au microphone : {{error}}",
"Playground": "Aire de jeu",
"Profile": "Profil",
"Prompt Content": "Contenu du prompt",
"Prompt suggestions": "Suggestions de prompt",
"Prompts": "Prompts",
"Pull a model from Ollama.com": "Tirer un modèle de Ollama.com",
"Pull Progress": "Progression du tirage",
"Query Params": "",
"RAG Template": "Modèle RAG",
"Raw Format": "Format brut",
"Record voice": "Enregistrer la voix",
"Redirecting you to OpenWebUI Community": "Vous redirige vers la communauté OpenWebUI",
"Release Notes": "Notes de version",
"Repeat Last N": "Répéter les derniers N",
"Repeat Penalty": "Pénalité de répétition",
"Request Mode": "Mode de demande",
"Reset Vector Storage": "Réinitialiser le stockage de vecteur",
"Response AutoCopy to Clipboard": "Copie automatique de la réponse dans le presse-papiers",
"Role": "Rôle",
"Rosé Pine": "Pin Rosé",
"Rosé Pine Dawn": "Aube Pin Rosé",
"Save": "Enregistrer",
"Save & Create": "Enregistrer & Créer",
"Save & Submit": "Enregistrer & Soumettre",
"Save & Update": "Enregistrer & Mettre à jour",
"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": "La sauvegarde des chat directement dans le stockage de votre navigateur n'est plus prise en charge. Veuillez prendre un moment pour télécharger et supprimer vos journaux de chat en cliquant sur le bouton ci-dessous. Ne vous inquiétez pas, vous pouvez facilement importer vos sauvegardes de chat via",
"Scan": "Scanner",
"Scan complete!": "Scan terminé !",
"Scan for documents from {{path}}": "Scanner des documents depuis {{path}}",
"Search": "Recherche",
"Search Documents": "Rechercher des documents",
"Search Prompts": "Rechercher des prompts",
"See readme.md for instructions": "Voir readme.md pour les instructions",
"See what's new": "Voir les nouveautés",
"Seed": "Graine",
"Select a mode": "",
"Select a model": "Sélectionner un modèle",
"Select an Ollama instance": "",
"Send a Messsage": "Envoyer un message",
"Send message": "Envoyer un message",
"Server connection verified": "Connexion au serveur vérifiée",
"Set as default": "Définir par défaut",
"Set Default Model": "Définir le modèle par défaut",
"Set Image Size": "Définir la taille de l'image",
"Set Steps": "Définir les étapes",
"Set Title Auto-Generation Model": "Définir le modèle de génération automatique de titre",
"Set Voice": "Définir la voix",
"Settings": "Paramètres",
"Settings saved successfully!": "",
"Share to OpenWebUI Community": "Partager avec la communauté OpenWebUI",
"short-summary": "résumé court",
"Show": "Montrer",
"Show Additional Params": "Show Additional Params",
"Show shortcuts": "Afficher les raccourcis",
"sidebar": "barre latérale",
"Sign in": "Se connecter",
"Sign Out": "Se déconnecter",
"Sign up": "S'inscrire",
"Speech recognition error: {{error}}": "Erreur de reconnaissance vocale : {{error}}",
"Speech-to-Text Engine": "Moteur de reconnaissance vocale",
"SpeechRecognition API is not supported in this browser.": "L'API SpeechRecognition n'est pas prise en charge dans ce navigateur.",
"Stop Sequence": "Séquence d'arrêt",
"STT Settings": "Paramètres STT",
"Submit": "Soumettre",
"Success": "Succès",
"Successfully updated.": "Mis à jour avec succès.",
"Sync All": "Synchroniser tout",
"System": "Système",
"System Prompt": "Invite de système",
"Tags": "Tags",
"Temperature": "Température",
"Template": "Modèle",
"Text Completion": "Complétion de texte",
"Text-to-Speech Engine": "Moteur de synthèse vocale",
"Tfs Z": "Tfs Z",
"Theme": "Thème",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Cela garantit que vos précieuses conversations sont en sécurité dans votre base de données. Merci !",
"This setting does not sync across browsers or devices.": "Ce paramètre ne se synchronise pas entre les navigateurs ou les appareils.",
"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": "Titre",
"Title Auto-Generation": "Génération automatique de titre",
"Title Generation Prompt": "Prompt de génération de titre",
"to": "à",
"To access the available model names for downloading,": "Pour accéder aux noms de modèles disponibles pour le téléchargement,",
"To access the GGUF models available for downloading,": "To access the GGUF models available for downloading,",
"to chat input.": "to chat input.",
"Toggle settings": "Basculer les paramètres",
"Toggle sidebar": "Basculer la barre latérale",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Problèmes d'accès à Ollama ?",
"TTS Settings": "Paramètres TTS",
"Type Hugging Face Resolve (Download) URL": "",
"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh ! Il y a eu un problème de connexion à {{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",
"Update password": "",
"Upload a GGUF model": "Upload a GGUF model",
"Upload files": "Téléverser des fichiers",
"Upload Progress": "Upload Progress",
"URL Mode": "URL Mode",
"Use '#' in the prompt input to load and select your documents.": "Utilisez '#' dans l'entrée du prompt pour charger et sélectionner vos documents.",
"Use Gravatar": "",
"user": "Utilisateur",
"User Permissions": "Permissions d'utilisateur",
"Users": "Utilisateurs",
"Utilize": "Utiliser",
"Valid time units:": "Unités de temps valides :",
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable pour les remplacer par le contenu du presse-papiers.",
"Version": "",
"Web": "Web",
"WebUI Add-ons": "Add-ons WebUI",
"WebUI Settings": "Paramètres WebUI",
"WebUI will make requests to": "",
"Whats New in": "Quoi de neuf dans",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Lorsque l'historique est désactivé, les nouveaux chats sur ce navigateur n'apparaîtront pas dans votre historique sur aucun de vos appareils.",
"Whisper (Local)": "Whisper (Local)",
"Write a prompt suggestion (e.g. Who are you?)": "Écrivez un prompt (e.x. Qui est-tu ?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "Ecrivez un résumé en 50 mots [sujet ou mot-clé]",
"You": "You",
"You're a helpful assistant.": "Vous êtes un assistant utile",
"You're now logged in.": "Vous êtes maintenant connecté."
}

View file

@ -0,0 +1,30 @@
[
{
"code": "en-US",
"title": "English (US)"
},
{
"code": "fa-IR",
"title": "فارسی (Farsi)"
},
{
"code": "de-DE",
"title": "Deutsch"
},
{
"code": "fr-FR",
"title": "French (France)"
},
{
"code": "uk-UA",
"title": "Ukrainian"
},
{
"code": "zh-TW",
"title": "Chinese (Traditional)"
},
{
"code": "zh-CN",
"title": "Chinese (Simplified)"
}
]

View file

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' для відсутності терміну дії.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)",
"(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": "Базова адреса URL API",
"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": "Базова адреса URL AUTOMATIC1111",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Необхідна 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 Params": "Параметри фрагментів",
"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':": "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 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": "Показувати ім'я користувача замість 'Ви' в чаті",
"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'. Дійсні одиниці часу: 'с', 'хв', 'г'.",
"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)": "Введіть URL-адресу API LiteLLM (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "Введіть ключ API LiteLLM (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "Введіть RPM API LiteLLM (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": "Включіть прапор `--api` при запуску stable-diffusion-webui",
"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.": "LLMs можуть помилятися. Перевірте важливу інформацію.",
"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",
"MMMM DD, YYYY": "MMMM DD, YYYY",
"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": "Основна URL-адреса Ollama",
"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)": "Open AI (Dall-E)",
"Open new chat": "Відкрити новий чат",
"OpenAI API": "API OpenAI",
"OpenAI API Key": "Ключ API OpenAI",
"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": "Необроблений формат",
"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",
"Rosé Pine Dawn": "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 Messsage": "Надіслати повідомлення",
"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": "Теги",
"Temperature": "Температура",
"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": "Введіть URL ресурсу Hugging Face Resolve (завантаження)",
"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.": "Для введення промтів до веб-сторінок (URL) або вибору документів, будь ласка, використовуйте символ '#'.",
"Use 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 буде робити запити до",
"Whats 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)": "Whisper (локально)",
"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

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' 或 '-1' 表示无过期时间。",
"(Beta)": "(测试版)",
"(e.g. `sh webui.sh --api`)": "(例如 `sh webui.sh --api`",
"(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 基础 URL",
"AUTOMATIC1111 Base URL is required.": "需要 AUTOMATIC1111 基础 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 Params": "块参数",
"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个词的限制并避免使用“标题”一词",
"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": "在聊天中显示用户名而不是“您”",
"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": "",
"Enter API Key": "",
"Enter Chunk Overlap": "",
"Enter Chunk Size": "",
"Enter Image Size (e.g. 512x512)": "",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "",
"Enter LiteLLM API Key (litellm_params.api_key)": "",
"Enter LiteLLM API RPM (litellm_params.rpm)": "",
"Enter LiteLLM Model (litellm_params.model)": "",
"Enter Max Tokens (litellm_params.max_tokens)": "",
"Enter model tag (e.g. {{modelTag}})": "",
"Enter Number of Steps (e.g. 50)": "",
"Enter stop sequence": "输入停止序列",
"Enter Top K": "",
"Enter URL (e.g. 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.": "大型语言模型可能会犯错。验证重要信息。",
"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",
"MMMM DD, YYYY": "MMMM DD, YYYY",
"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)": "Open AI (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)": "",
"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": "原始格式",
"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",
"Rosé Pine Dawn": "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": "",
"Send a Messsage": "发送消息",
"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": "语音转文字设置",
"Submit": "提交",
"Success": "成功",
"Successfully updated.": "成功更新。",
"Sync All": "同步所有",
"System": "系统",
"System Prompt": "系统提示",
"Tags": "标签",
"Temperature": "温度",
"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.": "提示在每次替换后在聊天输入中按Tab键可以连续更新多个变量槽。",
"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": "文本转语音设置",
"Type Hugging Face Resolve (Download) URL": "输入Hugging Face解析下载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": "",
"Whats 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)": "私语(本地)",
"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

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' 或 '-1' 表示無期限。",
"(Beta)": "(測試版)",
"(e.g. `sh webui.sh --api`)": "(例如 `sh webui.sh --api`)",
"(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 Base URL",
"API Key": "API Key",
"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 Params": "區塊參數",
"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個詞的限制避免使用'標題'這個詞:",
"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": "在聊天中顯示用戶名而不是“您”",
"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'.": "例如 '30秒', '10分鐘'。有效的時間單位為 '秒', '分', '小時'。",
"Edit Doc": "編輯文檔",
"Edit User": "編輯用戶",
"Email": "電子郵件",
"Enable Chat History": "啟用聊天歷史",
"Enable New Sign Ups": "允許新用戶註冊",
"Enabled": "已啟用",
"Enter {{role}} message here": "",
"Enter API Key": "輸入API Key",
"Enter Chunk Overlap": "輸入區塊重疊",
"Enter Chunk Size": "輸入區塊大小",
"Enter Image Size (e.g. 512x512)": "輸入圖片大小(例如 512x512",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "輸入 LiteLLM API 基礎 URLlitellm_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 RPMlitellm_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",
"MMMM DD, YYYY": "MMMM DD, YYYY",
"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 Base URL - Ollama 啟動的連接網址",
"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)": "Open AI (Dall-E)",
"Open new chat": "打開新聊天",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "OpenAI API Key",
"OpenAI API Key is required.": "需要 OpenAI API Key。",
"or": "或",
"Parameters": "參數",
"Password": "密碼",
"PDF Extract Images (OCR)": "",
"pending": "待定",
"Permission denied when accessing microphone: {{error}}": "訪問麥克風時權限被拒絕: {{error}}",
"Playground": "AI 對話遊樂場",
"Profile": "個人資料",
"Prompt Content": "提示內容",
"Prompt suggestions": "提示建議",
"Prompts": "提示",
"Pull a model from Ollama.com": "從 Ollama.com 下載模型",
"Pull Progress": "下載進度",
"Query Params": "查詢參數",
"RAG Template": "RAG 範例",
"Raw Format": "原始格式",
"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": "",
"Send a Messsage": "發送消息",
"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": "語音轉文字設置",
"Submit": "提交",
"Success": "成功",
"Successfully updated.": "更新成功。",
"Sync All": "同步所有",
"System": "系統",
"System Prompt": "系統提示",
"Tags": "標籤",
"Temperature": "溫度",
"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.": "提示:通過在每次替換後在聊天輸入中按 Tab 鍵連續更新多個變量槽。",
"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": "文本轉語音設置",
"Type Hugging Face Resolve (Download) URL": "輸入 Hugging Face 解析下載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": "",
"Whats 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)": "Whisper本地",
"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

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, tick, getContext } from 'svelte';
import { openDB, deleteDB } from 'idb'; import { openDB, deleteDB } from 'idb';
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { onMount, tick } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama'; import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
@ -36,6 +36,8 @@
import ChangelogModal from '$lib/components/ChangelogModal.svelte'; import ChangelogModal from '$lib/components/ChangelogModal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
let ollamaVersion = ''; let ollamaVersion = '';
let loaded = false; let loaded = false;
let showShortcutsButtonElement: HTMLButtonElement; let showShortcutsButtonElement: HTMLButtonElement;
@ -199,7 +201,7 @@
{#if loaded} {#if loaded}
<div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10"> <div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10">
<Tooltip content="help" placement="left"> <Tooltip content="Help" placement="left">
<button <button
id="show-shortcuts-button" id="show-shortcuts-button"
bind:this={showShortcutsButtonElement} bind:this={showShortcutsButtonElement}
@ -240,7 +242,7 @@
location.href = '/'; location.href = '/';
}} }}
> >
Check Again {$i18n.t('Check Again')}
</button> </button>
<button <button
@ -248,7 +250,7 @@
on:click={async () => { on:click={async () => {
localStorage.removeItem('token'); localStorage.removeItem('token');
location.href = '/auth'; location.href = '/auth';
}}>Sign Out</button }}>{$i18n.t('Sign Out')}</button
> >
</div> </div>
</div> </div>
@ -267,12 +269,14 @@
</div> </div>
<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full"> <div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
Saving chat logs directly to your browser's storage is no longer supported. Please {$i18n.t(
take a moment to download and delete your chat logs by clicking the button below. "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"
Don't worry, you can easily re-import your chat logs to the backend through <span )}
class="font-semibold dark:text-white">Settings > Chats > Import Chats</span <span class="font-semibold dark:text-white"
>. This ensures that your valuable conversations are securely saved to your backend >{$i18n.t('Settings')} > {$i18n.t('Chats')} > {$i18n.t('Import Chats')}</span
database. Thank you! >. {$i18n.t(
'This ensures that your valuable conversations are securely saved to your backend database. Thank you!'
)}
</div> </div>
<div class=" mt-6 mx-auto relative group w-fit"> <div class=" mt-6 mx-auto relative group w-fit">
@ -298,7 +302,7 @@
class="text-xs text-center w-full mt-2 text-gray-400 underline" class="text-xs text-center w-full mt-2 text-gray-400 underline"
on:click={async () => { on:click={async () => {
localDBChats = []; localDBChats = [];
}}>Close</button }}>{$i18n.t('Close')}</button
> >
</div> </div>
</div> </div>

View file

@ -2,7 +2,7 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -39,6 +39,9 @@
import { RAGTemplate } from '$lib/utils/rag'; import { RAGTemplate } from '$lib/utils/rag';
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants'; import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n');
let stopResponseFlag = false; let stopResponseFlag = false;
let autoScroll = true; let autoScroll = true;
let processing = ''; let processing = '';
@ -140,7 +143,9 @@
}; };
const scrollToBottom = () => { const scrollToBottom = () => {
if (messagesContainerElement) {
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight; messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
}
}; };
////////////////////////// //////////////////////////
@ -155,7 +160,7 @@
); );
if (selectedModels.includes('')) { if (selectedModels.includes('')) {
toast.error('Model not selected'); toast.error($i18n.t('Model not selected'));
} else if (messages.length != 0 && messages.at(-1).done != true) { } else if (messages.length != 0 && messages.at(-1).done != true) {
// Response not done // Response not done
console.log('wait'); console.log('wait');
@ -165,7 +170,9 @@
) { ) {
// Upload not done // Upload not done
toast.error( toast.error(
$i18n.t(
`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.`
)
); );
} else { } else {
// Reset chat message textarea height // Reset chat message textarea height
@ -201,7 +208,7 @@
if ($settings.saveChatHistory ?? true) { if ($settings.saveChatHistory ?? true) {
chat = await createNewChat(localStorage.token, { chat = await createNewChat(localStorage.token, {
id: $chatId, id: $chatId,
title: 'New Chat', title: $i18n.t('New Chat'),
models: selectedModels, models: selectedModels,
system: $settings.system ?? undefined, system: $settings.system ?? undefined,
options: { options: {
@ -267,7 +274,7 @@
await sendPromptOllama(model, prompt, responseMessageId, _chatId); await sendPromptOllama(model, prompt, responseMessageId, _chatId);
} }
} else { } else {
toast.error(`Model ${modelId} not found`); toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
} }
}) })
); );
@ -308,7 +315,7 @@
.map((file) => file.url.slice(file.url.indexOf(',') + 1)); .map((file) => file.url.slice(file.url.indexOf(',') + 1));
// Add images array only if it contains elements // Add images array only if it contains elements
if (imageUrls && imageUrls.length > 0) { if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
baseMessage.images = imageUrls; baseMessage.images = imageUrls;
} }
@ -479,12 +486,18 @@
responseMessage.content = error.error; responseMessage.content = error.error;
} }
} else { } else {
toast.error(`Uh-oh! There was an issue connecting to Ollama.`); toast.error(
responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`; $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { provider: 'Ollama' })
);
responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: 'Ollama'
});
} }
responseMessage.error = true; responseMessage.error = true;
responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`; responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: 'Ollama'
});
responseMessage.done = true; responseMessage.done = true;
messages = messages; messages = messages;
} }
@ -532,7 +545,8 @@
.filter((message) => message) .filter((message) => message)
.map((message, idx, arr) => ({ .map((message, idx, arr) => ({
role: message.role, role: message.role,
...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false ...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
message.role === 'user'
? { ? {
content: [ content: [
{ {
@ -656,12 +670,18 @@
} }
} }
} else { } else {
toast.error(`Uh-oh! There was an issue connecting to ${model}.`); toast.error(
responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { provider: model })
);
responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model
});
} }
responseMessage.error = true; responseMessage.error = true;
responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model
});
responseMessage.done = true; responseMessage.done = true;
messages = messages; messages = messages;
} }
@ -675,8 +695,13 @@
if (messages.length == 2) { if (messages.length == 2) {
window.history.replaceState(history.state, '', `/c/${_chatId}`); window.history.replaceState(history.state, '', `/c/${_chatId}`);
if ($settings?.titleAutoGenerateModel) {
await generateChatTitle(_chatId, userPrompt);
} else {
await setChatTitle(_chatId, userPrompt); await setChatTitle(_chatId, userPrompt);
} }
}
}; };
const stopResponse = () => { const stopResponse = () => {
@ -725,7 +750,7 @@
); );
} }
} else { } else {
toast.error(`Model ${modelId} not found`); toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
} }
}; };
@ -734,7 +759,9 @@
const title = await generateTitle( const title = await generateTitle(
localStorage.token, localStorage.token,
$settings?.titleGenerationPrompt ?? $settings?.titleGenerationPrompt ??
"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': {{prompt}}", $i18n.t(
"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':"
) + ' {{prompt}}',
$settings?.titleAutoGenerateModel ?? selectedModels[0], $settings?.titleAutoGenerateModel ?? selectedModels[0],
userPrompt userPrompt
); );

View file

@ -2,7 +2,7 @@
import { WEBUI_API_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, config, user } from '$lib/stores'; import { WEBUI_NAME, config, user } from '$lib/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@ -11,6 +11,8 @@
import EditUserModal from '$lib/components/admin/EditUserModal.svelte'; import EditUserModal from '$lib/components/admin/EditUserModal.svelte';
import SettingsModal from '$lib/components/admin/SettingsModal.svelte'; import SettingsModal from '$lib/components/admin/SettingsModal.svelte';
const i18n = getContext('i18n');
let loaded = false; let loaded = false;
let users = []; let users = [];
@ -37,7 +39,7 @@
}); });
if (res) { if (res) {
users = await getUsers(localStorage.token); users = await getUsers(localStorage.token);
toast.success('Successfully updated'); toast.success($i18n.t('Successfully updated.'));
} }
}; };
@ -62,9 +64,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title> <title>{$i18n.t('Admin Panel')} | {$WEBUI_NAME}</title>
{`Admin Panel | ${$WEBUI_NAME}`}
</title>
</svelte:head> </svelte:head>
{#key selectedUser} {#key selectedUser}
@ -88,7 +88,7 @@
<div class=" flex flex-col justify-center"> <div class=" flex flex-col justify-center">
<div class=" flex justify-between items-center"> <div class=" flex justify-between items-center">
<div class="flex items-center text-2xl font-semibold"> <div class="flex items-center text-2xl font-semibold">
All Users {$i18n.t('All Users')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" /> <div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300" <span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{users.length}</span >{users.length}</span
@ -115,12 +115,12 @@
/> />
</svg> </svg>
<div class=" text-xs">Admin Settings</div> <div class=" text-xs">{$i18n.t('Admin Settings')}</div>
</button> </button>
</div> </div>
</div> </div>
<div class=" text-gray-500 text-xs mt-1"> <div class=" text-gray-500 text-xs mt-1">
ⓘ Click on the user role button to change a user's role. {$i18n.t("Click on the user role button to change a user's role.")}
</div> </div>
<hr class=" my-3 dark:border-gray-600" /> <hr class=" my-3 dark:border-gray-600" />
@ -131,10 +131,10 @@
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400" class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"
> >
<tr> <tr>
<th scope="col" class="px-3 py-2"> Role </th> <th scope="col" class="px-3 py-2"> {$i18n.t('Role')} </th>
<th scope="col" class="px-3 py-2"> Name </th> <th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
<th scope="col" class="px-3 py-2"> Email </th> <th scope="col" class="px-3 py-2"> {$i18n.t('Email')} </th>
<th scope="col" class="px-3 py-2"> Action </th> <th scope="col" class="px-3 py-2"> {$i18n.t('Action')} </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -163,7 +163,7 @@
'bg-green-600 dark:bg-green-300'} {user.role === 'pending' && 'bg-green-600 dark:bg-green-300'} {user.role === 'pending' &&
'bg-gray-600 dark:bg-gray-300'}" 'bg-gray-600 dark:bg-gray-300'}"
/> />
{user.role}</button {$i18n.t(user.role)}</button
> >
</td> </td>
<td class="px-3 py-2 font-medium text-gray-900 dark:text-white w-max"> <td class="px-3 py-2 font-medium text-gray-900 dark:text-white w-max">

View file

@ -2,7 +2,7 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -40,6 +40,8 @@
import { RAGTemplate } from '$lib/utils/rag'; import { RAGTemplate } from '$lib/utils/rag';
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n');
let loaded = false; let loaded = false;
let stopResponseFlag = false; let stopResponseFlag = false;
@ -160,7 +162,9 @@
}; };
const scrollToBottom = () => { const scrollToBottom = () => {
if (messagesContainerElement) {
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight; messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
}
}; };
////////////////////////// //////////////////////////
@ -171,7 +175,7 @@
console.log('submitPrompt', $chatId); console.log('submitPrompt', $chatId);
if (selectedModels.includes('')) { if (selectedModels.includes('')) {
toast.error('Model not selected'); toast.error($i18n.t('Model not selected'));
} else if (messages.length != 0 && messages.at(-1).done != true) { } else if (messages.length != 0 && messages.at(-1).done != true) {
// Response not done // Response not done
console.log('wait'); console.log('wait');
@ -217,7 +221,7 @@
if ($settings.saveChatHistory ?? true) { if ($settings.saveChatHistory ?? true) {
chat = await createNewChat(localStorage.token, { chat = await createNewChat(localStorage.token, {
id: $chatId, id: $chatId,
title: 'New Chat', title: $i18n.t('New Chat'),
models: selectedModels, models: selectedModels,
system: $settings.system ?? undefined, system: $settings.system ?? undefined,
options: { options: {
@ -280,7 +284,7 @@
await sendPromptOllama(model, prompt, responseMessageId, _chatId); await sendPromptOllama(model, prompt, responseMessageId, _chatId);
} }
} else { } else {
toast.error(`Model ${modelId} not found`); toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
} }
}) })
); );
@ -321,7 +325,7 @@
.map((file) => file.url.slice(file.url.indexOf(',') + 1)); .map((file) => file.url.slice(file.url.indexOf(',') + 1));
// Add images array only if it contains elements // Add images array only if it contains elements
if (imageUrls && imageUrls.length > 0) { if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
baseMessage.images = imageUrls; baseMessage.images = imageUrls;
} }
@ -492,12 +496,18 @@
responseMessage.content = error.error; responseMessage.content = error.error;
} }
} else { } else {
toast.error(`Uh-oh! There was an issue connecting to Ollama.`); toast.error(
responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`; $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { provider: 'Ollama' })
);
responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: 'Ollama'
});
} }
responseMessage.error = true; responseMessage.error = true;
responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`; responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: 'Ollama'
});
responseMessage.done = true; responseMessage.done = true;
messages = messages; messages = messages;
} }
@ -545,7 +555,8 @@
.filter((message) => message) .filter((message) => message)
.map((message, idx, arr) => ({ .map((message, idx, arr) => ({
role: message.role, role: message.role,
...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false ...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
message.role === 'user'
? { ? {
content: [ content: [
{ {
@ -669,12 +680,18 @@
} }
} }
} else { } else {
toast.error(`Uh-oh! There was an issue connecting to ${model}.`); toast.error(
responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { provider: model })
);
responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model
});
} }
responseMessage.error = true; responseMessage.error = true;
responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model
});
responseMessage.done = true; responseMessage.done = true;
messages = messages; messages = messages;
} }
@ -688,8 +705,13 @@
if (messages.length == 2) { if (messages.length == 2) {
window.history.replaceState(history.state, '', `/c/${_chatId}`); window.history.replaceState(history.state, '', `/c/${_chatId}`);
if ($settings?.titleAutoGenerateModel) {
await generateChatTitle(_chatId, userPrompt);
} else {
await setChatTitle(_chatId, userPrompt); await setChatTitle(_chatId, userPrompt);
} }
}
}; };
const stopResponse = () => { const stopResponse = () => {
@ -725,7 +747,7 @@
); );
} }
} else { } else {
toast.error(`Model ${modelId} not found`); toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
} }
}; };
@ -747,7 +769,9 @@
const title = await generateTitle( const title = await generateTitle(
localStorage.token, localStorage.token,
$settings?.titleGenerationPrompt ?? $settings?.titleGenerationPrompt ??
"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': {{prompt}}", $i18n.t(
"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':"
) + ' {{prompt}}',
$settings?.titleAutoGenerateModel ?? selectedModels[0], $settings?.titleAutoGenerateModel ?? selectedModels[0],
userPrompt userPrompt
); );

View file

@ -3,7 +3,7 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, documents } from '$lib/stores'; import { WEBUI_NAME, documents } from '$lib/stores';
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents'; import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
@ -17,6 +17,9 @@
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte'; import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
import SettingsModal from '$lib/components/documents/SettingsModal.svelte'; import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
import AddDocModal from '$lib/components/documents/AddDocModal.svelte'; import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
const i18n = getContext('i18n');
let importFiles = ''; let importFiles = '';
let inputFiles = ''; let inputFiles = '';
@ -120,7 +123,7 @@
} }
} }
} else { } else {
toast.error(`File not found.`); toast.error($i18n.t(`File not found.`));
} }
} }
@ -149,9 +152,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title> <title>{$i18n.t('Documents')} | {$WEBUI_NAME}</title>
{`Documents | ${$WEBUI_NAME}`}
</title>
</svelte:head> </svelte:head>
{#if dragged} {#if dragged}
@ -188,7 +189,7 @@
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> <div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
<div class="mb-6"> <div class="mb-6">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class=" text-2xl font-semibold self-center">My Documents</div> <div class=" text-2xl font-semibold self-center">{$i18n.t('My Documents')}</div>
<div> <div>
<button <button
@ -211,12 +212,12 @@
/> />
</svg> </svg>
<div class=" text-xs">Document Settings</div> <div class=" text-xs">{$i18n.t('Document Settings')}</div>
</button> </button>
</div> </div>
</div> </div>
<div class=" text-gray-500 text-xs mt-1"> <div class=" text-gray-500 text-xs mt-1">
ⓘ Use '#' in the prompt input to load and select your documents. {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
</div> </div>
</div> </div>
@ -239,7 +240,7 @@
<input <input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={query} bind:value={query}
placeholder="Search Document" placeholder={$i18n.t('Search Documents')}
/> />
</div> </div>
@ -274,7 +275,7 @@
on:dragleave={onDragLeave} on:dragleave={onDragLeave}
> >
<div class=" pointer-events-none"> <div class=" pointer-events-none">
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div> <div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full"> <div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to my documents Drop any files here to add to my documents
@ -314,7 +315,7 @@
// await chats.set(await getChatListByTagName(localStorage.token, tag.name)); // await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}} }}
> >
<div class=" text-xs font-medium self-center line-clamp-1">all</div> <div class=" text-xs font-medium self-center line-clamp-1">{$i18n.t('all')}</div>
</button> </button>
{#each tags as tag} {#each tags as tag}
@ -344,7 +345,7 @@
// await chats.set(await getChatListByTagName(localStorage.token, tag.name)); // await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}} }}
> >
<div class=" text-xs font-medium self-center line-clamp-1">add tags</div> <div class=" text-xs font-medium self-center line-clamp-1">{$i18n.t('add tags')}</div>
</button> --> </button> -->
<button <button
@ -354,7 +355,9 @@
// await chats.set(await getChatListByTagName(localStorage.token, tag.name)); // await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}} }}
> >
<div class=" text-xs font-medium self-center line-clamp-1">delete</div> <div class=" text-xs font-medium self-center line-clamp-1">
{$i18n.t('delete')}
</div>
</button> </button>
</div> </div>
</div> </div>
@ -563,7 +566,7 @@
documentsImportInputElement.click(); documentsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium">Import Documents Mapping</div> <div class=" self-center mr-2 font-medium">{$i18n.t('Import Documents Mapping')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
@ -590,7 +593,7 @@
saveAs(blob, `documents-mapping-export-${Date.now()}.json`); saveAs(blob, `documents-mapping-export-${Date.now()}.json`);
}} }}
> >
<div class=" self-center mr-2 font-medium">Export Documents Mapping</div> <div class=" self-center mr-2 font-medium">{$i18n.t('Export Documents Mapping')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg

View file

@ -3,7 +3,7 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, modelfiles, settings, user } from '$lib/stores'; import { WEBUI_NAME, modelfiles, settings, user } from '$lib/stores';
import { createModel, deleteModel } from '$lib/apis/ollama'; import { createModel, deleteModel } from '$lib/apis/ollama';
@ -14,6 +14,8 @@
} from '$lib/apis/modelfiles'; } from '$lib/apis/modelfiles';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
const i18n = getContext('i18n');
let localModelfiles = []; let localModelfiles = [];
let importFiles; let importFiles;
let modelfilesImportInputElement: HTMLInputElement; let modelfilesImportInputElement: HTMLInputElement;
@ -26,7 +28,7 @@
}); });
if (success) { if (success) {
toast.success(`Deleted ${tagName}`); toast.success($i18n.t(`Deleted {tagName}`, { tagName }));
} }
return success; return success;
@ -39,7 +41,7 @@
}; };
const shareModelfile = async (modelfile) => { const shareModelfile = async (modelfile) => {
toast.success('Redirecting you to OpenWebUI Community'); toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com'; const url = 'https://openwebui.com';
@ -74,14 +76,14 @@
<svelte:head> <svelte:head>
<title> <title>
{`Modelfiles | ${$WEBUI_NAME}`} {$i18n.t('Modelfiles')} | {$WEBUI_NAME}
</title> </title>
</svelte:head> </svelte:head>
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white"> <div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
<div class="flex flex-col justify-between w-full overflow-y-auto"> <div class="flex flex-col justify-between w-full overflow-y-auto">
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> <div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
<div class=" text-2xl font-semibold mb-3">My Modelfiles</div> <div class=" text-2xl font-semibold mb-3">{$i18n.t('My Modelfiles')}</div>
<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/modelfiles/create"> <a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/modelfiles/create">
<div class=" self-center w-10"> <div class=" self-center w-10">
@ -104,8 +106,8 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold">Create a modelfile</div> <div class=" font-bold">{$i18n.t('Create a modelfile')}</div>
<div class=" text-sm">Customize Ollama models for a specific purpose</div> <div class=" text-sm">{$i18n.t('Customize Ollama models for a specific purpose')}</div>
</div> </div>
</a> </a>
@ -270,7 +272,7 @@
modelfilesImportInputElement.click(); modelfilesImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium">Import Modelfiles</div> <div class=" self-center mr-2 font-medium">{$i18n.t('Import Modelfiles')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
@ -294,7 +296,7 @@
saveModelfiles($modelfiles); saveModelfiles($modelfiles);
}} }}
> >
<div class=" self-center mr-2 font-medium">Export Modelfiles</div> <div class=" self-center mr-2 font-medium">{$i18n.t('Export Modelfiles')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
@ -335,7 +337,7 @@
await modelfiles.set(await getModelfiles(localStorage.token)); await modelfiles.set(await getModelfiles(localStorage.token));
}} }}
> >
<div class=" self-center mr-2 font-medium">Sync All</div> <div class=" self-center mr-2 font-medium">{$i18n.t('Sync All')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
@ -386,7 +388,7 @@
</div> </div>
<div class=" my-16"> <div class=" my-16">
<div class=" text-2xl font-semibold mb-3">Made by OpenWebUI Community</div> <div class=" text-2xl font-semibold mb-3">{$i18n.t('Made by OpenWebUI Community')}</div>
<a <a
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
@ -413,8 +415,8 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold">Discover a modelfile</div> <div class=" font-bold">{$i18n.t('Discover a modelfile')}</div>
<div class=" text-sm">Discover, download, and explore model presets</div> <div class=" text-sm">{$i18n.t('Discover, download, and explore model presets')}</div>
</div> </div>
</a> </a>
</div> </div>

View file

@ -6,10 +6,12 @@
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte'; import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { createModel } from '$lib/apis/ollama'; import { createModel } from '$lib/apis/ollama';
import { createNewModelfile, getModelfileByTagName, getModelfiles } from '$lib/apis/modelfiles'; import { createNewModelfile, getModelfileByTagName, getModelfiles } from '$lib/apis/modelfiles';
const i18n = getContext('i18n');
let loading = false; let loading = false;
let filesInputElement; let filesInputElement;
@ -349,7 +351,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
}} }}
/> />
<div class=" text-2xl font-semibold mb-6">My Modelfiles</div> <div class=" text-2xl font-semibold mb-6">{$i18n.t('My Modelfiles')}</div>
<button <button
class="flex space-x-1" class="flex space-x-1"
@ -371,7 +373,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
/> />
</svg> </svg>
</div> </div>
<div class=" self-center font-medium text-sm">Back</div> <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
</button> </button>
<hr class="my-3 dark:border-gray-700" /> <hr class="my-3 dark:border-gray-700" />
@ -418,12 +420,12 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
<div class="my-2 flex space-x-2"> <div class="my-2 flex space-x-2">
<div class="flex-1"> <div class="flex-1">
<div class=" text-sm font-semibold mb-2">Name*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Name')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
placeholder="Name your modelfile" placeholder={$i18n.t('Name your modelfile')}
bind:value={title} bind:value={title}
required required
/> />
@ -431,12 +433,12 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class=" text-sm font-semibold mb-2">Model Tag Name*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Model Tag Name')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
placeholder="Add a model tag name" placeholder={$i18n.t('Add a model tag name')}
bind:value={tagName} bind:value={tagName}
required required
/> />
@ -445,12 +447,12 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
</div> </div>
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Description*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Description')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
placeholder="Add a short description about what this modelfile does" placeholder={$i18n.t('Add a short description about what this modelfile does')}
bind:value={desc} bind:value={desc}
required required
/> />
@ -459,7 +461,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
<div class="my-2"> <div class="my-2">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-sm font-semibold">Modelfile</div> <div class=" self-center text-sm font-semibold">{$i18n.t('Modelfile')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -469,9 +471,9 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
}} }}
> >
{#if raw} {#if raw}
<span class="ml-2 self-center"> Raw Format </span> <span class="ml-2 self-center"> {$i18n.t('Raw Format')} </span>
{:else} {:else}
<span class="ml-2 self-center"> Builder Mode </span> <span class="ml-2 self-center"> {$i18n.t('Builder Mode')} </span>
{/if} {/if}
</button> </button>
</div> </div>
@ -480,7 +482,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
{#if raw} {#if raw}
<div class="mt-2"> <div class="mt-2">
<div class=" text-xs font-semibold mb-2">Content*</div> <div class=" text-xs font-semibold mb-2">{$i18n.t('Content')}*</div>
<div> <div>
<textarea <textarea
@ -493,12 +495,13 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
</div> </div>
<div class="text-xs text-gray-400 dark:text-gray-500"> <div class="text-xs text-gray-400 dark:text-gray-500">
Not sure what to write? Switch to <button {$i18n.t('Not sure what to write? Switch to')}
<button
class="text-gray-500 dark:text-gray-300 font-medium cursor-pointer" class="text-gray-500 dark:text-gray-300 font-medium cursor-pointer"
type="button" type="button"
on:click={() => { on:click={() => {
raw = !raw; raw = !raw;
}}>Builder Mode</button }}>{$i18n.t('Builder Mode')}</button
> >
or or
<a <a
@ -506,13 +509,13 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
href="https://openwebui.com" href="https://openwebui.com"
target="_blank" target="_blank"
> >
Click here to check other modelfiles. {$i18n.t('Click here to check other modelfiles.')}
</a> </a>
</div> </div>
</div> </div>
{:else} {:else}
<div class="my-2"> <div class="my-2">
<div class=" text-xs font-semibold mb-2">From (Base Model)*</div> <div class=" text-xs font-semibold mb-2">{$i18n.t('From (Base Model)')}*</div>
<div> <div>
<input <input
@ -524,16 +527,17 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
</div> </div>
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
To access the available model names for downloading, <a {$i18n.t('To access the available model names for downloading,')}
<a
class=" text-gray-500 dark:text-gray-300 font-medium" class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://ollama.com/library" href="https://ollama.com/library"
target="_blank">click here.</a target="_blank">{$i18n.t('click here.')}</a
> >
</div> </div>
</div> </div>
<div class="my-1"> <div class="my-1">
<div class=" text-xs font-semibold mb-2">System Prompt</div> <div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div>
<div> <div>
<textarea <textarea
@ -546,7 +550,9 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
</div> </div>
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-sm font-semibold">Modelfile Advanced Settings</div> <div class=" self-center text-sm font-semibold">
{$i18n.t('Modelfile Advanced Settings')}
</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -556,16 +562,16 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
}} }}
> >
{#if advanced} {#if advanced}
<span class="ml-2 self-center"> Custom </span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{:else} {:else}
<span class="ml-2 self-center"> Default </span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if advanced} {#if advanced}
<div class="my-2"> <div class="my-2">
<div class=" text-xs font-semibold mb-2">Template</div> <div class=" text-xs font-semibold mb-2">{$i18n.t('Template')}</div>
<div> <div>
<textarea <textarea
@ -578,7 +584,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
</div> </div>
<div class="my-2"> <div class="my-2">
<div class=" text-xs font-semibold mb-2">Parameters</div> <div class=" text-xs font-semibold mb-2">{$i18n.t('Parameters')}</div>
<div> <div>
<AdvancedParams bind:options /> <AdvancedParams bind:options />
@ -590,7 +596,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
<div class="my-2"> <div class="my-2">
<div class="flex w-full justify-between mb-2"> <div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">Prompt suggestions</div> <div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -618,7 +624,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
<div class=" flex border dark:border-gray-600 rounded-lg"> <div class=" flex border dark:border-gray-600 rounded-lg">
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600" class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder="Write a prompt suggestion (e.g. Who are you?)" placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')}
bind:value={prompt.content} bind:value={prompt.content}
/> />
@ -647,7 +653,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
</div> </div>
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Categories</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Categories')}</div>
<div class="grid grid-cols-4"> <div class="grid grid-cols-4">
{#each Object.keys(categories) as category} {#each Object.keys(categories) as category}
@ -661,7 +667,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
{#if pullProgress !== null} {#if pullProgress !== null}
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Pull Progress</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Pull Progress')}</div>
<div class="w-full rounded-full dark:bg-gray-800"> <div class="w-full rounded-full dark:bg-gray-800">
<div <div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
@ -684,7 +690,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
type="submit" type="submit"
disabled={loading} disabled={loading}
> >
<div class=" self-center font-medium">Save & Create</div> <div class=" self-center font-medium">{$i18n.t('Save & Create')}</div>
{#if loading} {#if loading}
<div class="ml-1.5 self-center"> <div class="ml-1.5 self-center">

View file

@ -3,7 +3,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { settings, user, config, modelfiles } from '$lib/stores'; import { settings, user, config, modelfiles } from '$lib/stores';
@ -14,6 +14,8 @@
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte'; import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
const i18n = getContext('i18n');
let loading = false; let loading = false;
let filesInputElement; let filesInputElement;
@ -248,7 +250,7 @@
}} }}
/> />
<div class=" text-2xl font-semibold mb-6">My Modelfiles</div> <div class=" text-2xl font-semibold mb-6">{$i18n.t('My Modelfiles')}</div>
<button <button
class="flex space-x-1" class="flex space-x-1"
@ -270,7 +272,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center font-medium text-sm">Back</div> <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
</button> </button>
<hr class="my-3 dark:border-gray-700" /> <hr class="my-3 dark:border-gray-700" />
@ -317,12 +319,12 @@
<div class="my-2 flex space-x-2"> <div class="my-2 flex space-x-2">
<div class="flex-1"> <div class="flex-1">
<div class=" text-sm font-semibold mb-2">Name*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Name')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
placeholder="Name your modelfile" placeholder={$i18n.t('Name your modelfile')}
bind:value={title} bind:value={title}
required required
/> />
@ -330,12 +332,12 @@
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class=" text-sm font-semibold mb-2">Model Tag Name*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Model Tag Name')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent disabled:text-gray-500 border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent disabled:text-gray-500 border dark:border-gray-600 outline-none rounded-lg"
placeholder="Add a model tag name" placeholder={$i18n.t('Add a model tag name')}
value={tagName} value={tagName}
disabled disabled
required required
@ -345,12 +347,12 @@
</div> </div>
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Description*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Description')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
placeholder="Add a short description about what this modelfile does" placeholder={$i18n.t('Add a short description about what this modelfile does')}
bind:value={desc} bind:value={desc}
required required
/> />
@ -359,13 +361,13 @@
<div class="my-2"> <div class="my-2">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-sm font-semibold">Modelfile</div> <div class=" self-center text-sm font-semibold">{$i18n.t('Modelfile')}</div>
</div> </div>
<!-- <div class=" text-sm font-semibold mb-2"></div> --> <!-- <div class=" text-sm font-semibold mb-2"></div> -->
<div class="mt-2"> <div class="mt-2">
<div class=" text-xs font-semibold mb-2">Content*</div> <div class=" text-xs font-semibold mb-2">{$i18n.t('Content')}*</div>
<div> <div>
<textarea <textarea
@ -381,7 +383,7 @@
<div class="my-2"> <div class="my-2">
<div class="flex w-full justify-between mb-2"> <div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">Prompt suggestions</div> <div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
@ -409,7 +411,7 @@
<div class=" flex border dark:border-gray-600 rounded-lg"> <div class=" flex border dark:border-gray-600 rounded-lg">
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600" class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder="Write a prompt suggestion (e.g. Who are you?)" placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')}
bind:value={prompt.content} bind:value={prompt.content}
/> />
@ -438,7 +440,7 @@
</div> </div>
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Categories</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Categories')}</div>
<div class="grid grid-cols-4"> <div class="grid grid-cols-4">
{#each Object.keys(categories) as category} {#each Object.keys(categories) as category}
@ -453,7 +455,7 @@
{#if pullProgress !== null} {#if pullProgress !== null}
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Pull Progress</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Pull Progress')}</div>
<div class="w-full rounded-full dark:bg-gray-800"> <div class="w-full rounded-full dark:bg-gray-800">
<div <div
class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full" class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
@ -476,7 +478,7 @@
type="submit" type="submit"
disabled={loading} disabled={loading}
> >
<div class=" self-center font-medium">Save & Update</div> <div class=" self-center font-medium">{$i18n.t('Save & Update')}</div>
{#if loading} {#if loading}
<div class="ml-1.5 self-center"> <div class="ml-1.5 self-center">

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@ -19,6 +19,8 @@
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';
const i18n = getContext('i18n');
let mode = 'chat'; let mode = 'chat';
let loaded = false; let loaded = false;
let text = ''; let text = '';
@ -261,18 +263,19 @@
<svelte:head> <svelte:head>
<title> <title>
{`Playground | ${$WEBUI_NAME}`} {$i18n.t('Playground')} | {$WEBUI_NAME}
</title> </title>
</svelte:head> </svelte:head>
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white"> <div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
<div class=" flex flex-col justify-between w-full overflow-y-auto h-[100dvh]"> <div class=" flex flex-col justify-between w-full overflow-y-auto h-[100dvh]">
<div class="max-w-2xl mx-auto w-full px-3 p-3 md:px-0 h-full"> <div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10 h-full">
<div class=" flex flex-col h-full"> <div class=" flex flex-col h-full">
<div class="flex flex-col justify-between mb-2.5 gap-1"> <div class="flex flex-col justify-between mb-2.5 gap-1">
<div class="flex justify-between items-center gap-2"> <div class="flex justify-between items-center gap-2">
<div class=" text-2xl font-semibold self-center flex"> <div class=" text-2xl font-semibold self-center flex">
Playground <span class=" text-xs text-gray-500 self-center ml-1">(Beta)</span> {$i18n.t('Playground')}
<span class=" text-xs text-gray-500 self-center ml-1">{$i18n.t('(Beta)')}</span>
</div> </div>
<div> <div>
@ -289,9 +292,9 @@
}} }}
> >
{#if mode === 'complete'} {#if mode === 'complete'}
Text Completion {$i18n.t('Text Completion')}
{:else if mode === 'chat'} {:else if mode === 'chat'}
Chat {$i18n.t('Chat')}
{/if} {/if}
<div> <div>
@ -318,7 +321,9 @@
class="outline-none bg-transparent text-sm font-medium rounded-lg w-full placeholder-gray-400" class="outline-none bg-transparent text-sm font-medium rounded-lg w-full placeholder-gray-400"
bind:value={selectedModelId} bind:value={selectedModelId}
> >
<option class=" text-gray-800" value="" selected disabled>Select a model</option> <option class=" text-gray-800" value="" selected disabled
>{$i18n.t('Select a model')}</option
>
{#each $models as model} {#each $models as model}
{#if model.name === 'hr'} {#if model.name === 'hr'}
@ -363,12 +368,12 @@
{#if mode === 'chat'} {#if mode === 'chat'}
<div class="p-1"> <div class="p-1">
<div class="p-3 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg"> <div class="p-3 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg">
<div class=" text-sm font-medium">System</div> <div class=" text-sm font-medium">{$i18n.t('System')}</div>
<textarea <textarea
id="system-textarea" id="system-textarea"
class="w-full h-full bg-transparent resize-none outline-none text-sm" class="w-full h-full bg-transparent resize-none outline-none text-sm"
bind:value={system} bind:value={system}
placeholder="You're a helpful assistant." placeholder={$i18n.t("You're a helpful assistant.")}
rows="4" rows="4"
/> />
</div> </div>
@ -388,7 +393,7 @@
bind:this={textCompletionAreaElement} bind:this={textCompletionAreaElement}
class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm" class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm"
bind:value={text} bind:value={text}
placeholder="You're a helpful assistant." placeholder={$i18n.t("You're a helpful assistant.")}
/> />
{:else if mode === 'chat'} {:else if mode === 'chat'}
<ChatCompletion bind:messages /> <ChatCompletion bind:messages />
@ -405,7 +410,7 @@
submitHandler(); submitHandler();
}} }}
> >
Submit {$i18n.t('Submit')}
</button> </button>
{:else} {:else}
<button <button
@ -414,7 +419,7 @@
stopResponse(); stopResponse();
}} }}
> >
Cancel {$i18n.t('Cancel')}
</button> </button>
{/if} {/if}
</div> </div>

View file

@ -3,17 +3,19 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, prompts } from '$lib/stores'; import { WEBUI_NAME, prompts } from '$lib/stores';
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts'; import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
const i18n = getContext('i18n');
let importFiles = ''; let importFiles = '';
let query = ''; let query = '';
let promptsImportInputElement: HTMLInputElement; let promptsImportInputElement: HTMLInputElement;
const sharePrompt = async (prompt) => { const sharePrompt = async (prompt) => {
toast.success('Redirecting you to OpenWebUI Community'); toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com'; const url = 'https://openwebui.com';
@ -38,7 +40,7 @@
<svelte:head> <svelte:head>
<title> <title>
{`Prompts | ${$WEBUI_NAME}`} {$i18n.t('Prompts')} | {$WEBUI_NAME}
</title> </title>
</svelte:head> </svelte:head>
@ -46,7 +48,7 @@
<div class="flex flex-col justify-between w-full overflow-y-auto"> <div class="flex flex-col justify-between w-full overflow-y-auto">
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> <div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
<div class="mb-6 flex justify-between items-center"> <div class="mb-6 flex justify-between items-center">
<div class=" text-2xl font-semibold self-center">My Prompts</div> <div class=" text-2xl font-semibold self-center">{$i18n.t('My Prompts')}</div>
</div> </div>
<div class=" flex w-full space-x-2"> <div class=" flex w-full space-x-2">
@ -68,7 +70,7 @@
<input <input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={query} bind:value={query}
placeholder="Search Prompt" placeholder={$i18n.t('Search Prompts')}
/> />
</div> </div>
@ -246,7 +248,7 @@
promptsImportInputElement.click(); promptsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium">Import Prompts</div> <div class=" self-center mr-2 font-medium">{$i18n.t('Import Prompts')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
@ -274,7 +276,7 @@
saveAs(blob, `prompts-export-${Date.now()}.json`); saveAs(blob, `prompts-export-${Date.now()}.json`);
}} }}
> >
<div class=" self-center mr-2 font-medium">Export Prompts</div> <div class=" self-center mr-2 font-medium">{$i18n.t('Export Prompts')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
@ -303,7 +305,7 @@
</div> </div>
<div class=" my-16"> <div class=" my-16">
<div class=" text-2xl font-semibold mb-3">Made by OpenWebUI Community</div> <div class=" text-2xl font-semibold mb-3">{$i18n.t('Made by OpenWebUI Community')}</div>
<a <a
class=" flex space-x-4 cursor-pointer w-full mb-3 px-3 py-2" class=" flex space-x-4 cursor-pointer w-full mb-3 px-3 py-2"
@ -330,8 +332,8 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold">Discover a prompt</div> <div class=" font-bold">{$i18n.t('Discover a prompt')}</div>
<div class=" text-sm">Discover, download, and explore custom prompts</div> <div class=" text-sm">{$i18n.t('Discover, download, and explore custom prompts')}</div>
</div> </div>
</a> </a>
</div> </div>

View file

@ -3,10 +3,12 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { prompts } from '$lib/stores'; import { prompts } from '$lib/stores';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { createNewPrompt, getPrompts } from '$lib/apis/prompts'; import { createNewPrompt, getPrompts } from '$lib/apis/prompts';
const i18n = getContext('i18n');
let loading = false; let loading = false;
// /////////// // ///////////
@ -36,7 +38,9 @@
await goto('/prompts'); await goto('/prompts');
} }
} else { } else {
toast.error('Only alphanumeric characters and hyphens are allowed in the command string.'); toast.error(
$i18n.t('Only alphanumeric characters and hyphens are allowed in the command string.')
);
} }
loading = false; loading = false;
@ -92,7 +96,7 @@
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white"> <div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
<div class=" flex flex-col justify-between w-full overflow-y-auto"> <div class=" flex flex-col justify-between w-full overflow-y-auto">
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> <div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
<div class=" text-2xl font-semibold mb-6">My Prompts</div> <div class=" text-2xl font-semibold mb-6">{$i18n.t('My Prompts')}</div>
<button <button
class="flex space-x-1" class="flex space-x-1"
@ -114,7 +118,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center font-medium text-sm">Back</div> <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
</button> </button>
<hr class="my-3 dark:border-gray-700" /> <hr class="my-3 dark:border-gray-700" />
@ -125,12 +129,12 @@
}} }}
> >
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Title*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Title')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
placeholder="Add a short title for this prompt" placeholder={$i18n.t('Add a short title for this prompt')}
bind:value={title} bind:value={title}
required required
/> />
@ -138,7 +142,7 @@
</div> </div>
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Command*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Command')}*</div>
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
<div <div
@ -148,34 +152,38 @@
</div> </div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-r-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-r-lg"
placeholder="short-summary" placeholder={$i18n.t('short-summary')}
bind:value={command} bind:value={command}
required required
/> />
</div> </div>
<div class="text-xs text-gray-400 dark:text-gray-500"> <div class="text-xs text-gray-400 dark:text-gray-500">
Only <span class=" text-gray-600 dark:text-gray-300 font-medium" {$i18n.t('Only')}
>alphanumeric characters and hyphens</span <span class=" text-gray-600 dark:text-gray-300 font-medium"
>{$i18n.t('alphanumeric characters and hyphens')}</span
> >
are allowed; Activate this command by typing "<span {$i18n.t('are allowed - Activate this command by typing')}&nbsp;"<span
class=" text-gray-600 dark:text-gray-300 font-medium" class=" text-gray-600 dark:text-gray-300 font-medium"
> >
/{command} /{command}
</span>" to chat input. </span>" &nbsp;
{$i18n.t('to chat input.')}
</div> </div>
</div> </div>
<div class="my-2"> <div class="my-2">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-sm font-semibold">Prompt Content*</div> <div class=" self-center text-sm font-semibold">{$i18n.t('Prompt Content')}*</div>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div> <div>
<textarea <textarea
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
placeholder={`Write a summary in 50 words that summarizes [topic or keyword].`} placeholder={$i18n.t(
'Write a summary in 50 words that summarizes [topic or keyword].'
)}
rows="6" rows="6"
bind:value={content} bind:value={content}
required required
@ -183,18 +191,20 @@
</div> </div>
<div class="text-xs text-gray-400 dark:text-gray-500"> <div class="text-xs text-gray-400 dark:text-gray-500">
ⓘ Format your variables using square brackets like this: <span {$i18n.t('Format your variables using square brackets like this:')}&nbsp;<span
class=" text-gray-600 dark:text-gray-300 font-medium">[variable]</span class=" text-gray-600 dark:text-gray-300 font-medium">[{$i18n.t('variable')}]</span
> >.
. Make sure to enclose them with {$i18n.t('Make sure to enclose them with')}
<span class=" text-gray-600 dark:text-gray-300 font-medium">'['</span> <span class=" text-gray-600 dark:text-gray-300 font-medium">'['</span>
and <span class=" text-gray-600 dark:text-gray-300 font-medium">']'</span>. {$i18n.t('and')}
<span class=" text-gray-600 dark:text-gray-300 font-medium">']'</span>.
</div> </div>
<div class="text-xs text-gray-400 dark:text-gray-500"> <div class="text-xs text-gray-400 dark:text-gray-500">
Utilize <span class=" text-gray-600 dark:text-gray-300 font-medium" {$i18n.t('Utilize')}<span class=" text-gray-600 dark:text-gray-300 font-medium">
>{`{{CLIPBOARD}}`}</span {` {{CLIPBOARD}}`}</span
> variable to have them replaced with clipboard content. >
{$i18n.t('variable to have them replaced with clipboard content.')}
</div> </div>
</div> </div>
</div> </div>
@ -207,7 +217,7 @@
type="submit" type="submit"
disabled={loading} disabled={loading}
> >
<div class=" self-center font-medium">Save & Create</div> <div class=" self-center font-medium">{$i18n.t('Save & Create')}</div>
{#if loading} {#if loading}
<div class="ml-1.5 self-center"> <div class="ml-1.5 self-center">

View file

@ -3,7 +3,9 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { prompts } from '$lib/stores'; import { prompts } from '$lib/stores';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
const i18n = getContext('i18n');
import { getPrompts, updatePromptByCommand } from '$lib/apis/prompts'; import { getPrompts, updatePromptByCommand } from '$lib/apis/prompts';
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -34,7 +36,9 @@
await goto('/prompts'); await goto('/prompts');
} }
} else { } else {
toast.error('Only alphanumeric characters and hyphens are allowed in the command string.'); toast.error(
$i18n.t('Only alphanumeric characters and hyphens are allowed in the command string.')
);
} }
loading = false; loading = false;
@ -74,7 +78,7 @@
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white"> <div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
<div class="flex flex-col justify-between w-full overflow-y-auto"> <div class="flex flex-col justify-between w-full overflow-y-auto">
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> <div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
<div class=" text-2xl font-semibold mb-6">My Prompts</div> <div class=" text-2xl font-semibold mb-6">{$i18n.t('My Prompts')}</div>
<button <button
class="flex space-x-1" class="flex space-x-1"
@ -96,7 +100,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center font-medium text-sm">Back</div> <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
</button> </button>
<hr class="my-3 dark:border-gray-700" /> <hr class="my-3 dark:border-gray-700" />
@ -107,12 +111,12 @@
}} }}
> >
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Title*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Title')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
placeholder="Add a short title for this prompt" placeholder={$i18n.t('Add a short title for this prompt')}
bind:value={title} bind:value={title}
required required
/> />
@ -120,7 +124,7 @@
</div> </div>
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">Command*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Command')}*</div>
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
<div <div
@ -138,27 +142,31 @@
</div> </div>
<div class="text-xs text-gray-400 dark:text-gray-500"> <div class="text-xs text-gray-400 dark:text-gray-500">
Only <span class=" text-gray-600 dark:text-gray-300 font-medium" {$i18n.t('Only')}
>alphanumeric characters and hyphens</span <span class=" text-gray-600 dark:text-gray-300 font-medium"
>{$i18n.t('alphanumeric characters and hyphens')}</span
> >
are allowed; Activate this command by typing "<span {$i18n.t('are allowed - Activate this command by typing')}&nbsp;"<span
class=" text-gray-600 dark:text-gray-300 font-medium" class=" text-gray-600 dark:text-gray-300 font-medium"
> >
/{command} /{command}
</span>" to chat input. </span>" &nbsp;
{$i18n.t('to chat input.')}
</div> </div>
</div> </div>
<div class="my-2"> <div class="my-2">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-sm font-semibold">Prompt Content*</div> <div class=" self-center text-sm font-semibold">{$i18n.t('Prompt Content')}*</div>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div> <div>
<textarea <textarea
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
placeholder={`Write a summary in 50 words that summarizes [topic or keyword].`} placeholder={$i18n.t(
`Write a summary in 50 words that summarizes [topic or keyword].`
)}
rows="6" rows="6"
bind:value={content} bind:value={content}
required required
@ -166,12 +174,20 @@
</div> </div>
<div class="text-xs text-gray-400 dark:text-gray-500"> <div class="text-xs text-gray-400 dark:text-gray-500">
Format your variables using square brackets like this: <span {$i18n.t('Format your variables using square brackets like this:')}&nbsp;<span
class=" text-gray-600 dark:text-gray-300 font-medium">[variable]</span class=" text-gray-600 dark:text-gray-300 font-medium">[{$i18n.t('variable')}]</span
> >.
. Make sure to enclose them with {$i18n.t('Make sure to enclose them with')}
<span class=" text-gray-600 dark:text-gray-300 font-medium">'['</span> <span class=" text-gray-600 dark:text-gray-300 font-medium">'['</span>
and <span class=" text-gray-600 dark:text-gray-300 font-medium">']'</span> . {$i18n.t('and')}
<span class=" text-gray-600 dark:text-gray-300 font-medium">']'</span>.
</div>
<div class="text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Utilize')}<span class=" text-gray-600 dark:text-gray-300 font-medium">
{` {{CLIPBOARD}}`}</span
>
{$i18n.t('variable to have them replaced with clipboard content.')}
</div> </div>
</div> </div>
</div> </div>
@ -184,7 +200,7 @@
type="submit" type="submit"
disabled={loading} disabled={loading}
> >
<div class=" self-center font-medium">Save & Update</div> <div class=" self-center font-medium">{$i18n.t('Save & Update')}</div>
{#if loading} {#if loading}
<div class="ml-1.5 self-center"> <div class="ml-1.5 self-center">

View file

@ -11,6 +11,9 @@
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';
setContext('i18n', i18n);
let loaded = false; let loaded = false;

View file

@ -3,9 +3,11 @@
import { userSignIn, userSignUp } from '$lib/apis/auths'; import { userSignIn, userSignUp } from '$lib/apis/auths';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, config, user } from '$lib/stores'; import { WEBUI_NAME, config, user } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
let loaded = false; let loaded = false;
let mode = 'signin'; let mode = 'signin';
@ -16,7 +18,7 @@
const setSessionUser = async (sessionUser) => { const setSessionUser = async (sessionUser) => {
if (sessionUser) { if (sessionUser) {
console.log(sessionUser); console.log(sessionUser);
toast.success(`You're now logged in.`); toast.success($i18n.t(`You're now logged in.`));
localStorage.token = sessionUser.token; localStorage.token = sessionUser.token;
await user.set(sessionUser); await user.set(sessionUser);
goto('/'); goto('/');
@ -96,26 +98,30 @@
}} }}
> >
<div class=" text-xl sm:text-2xl font-bold"> <div class=" text-xl sm:text-2xl font-bold">
{mode === 'signin' ? 'Sign in' : 'Sign up'} to {$WEBUI_NAME} {mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Sign up')}
{$i18n.t('to')}
{$WEBUI_NAME}
</div> </div>
{#if mode === 'signup'} {#if mode === 'signup'}
<div class=" mt-1 text-xs font-medium text-gray-500"> <div class=" mt-1 text-xs font-medium text-gray-500">
{$WEBUI_NAME} does not make any external connections, and your data stays securely on {$WEBUI_NAME}
your locally hosted server. {$i18n.t(
'does not make any external connections, and your data stays securely on your locally hosted server.'
)}
</div> </div>
{/if} {/if}
<div class="flex flex-col mt-4"> <div class="flex flex-col mt-4">
{#if mode === 'signup'} {#if mode === 'signup'}
<div> <div>
<div class=" text-sm font-semibold text-left mb-1">Name</div> <div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Name')}</div>
<input <input
bind:value={name} bind:value={name}
type="text" type="text"
class=" border px-4 py-2.5 rounded-2xl w-full text-sm" class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
autocomplete="name" autocomplete="name"
placeholder="Enter Your Full Name" placeholder={$i18n.t('Enter Your Full Name')}
required required
/> />
</div> </div>
@ -124,24 +130,24 @@
{/if} {/if}
<div class="mb-2"> <div class="mb-2">
<div class=" text-sm font-semibold text-left mb-1">Email</div> <div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Email')}</div>
<input <input
bind:value={email} bind:value={email}
type="email" type="email"
class=" border px-4 py-2.5 rounded-2xl w-full text-sm" class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
autocomplete="email" autocomplete="email"
placeholder="Enter Your Email" placeholder={$i18n.t('Enter Your Email')}
required required
/> />
</div> </div>
<div> <div>
<div class=" text-sm font-semibold text-left mb-1">Password</div> <div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Password')}</div>
<input <input
bind:value={password} bind:value={password}
type="password" type="password"
class=" border px-4 py-2.5 rounded-2xl w-full text-sm" class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
placeholder="Enter Your Password" placeholder={$i18n.t('Enter Your Password')}
autocomplete="current-password" autocomplete="current-password"
required required
/> />
@ -153,11 +159,13 @@
class=" bg-gray-900 hover:bg-gray-800 w-full rounded-full text-white font-semibold text-sm py-3 transition" class=" bg-gray-900 hover:bg-gray-800 w-full rounded-full text-white font-semibold text-sm py-3 transition"
type="submit" type="submit"
> >
{mode === 'signin' ? 'Sign In' : 'Create Account'} {mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Create Account')}
</button> </button>
<div class=" mt-4 text-sm text-center"> <div class=" mt-4 text-sm text-center">
{mode === 'signin' ? `Don't have an account?` : `Already have an account?`} {mode === 'signin'
? $i18n.t("Don't have an account?")
: $i18n.t('Already have an account?')}
<button <button
class=" font-medium underline" class=" font-medium underline"
@ -170,7 +178,7 @@
} }
}} }}
> >
{mode === 'signin' ? `Sign up` : `Sign In`} {mode === 'signin' ? $i18n.t('Sign up') : $i18n.t('Sign in')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,7 +1,9 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { WEBUI_NAME, config } from '$lib/stores'; import { WEBUI_NAME, config } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
let loaded = false; let loaded = false;
@ -19,22 +21,25 @@
<div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center"> <div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center">
<div class="m-auto pb-44 flex flex-col justify-center"> <div class="m-auto pb-44 flex flex-col justify-center">
<div class="max-w-md"> <div class="max-w-md">
<div class="text-center text-2xl font-medium z-50">{$WEBUI_NAME} Backend Required</div> <div class="text-center text-2xl font-medium z-50">
{$i18n.t('{{webUIName}} Backend Required', { webUIName: $WEBUI_NAME })}
</div>
<div class=" mt-4 text-center text-sm w-full"> <div class=" mt-4 text-center text-sm w-full">
Oops! You're using an unsupported method (frontend only). Please serve the WebUI from {$i18n.t(
the backend. "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend."
)}
<br class=" " /> <br class=" " />
<br class=" " /> <br class=" " />
<a <a
class=" font-semibold underline" class=" font-semibold underline"
href="https://github.com/open-webui/open-webui#how-to-install-" href="https://github.com/open-webui/open-webui#how-to-install-"
target="_blank">See readme.md for instructions</a target="_blank">{$i18n.t('See readme.md for instructions')}</a
> >
or {$i18n.t('or')}
<a class=" font-semibold underline" href="https://discord.gg/5rJgQTnV4s" target="_blank" <a class=" font-semibold underline" href="https://discord.gg/5rJgQTnV4s" target="_blank"
>join our Discord for help.</a >{$i18n.t('join our Discord for help.')}</a
> >
</div> </div>
@ -45,7 +50,7 @@
location.href = '/'; location.href = '/';
}} }}
> >
Check Again {$i18n.t('Check Again')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -3,16 +3,13 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
html { 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';
} }
pre { pre {
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',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
white-space: pre-wrap; white-space: pre-wrap;
} }
} }