Merge pull request #1555 from open-webui/dev

0.1.119
This commit is contained in:
Timothy Jaeryang Baek 2024-04-16 15:12:52 -07:00 committed by GitHub
commit 851754700a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1462 additions and 294 deletions

View file

@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.119] - 2024-04-16
### Added
- **🌟 Enhanced RAG Embedding Support**: Ollama, and OpenAI models can now be used for RAG embedding model.
- **🔄 Seamless Integration**: Copy 'ollama run <model name>' directly from Ollama page to easily select and pull models.
- **🏷️ Tagging Feature**: Add tags to chats directly via the sidebar chat menu.
- **📱 Mobile Accessibility**: Swipe left and right on mobile to effortlessly open and close the sidebar.
- **🔍 Improved Navigation**: Admin panel now supports pagination for user list.
- **🌍 Additional Language Support**: Added Polish language support.
### Fixed
- **🌍 Language Enhancements**: Vietnamese and Spanish translations have been improved.
- **🔧 Helm Fixes**: Resolved issues with Helm trailing slash and manifest.json.
### Changed
- **🐳 Docker Optimization**: Updated docker image build process to utilize 'uv' for significantly faster builds compared to 'pip3'.
## [0.1.118] - 2024-04-10
### Added

View file

@ -93,15 +93,16 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
# install python dependencies
COPY ./backend/requirements.txt ./requirements.txt
RUN if [ "$USE_CUDA" = "true" ]; then \
RUN pip3 install uv && \
if [ "$USE_CUDA" = "true" ]; then \
# If you use CUDA the whisper and embedding model will be downloaded on first use
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
pip3 install -r requirements.txt --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" && \
python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \
else \
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
pip3 install -r requirements.txt --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" && \
python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \
fi

View file

@ -28,6 +28,7 @@ from config import (
UPLOAD_DIR,
WHISPER_MODEL,
WHISPER_MODEL_DIR,
WHISPER_MODEL_AUTO_UPDATE,
DEVICE_TYPE,
)
@ -69,12 +70,24 @@ def transcribe(
f.write(contents)
f.close()
model = WhisperModel(
WHISPER_MODEL,
device=whisper_device_type,
compute_type="int8",
download_root=WHISPER_MODEL_DIR,
)
whisper_kwargs = {
"model_size_or_path": WHISPER_MODEL,
"device": whisper_device_type,
"compute_type": "int8",
"download_root": WHISPER_MODEL_DIR,
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
}
log.debug(f"whisper_kwargs: {whisper_kwargs}")
try:
model = WhisperModel(**whisper_kwargs)
except:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
)
whisper_kwargs["local_files_only"] = False
model = WhisperModel(**whisper_kwargs)
segments, info = model.transcribe(file_path, beam_size=5)
log.info(

View file

@ -29,7 +29,13 @@ import base64
import json
import logging
from config import SRC_LOG_LEVELS, CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL
from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
ENABLE_IMAGE_GENERATION,
AUTOMATIC1111_BASE_URL,
COMFYUI_BASE_URL,
)
log = logging.getLogger(__name__)
@ -48,7 +54,7 @@ app.add_middleware(
)
app.state.ENGINE = ""
app.state.ENABLED = False
app.state.ENABLED = ENABLE_IMAGE_GENERATION
app.state.OPENAI_API_KEY = ""
app.state.MODEL = ""

View file

@ -612,8 +612,13 @@ async def generate_embeddings(
user=Depends(get_current_user),
):
if url_idx == None:
if form_data.model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
@ -649,6 +654,60 @@ async def generate_embeddings(
)
def generate_ollama_embeddings(
form_data: GenerateEmbeddingsForm,
url_idx: Optional[int] = None,
):
log.info(f"generate_ollama_embeddings {form_data}")
if url_idx == None:
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
try:
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
data = r.json()
log.info(f"generate_ollama_embeddings {data}")
if "embedding" in data:
return data["embedding"]
else:
raise "Something went wrong :/"
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
error_detail = f"Ollama: {e}"
raise error_detail
class GenerateCompletionForm(BaseModel):
model: str
prompt: str
@ -672,8 +731,13 @@ async def generate_completion(
):
if url_idx == None:
if form_data.model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
@ -770,8 +834,13 @@ async def generate_chat_completion(
):
if url_idx == None:
if form_data.model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
@ -874,8 +943,13 @@ async def generate_openai_chat_completion(
):
if url_idx == None:
if form_data.model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,

View file

@ -39,13 +39,22 @@ import uuid
import json
from apps.ollama.main import generate_ollama_embeddings, GenerateEmbeddingsForm
from apps.web.models.documents import (
Documents,
DocumentForm,
DocumentResponse,
)
from apps.rag.utils import query_doc, query_collection, get_embedding_model_path
from apps.rag.utils import (
query_doc,
query_embeddings_doc,
query_collection,
query_embeddings_collection,
get_embedding_model_path,
generate_openai_embeddings,
)
from utils.misc import (
calculate_sha256,
@ -58,6 +67,7 @@ from config import (
SRC_LOG_LEVELS,
UPLOAD_DIR,
DOCS_DIR,
RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
DEVICE_TYPE,
@ -74,16 +84,21 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
app = FastAPI()
app.state.PDF_EXTRACT_IMAGES = False
app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.RAG_TEMPLATE = RAG_TEMPLATE
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.TOP_K = 4
app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.RAG_TEMPLATE = RAG_TEMPLATE
app.state.RAG_OPENAI_API_BASE_URL = "https://api.openai.com"
app.state.RAG_OPENAI_API_KEY = ""
app.state.PDF_EXTRACT_IMAGES = False
app.state.sentence_transformer_ef = (
embedding_functions.SentenceTransformerEmbeddingFunction(
@ -121,45 +136,72 @@ async def get_status():
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
"template": app.state.RAG_TEMPLATE,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
}
@app.get("/embedding/model")
async def get_embedding_model(user=Depends(get_admin_user)):
@app.get("/embedding")
async def get_embedding_config(user=Depends(get_admin_user)):
return {
"status": True,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
"openai_config": {
"url": app.state.RAG_OPENAI_API_BASE_URL,
"key": app.state.RAG_OPENAI_API_KEY,
},
}
class OpenAIConfigForm(BaseModel):
url: str
key: str
class EmbeddingModelUpdateForm(BaseModel):
openai_config: Optional[OpenAIConfigForm] = None
embedding_engine: str
embedding_model: str
@app.post("/embedding/model/update")
async def update_embedding_model(
@app.post("/embedding/update")
async def update_embedding_config(
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
):
log.info(
f"Updating embedding model: {app.state.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
)
try:
sentence_transformer_ef = (
embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=get_embedding_model_path(form_data.embedding_model, True),
device=DEVICE_TYPE,
)
)
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
app.state.sentence_transformer_ef = sentence_transformer_ef
if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
app.state.sentence_transformer_ef = None
if form_data.openai_config != None:
app.state.RAG_OPENAI_API_BASE_URL = form_data.openai_config.url
app.state.RAG_OPENAI_API_KEY = form_data.openai_config.key
else:
sentence_transformer_ef = (
embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=get_embedding_model_path(
form_data.embedding_model, True
),
device=DEVICE_TYPE,
)
)
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
app.state.sentence_transformer_ef = sentence_transformer_ef
return {
"status": True,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
"openai_config": {
"url": app.state.RAG_OPENAI_API_BASE_URL,
"key": app.state.RAG_OPENAI_API_KEY,
},
}
except Exception as e:
@ -252,12 +294,37 @@ def query_doc_handler(
):
try:
return query_doc(
collection_name=form_data.collection_name,
query=form_data.query,
k=form_data.k if form_data.k else app.state.TOP_K,
embedding_function=app.state.sentence_transformer_ef,
)
if app.state.RAG_EMBEDDING_ENGINE == "":
return query_doc(
collection_name=form_data.collection_name,
query=form_data.query,
k=form_data.k if form_data.k else app.state.TOP_K,
embedding_function=app.state.sentence_transformer_ef,
)
else:
if app.state.RAG_EMBEDDING_ENGINE == "ollama":
query_embeddings = generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": app.state.RAG_EMBEDDING_MODEL,
"prompt": form_data.query,
}
)
)
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
query_embeddings = generate_openai_embeddings(
model=app.state.RAG_EMBEDDING_MODEL,
text=form_data.query,
key=app.state.RAG_OPENAI_API_KEY,
url=app.state.RAG_OPENAI_API_BASE_URL,
)
return query_embeddings_doc(
collection_name=form_data.collection_name,
query_embeddings=query_embeddings,
k=form_data.k if form_data.k else app.state.TOP_K,
)
except Exception as e:
log.exception(e)
raise HTTPException(
@ -277,12 +344,45 @@ def query_collection_handler(
form_data: QueryCollectionsForm,
user=Depends(get_current_user),
):
return query_collection(
collection_names=form_data.collection_names,
query=form_data.query,
k=form_data.k if form_data.k else app.state.TOP_K,
embedding_function=app.state.sentence_transformer_ef,
)
try:
if app.state.RAG_EMBEDDING_ENGINE == "":
return query_collection(
collection_names=form_data.collection_names,
query=form_data.query,
k=form_data.k if form_data.k else app.state.TOP_K,
embedding_function=app.state.sentence_transformer_ef,
)
else:
if app.state.RAG_EMBEDDING_ENGINE == "ollama":
query_embeddings = generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": app.state.RAG_EMBEDDING_MODEL,
"prompt": form_data.query,
}
)
)
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
query_embeddings = generate_openai_embeddings(
model=app.state.RAG_EMBEDDING_MODEL,
text=form_data.query,
key=app.state.RAG_OPENAI_API_KEY,
url=app.state.RAG_OPENAI_API_BASE_URL,
)
return query_embeddings_collection(
collection_names=form_data.collection_names,
query_embeddings=query_embeddings,
k=form_data.k if form_data.k else app.state.TOP_K,
)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@app.post("/web")
@ -317,9 +417,11 @@ def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> b
chunk_overlap=app.state.CHUNK_OVERLAP,
add_start_index=True,
)
docs = text_splitter.split_documents(data)
if len(docs) > 0:
log.info(f"store_data_in_vector_db {docs}")
return store_docs_in_vector_db(docs, collection_name, overwrite), None
else:
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
@ -338,6 +440,7 @@ def store_text_in_vector_db(
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
texts = [doc.page_content for doc in docs]
metadatas = [doc.metadata for doc in docs]
@ -349,18 +452,52 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
log.info(f"deleting existing collection {collection_name}")
CHROMA_CLIENT.delete_collection(name=collection_name)
collection = CHROMA_CLIENT.create_collection(
name=collection_name,
embedding_function=app.state.sentence_transformer_ef,
)
if app.state.RAG_EMBEDDING_ENGINE == "":
for batch in create_batches(
api=CHROMA_CLIENT,
ids=[str(uuid.uuid1()) for _ in texts],
metadatas=metadatas,
documents=texts,
):
collection.add(*batch)
collection = CHROMA_CLIENT.create_collection(
name=collection_name,
embedding_function=app.state.sentence_transformer_ef,
)
for batch in create_batches(
api=CHROMA_CLIENT,
ids=[str(uuid.uuid1()) for _ in texts],
metadatas=metadatas,
documents=texts,
):
collection.add(*batch)
else:
collection = CHROMA_CLIENT.create_collection(name=collection_name)
if app.state.RAG_EMBEDDING_ENGINE == "ollama":
embeddings = [
generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{"model": app.state.RAG_EMBEDDING_MODEL, "prompt": text}
)
)
for text in texts
]
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
embeddings = [
generate_openai_embeddings(
model=app.state.RAG_EMBEDDING_MODEL,
text=text,
key=app.state.RAG_OPENAI_API_KEY,
url=app.state.RAG_OPENAI_API_BASE_URL,
)
for text in texts
]
for batch in create_batches(
api=CHROMA_CLIENT,
ids=[str(uuid.uuid1()) for _ in texts],
metadatas=metadatas,
embeddings=embeddings,
documents=texts,
):
collection.add(*batch)
return True
except Exception as e:

View file

@ -2,10 +2,16 @@ import os
import re
import logging
from typing import List
import requests
from huggingface_hub import snapshot_download
from apps.ollama.main import generate_ollama_embeddings, GenerateEmbeddingsForm
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@ -26,6 +32,24 @@ def query_doc(collection_name: str, query: str, k: int, embedding_function):
raise e
def query_embeddings_doc(collection_name: str, query_embeddings, k: int):
try:
# if you use docker use the model from the environment variable
log.info(f"query_embeddings_doc {query_embeddings}")
collection = CHROMA_CLIENT.get_collection(
name=collection_name,
)
result = collection.query(
query_embeddings=[query_embeddings],
n_results=k,
)
log.info(f"query_embeddings_doc:result {result}")
return result
except Exception as e:
raise e
def merge_and_sort_query_results(query_results, k):
# Initialize lists to store combined data
combined_ids = []
@ -96,14 +120,46 @@ def query_collection(
return merge_and_sort_query_results(results, k)
def query_embeddings_collection(collection_names: List[str], query_embeddings, k: int):
results = []
log.info(f"query_embeddings_collection {query_embeddings}")
for collection_name in collection_names:
try:
collection = CHROMA_CLIENT.get_collection(name=collection_name)
result = collection.query(
query_embeddings=[query_embeddings],
n_results=k,
)
results.append(result)
except:
pass
return merge_and_sort_query_results(results, k)
def rag_template(template: str, context: str, query: str):
template = template.replace("[context]", context)
template = template.replace("[query]", query)
return template
def rag_messages(docs, messages, template, k, embedding_function):
log.debug(f"docs: {docs}")
def rag_messages(
docs,
messages,
template,
k,
embedding_engine,
embedding_model,
embedding_function,
openai_key,
openai_url,
):
log.debug(
f"docs: {docs} {messages} {embedding_engine} {embedding_model} {embedding_function} {openai_key} {openai_url}"
)
last_user_message_idx = None
for i in range(len(messages) - 1, -1, -1):
@ -136,22 +192,57 @@ def rag_messages(docs, messages, template, k, embedding_function):
context = None
try:
if doc["type"] == "collection":
context = query_collection(
collection_names=doc["collection_names"],
query=query,
k=k,
embedding_function=embedding_function,
)
elif doc["type"] == "text":
if doc["type"] == "text":
context = doc["content"]
else:
context = query_doc(
collection_name=doc["collection_name"],
query=query,
k=k,
embedding_function=embedding_function,
)
if embedding_engine == "":
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,
)
else:
if embedding_engine == "ollama":
query_embeddings = generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": embedding_model,
"prompt": query,
}
)
)
elif embedding_engine == "openai":
query_embeddings = generate_openai_embeddings(
model=embedding_model,
text=query,
key=openai_key,
url=openai_url,
)
if doc["type"] == "collection":
context = query_embeddings_collection(
collection_names=doc["collection_names"],
query_embeddings=query_embeddings,
k=k,
)
else:
context = query_embeddings_doc(
collection_name=doc["collection_name"],
query_embeddings=query_embeddings,
k=k,
)
except Exception as e:
log.exception(e)
context = None
@ -230,3 +321,26 @@ def get_embedding_model_path(
except Exception as e:
log.exception(f"Cannot determine embedding model snapshot path: {e}")
return embedding_model
def generate_openai_embeddings(
model: str, text: str, key: str, url: str = "https://api.openai.com"
):
try:
r = requests.post(
f"{url}/v1/embeddings",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {key}",
},
json={"input": text, "model": model},
)
r.raise_for_status()
data = r.json()
if "data" in data:
return data["data"][0]["embedding"]
else:
raise "Something went wrong :/"
except Exception as e:
print(e)
return None

View file

@ -18,6 +18,51 @@ from secrets import token_bytes
from constants import ERROR_MESSAGES
####################################
# LOGGING
####################################
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
if GLOBAL_LOG_LEVEL in log_levels:
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
else:
GLOBAL_LOG_LEVEL = "INFO"
log = logging.getLogger(__name__)
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
log_sources = [
"AUDIO",
"COMFYUI",
"CONFIG",
"DB",
"IMAGES",
"LITELLM",
"MAIN",
"MODELS",
"OLLAMA",
"OPENAI",
"RAG",
"WEBHOOK",
]
SRC_LOG_LEVELS = {}
for source in log_sources:
log_env_var = source + "_LOG_LEVEL"
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
if SRC_LOG_LEVELS[source] not in log_levels:
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
####################################
# Load .env file
####################################
try:
from dotenv import load_dotenv, find_dotenv
@ -122,47 +167,6 @@ STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
shutil.copyfile(f"{FRONTEND_BUILD_DIR}/favicon.png", f"{STATIC_DIR}/favicon.png")
####################################
# LOGGING
####################################
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
if GLOBAL_LOG_LEVEL in log_levels:
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
else:
GLOBAL_LOG_LEVEL = "INFO"
log = logging.getLogger(__name__)
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
log_sources = [
"AUDIO",
"COMFYUI",
"CONFIG",
"DB",
"IMAGES",
"LITELLM",
"MAIN",
"MODELS",
"OLLAMA",
"OPENAI",
"RAG",
"WEBHOOK",
]
SRC_LOG_LEVELS = {}
for source in log_sources:
log_env_var = source + "_LOG_LEVEL"
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
if SRC_LOG_LEVELS[source] not in log_levels:
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
####################################
# CUSTOM_NAME
####################################
@ -401,6 +405,9 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (all-MiniLM-L6-v2)
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2")
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
@ -409,7 +416,7 @@ RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
)
# device type ebbeding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
if USE_CUDA.lower() == "true":
@ -446,11 +453,17 @@ Query: [query]"""
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base")
WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
WHISPER_MODEL_AUTO_UPDATE = (
os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
)
####################################
# Images
####################################
ENABLE_IMAGE_GENERATION = (
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
)
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")

View file

@ -114,7 +114,11 @@ class RAGMiddleware(BaseHTTPMiddleware):
data["messages"],
rag_app.state.RAG_TEMPLATE,
rag_app.state.TOP_K,
rag_app.state.RAG_EMBEDDING_ENGINE,
rag_app.state.RAG_EMBEDDING_MODEL,
rag_app.state.sentence_transformer_ef,
rag_app.state.RAG_OPENAI_API_KEY,
rag_app.state.RAG_OPENAI_API_BASE_URL,
)
del data["docs"]

View file

@ -10,7 +10,7 @@ ollama
{{- if .Values.ollama.externalHost }}
{{- printf .Values.ollama.externalHost }}
{{- else }}
{{- printf "http://%s.%s.svc.cluster.local:%d/" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
{{- printf "http://%s.%s.svc.cluster.local:%d" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
{{- end }}
{{- end }}

4
package-lock.json generated
View file

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

View file

@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.1.118",
"version": "0.1.119",
"private": true,
"scripts": {
"dev": "vite dev --host",

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" crossorigin="use-credentials" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="robots" content="noindex,nofollow" />
<script>

View file

@ -1,4 +1,5 @@
import { OLLAMA_API_BASE_URL } from '$lib/constants';
import { promptTemplate } from '$lib/utils';
export const getOllamaUrls = async (token: string = '') => {
let error = null;
@ -144,7 +145,7 @@ export const generateTitle = async (
) => {
let error = null;
template = template.replace(/{{prompt}}/g, prompt);
template = promptTemplate(template, prompt);
console.log(template);
@ -219,6 +220,32 @@ export const generatePrompt = async (token: string = '', model: string, conversa
return res;
};
export const generateEmbeddings = async (token: string = '', model: string, text: string) => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/embeddings`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
prompt: text
})
}).catch((err) => {
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const generateTextCompletion = async (token: string = '', model: string, text: string) => {
let error = null;

View file

@ -1,4 +1,5 @@
import { OPENAI_API_BASE_URL } from '$lib/constants';
import { promptTemplate } from '$lib/utils';
export const getOpenAIUrls = async (token: string = '') => {
let error = null;
@ -273,7 +274,7 @@ export const generateTitle = async (
) => {
let error = null;
template = template.replace(/{{prompt}}/g, prompt);
template = promptTemplate(template, prompt);
console.log(template);

View file

@ -346,10 +346,10 @@ export const resetVectorDB = async (token: string) => {
return res;
};
export const getEmbeddingModel = async (token: string) => {
export const getEmbeddingConfig = async (token: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/embedding/model`, {
const res = await fetch(`${RAG_API_BASE_URL}/embedding`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@ -373,14 +373,21 @@ export const getEmbeddingModel = async (token: string) => {
return res;
};
type OpenAIConfigForm = {
key: string;
url: string;
};
type EmbeddingModelUpdateForm = {
openai_config?: OpenAIConfigForm;
embedding_engine: string;
embedding_model: string;
};
export const updateEmbeddingModel = async (token: string, payload: EmbeddingModelUpdateForm) => {
export const updateEmbeddingConfig = async (token: string, payload: EmbeddingModelUpdateForm) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/embedding/model/update`, {
const res = await fetch(`${RAG_API_BASE_URL}/embedding/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -32,6 +32,7 @@
<button
class="self-center"
on:click={() => {
localStorage.version = $config.version;
show = false;
}}
>

View file

@ -18,6 +18,7 @@
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
import { imageGenerations } from '$lib/apis/images';
import {
approximateToHumanReadable,
extractSentences,
revertSanitizedResponseContent,
sanitizeResponseContent
@ -122,7 +123,10 @@
eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
eval_duration: ${
Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
}ms</span>`,
}ms<br/>
approximate_total: ${approximateToHumanReadable(
message.info.total_duration
)}</span>`,
allowHTML: true
});
}

View file

@ -33,7 +33,7 @@
: items;
const pullModelHandler = async () => {
const sanitizedModelTag = searchValue.trim();
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
console.log($MODEL_DOWNLOAD_POOL);
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {

View file

@ -139,7 +139,7 @@
};
const pullModelHandler = async () => {
const sanitizedModelTag = modelTag.trim();
const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, '');
if (modelDownloadStatus[sanitizedModelTag]) {
toast.error(
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {

View file

@ -0,0 +1,52 @@
<script>
import {
addTagById,
deleteTagById,
getAllChatTags,
getTagsById,
updateChatById
} from '$lib/apis/chats';
import { tags as _tags } from '$lib/stores';
import { onMount } from 'svelte';
import Tags from '../common/Tags.svelte';
export let chatId = '';
let tags = [];
const getTags = async () => {
return await getTagsById(localStorage.token, chatId).catch(async (error) => {
return [];
});
};
const addTag = async (tagName) => {
const res = await addTagById(localStorage.token, chatId, tagName);
tags = await getTags();
await updateChatById(localStorage.token, chatId, {
tags: tags
});
_tags.set(await getAllChatTags(localStorage.token));
};
const deleteTag = async (tagName) => {
const res = await deleteTagById(localStorage.token, chatId, tagName);
tags = await getTags();
await updateChatById(localStorage.token, chatId, {
tags: tags
});
_tags.set(await getAllChatTags(localStorage.token));
};
onMount(async () => {
if (chatId) {
tags = await getTags();
}
});
</script>
<Tags {tags} {deleteTag} {addTag} />

View file

@ -4,10 +4,12 @@
import { flyAndScale } from '$lib/utils/transitions';
export let show = false;
const dispatch = createEventDispatcher();
</script>
<DropdownMenu.Root
bind:open={show}
onOpenChange={(state) => {
dispatch('change', state);
}}

View file

@ -0,0 +1,42 @@
<script lang="ts">
import { Pagination } from 'bits-ui';
import { createEventDispatcher } from 'svelte';
import ChevronLeft from '../icons/ChevronLeft.svelte';
import ChevronRight from '../icons/ChevronRight.svelte';
export let page = 0;
export let count = 0;
export let perPage = 20;
</script>
<div class="flex justify-center">
<Pagination.Root bind:page {count} {perPage} let:pages>
<div class="my-2 flex items-center">
<Pagination.PrevButton
class="mr-[25px] inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent"
>
<ChevronLeft className="size-4" strokeWidth="2" />
</Pagination.PrevButton>
<div class="flex items-center gap-2.5">
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<div class="text-sm font-medium text-foreground-alt">...</div>
{:else}
<Pagination.Page
{page}
class="inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-sm font-medium hover:bg-dark-10 active:scale-98 disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent data-[selected]:bg-black data-[selected]:text-gray-100 data-[selected]:hover:bg-black dark:data-[selected]:bg-white dark:data-[selected]:text-gray-900 dark:data-[selected]:hover:bg-white"
>
{page.value}
</Pagination.Page>
{/if}
{/each}
</div>
<Pagination.NextButton
class="ml-[25px] inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent"
>
<ChevronRight className="size-4" strokeWidth="2" />
</Pagination.NextButton>
</div>
</Pagination.Root>
</div>

View file

@ -16,7 +16,6 @@
const i18n = getContext('i18n');
export let show = false;
export let selectedDoc;
let uploadDocInputElement: HTMLInputElement;
let inputFiles;
let tags = [];

View file

@ -7,11 +7,11 @@
scanDocs,
updateQuerySettings,
resetVectorDB,
getEmbeddingModel,
updateEmbeddingModel
getEmbeddingConfig,
updateEmbeddingConfig
} from '$lib/apis/rag';
import { documents } from '$lib/stores';
import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
@ -26,6 +26,12 @@
let showResetConfirm = false;
let embeddingEngine = '';
let embeddingModel = '';
let openAIKey = '';
let openAIUrl = '';
let chunkSize = 0;
let chunkOverlap = 0;
let pdfExtractImages = true;
@ -35,8 +41,6 @@
k: 4
};
let embeddingModel = '';
const scanHandler = async () => {
scanDirLoading = true;
const res = await scanDocs(localStorage.token);
@ -49,7 +53,15 @@
};
const embeddingModelUpdateHandler = async () => {
if (embeddingModel.split('/').length - 1 > 1) {
if (embeddingEngine === '' && embeddingModel.split('/').length - 1 > 1) {
toast.error(
$i18n.t(
'Model filesystem path detected. Model shortname is required for update, cannot continue.'
)
);
return;
}
if (embeddingEngine === 'ollama' && embeddingModel === '') {
toast.error(
$i18n.t(
'Model filesystem path detected. Model shortname is required for update, cannot continue.'
@ -58,14 +70,37 @@
return;
}
if (embeddingEngine === 'openai' && embeddingModel === '') {
toast.error(
$i18n.t(
'Model filesystem path detected. Model shortname is required for update, cannot continue.'
)
);
return;
}
if ((embeddingEngine === 'openai' && openAIKey === '') || openAIUrl === '') {
toast.error($i18n.t('OpenAI URL/Key required.'));
return;
}
console.log('Update embedding model attempt:', embeddingModel);
updateEmbeddingModelLoading = true;
const res = await updateEmbeddingModel(localStorage.token, {
embedding_model: embeddingModel
const res = await updateEmbeddingConfig(localStorage.token, {
embedding_engine: embeddingEngine,
embedding_model: embeddingModel,
...(embeddingEngine === 'openai'
? {
openai_config: {
key: openAIKey,
url: openAIUrl
}
}
: {})
}).catch(async (error) => {
toast.error(error);
embeddingModel = (await getEmbeddingModel(localStorage.token)).embedding_model;
await setEmbeddingConfig();
return null;
});
updateEmbeddingModelLoading = false;
@ -73,7 +108,7 @@
if (res) {
console.log('embeddingModelUpdateHandler:', res);
if (res.status === true) {
toast.success($i18n.t('Model {{embedding_model}} update complete!', res), {
toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), {
duration: 1000 * 10
});
}
@ -91,6 +126,18 @@
querySettings = await updateQuerySettings(localStorage.token, querySettings);
};
const setEmbeddingConfig = async () => {
const embeddingConfig = await getEmbeddingConfig(localStorage.token);
if (embeddingConfig) {
embeddingEngine = embeddingConfig.embedding_engine;
embeddingModel = embeddingConfig.embedding_model;
openAIKey = embeddingConfig.openai_config.key;
openAIUrl = embeddingConfig.openai_config.url;
}
};
onMount(async () => {
const res = await getRAGConfig(localStorage.token);
@ -101,7 +148,7 @@
chunkOverlap = res.chunk.chunk_overlap;
}
embeddingModel = (await getEmbeddingModel(localStorage.token)).embedding_model;
await setEmbeddingConfig();
querySettings = await getQuerySettings(localStorage.token);
});
@ -118,81 +165,212 @@
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={embeddingEngine}
placeholder="Select an embedding model engine"
on:change={(e) => {
if (e.target.value === 'ollama') {
embeddingModel = '';
} else if (e.target.value === 'openai') {
embeddingModel = 'text-embedding-3-small';
}
}}
>
<option value="">{$i18n.t('Default (SentenceTransformer)')}</option>
<option value="ollama">{$i18n.t('Ollama')}</option>
<option value="openai">{$i18n.t('OpenAI')}</option>
</select>
</div>
<button
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
? ' cursor-not-allowed'
: ''}"
on:click={() => {
scanHandler();
console.log('check');
}}
type="button"
disabled={scanDirLoading}
>
<div class="self-center font-medium">{$i18n.t('Scan')}</div>
{#if scanDirLoading}
<div class="ml-3 self-center">
<svg
class=" w-3 h-3"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
</div>
<hr class=" dark:border-gray-700" />
{#if embeddingEngine === 'openai'}
<div class="mt-1 flex gap-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={openAIUrl}
required
/>
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={openAIKey}
required
/>
</div>
{/if}
</div>
<div class="space-y-2">
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Update Embedding Model')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Update embedding model (e.g. {{model}})', {
model: embeddingModel.slice(-40)
})}
bind:value={embeddingModel}
/>
</div>
<button
class="px-2.5 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={() => {
embeddingModelUpdateHandler();
}}
disabled={updateEmbeddingModelLoading}
>
{#if updateEmbeddingModelLoading}
<div class="self-center">
{#if embeddingEngine === 'ollama'}
<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={embeddingModel}
placeholder={$i18n.t('Select a model')}
required
>
{#if !embeddingModel}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each $models.filter((m) => m.id && !m.external) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
>
{/each}
</select>
</div>
<button
class="px-2.5 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={() => {
embeddingModelUpdateHandler();
}}
disabled={updateEmbeddingModelLoading}
>
{#if updateEmbeddingModelLoading}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<svg
class=" w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
{:else}
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Update embedding model (e.g. {{model}})', {
model: embeddingModel.slice(-40)
})}
bind:value={embeddingModel}
/>
</div>
<button
class="px-2.5 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={() => {
embeddingModelUpdateHandler();
}}
disabled={updateEmbeddingModelLoading}
>
{#if updateEmbeddingModelLoading}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
</div>
{/if}
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(
'Warning: If you update or change your embedding model, you will need to re-import all documents.'
)}
</div>
<hr class=" dark:border-gray-700 my-3" />
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
</div>
<button
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
? ' cursor-not-allowed'
: ''}"
on:click={() => {
scanHandler();
console.log('check');
}}
type="button"
disabled={scanDirLoading}
>
<div class="self-center font-medium">{$i18n.t('Scan')}</div>
{#if scanDirLoading}
<div class="ml-3 self-center">
<svg
class=" w-3 h-3"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
@ -215,30 +393,10 @@
/></svg
>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(
'Warning: If you update or change your embedding model, you will need to re-import all documents.'
)}
</div>
<hr class=" dark:border-gray-700 my-3" />
<div class=" ">

View file

@ -0,0 +1,15 @@
<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="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>

View file

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

View file

@ -19,10 +19,6 @@
export let chat;
export let selectedModels;
export let tags = [];
export let addTag: Function;
export let deleteTag: Function;
export let showModelSelector = true;
let showShareChatModal = false;
@ -85,9 +81,6 @@
downloadHandler={() => {
showDownloadChatModal = !showDownloadChatModal;
}}
{tags}
{deleteTag}
{addTag}
>
<button
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"

View file

@ -10,8 +10,8 @@
import { flyAndScale } from '$lib/utils/transitions';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import Tags from '$lib/components/common/Tags.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tags from '$lib/components/chat/Tags.svelte';
import { downloadChatAsPDF } from '$lib/apis/utils';
export let shareEnabled: boolean = false;
@ -21,10 +21,6 @@
// export let tagHandler: Function;
export let chat;
export let tags;
export let deleteTag: Function;
export let addTag: Function;
export let onClose: Function = () => {};
const downloadTxt = async () => {
@ -190,7 +186,7 @@
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1">
<Tags {tags} {deleteTag} {addTag} />
<Tags chatId={chat.id} />
</div>
<!-- <DropdownMenu.Item

View file

@ -45,6 +45,39 @@
show = true;
}
await chats.set(await getChatList(localStorage.token));
let touchstartX = 0;
let touchendX = 0;
function checkDirection() {
const screenWidth = window.innerWidth;
const swipeDistance = Math.abs(touchendX - touchstartX);
if (swipeDistance >= screenWidth / 4) {
if (touchendX < touchstartX) {
show = false;
}
if (touchendX > touchstartX) {
show = true;
}
}
}
const onTouchStart = (e) => {
touchstartX = e.changedTouches[0].screenX;
};
const onTouchEnd = (e) => {
touchendX = e.changedTouches[0].screenX;
checkDirection();
};
document.addEventListener('touchstart', onTouchStart);
document.addEventListener('touchend', onTouchEnd);
return () => {
document.removeEventListener('touchstart', onTouchStart);
document.removeEventListener('touchend', onTouchEnd);
};
});
// Helper function to fetch and add chat content to each chat
@ -513,6 +546,7 @@
{:else}
<div class="flex self-center space-x-1.5 z-10">
<ChatMenu
chatId={chat.id}
renameHandler={() => {
chatTitle = chat.title;
chatTitleEditId = chat.id;
@ -706,6 +740,7 @@
</div>
<div
id="sidebar-handle"
class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
>
<Tooltip

View file

@ -6,14 +6,19 @@
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
export let renameHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
export let chatId = '';
let show = false;
</script>
<Dropdown
bind:show
on:change={(e) => {
if (e.detail === false) {
onClose();
@ -26,14 +31,14 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
class="w-full max-w-[150px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow"
sideOffset={-2}
side="bottom"
align="start"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
renameHandler();
}}
@ -43,7 +48,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
deleteHandler();
}}
@ -51,6 +56,12 @@
<GarbageBin strokeWidth="2" />
<div class="flex items-center">Delete</div>
</DropdownMenu.Item>
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1">
<Tags {chatId} />
</div>
</DropdownMenu.Content>
</div>
</Dropdown>

View file

@ -63,7 +63,7 @@
"Click here to select": "Presiona aquí para seleccionar",
"Click here to select documents.": "Presiona aquí para seleccionar documentos",
"click here.": "Presiona aquí.",
"Click on the user role button to change a user's role.": "Presiona en el botón de roles del usuario para cambiar el rol de un usuario.",
"Click on the user role button to change a user's role.": "Presiona en el botón de roles del usuario para cambiar su rol.",
"Close": "Cerrar",
"Collection": "Colección",
"Command": "Comando",
@ -120,9 +120,10 @@
"Edit Doc": "Editar Documento",
"Edit User": "Editar Usuario",
"Email": "Email",
"Embedding model: {{embedding_model}}": "Modelo de Embedding: {{embedding_model}}",
"Enable Chat History": "Activa el Historial de Chat",
"Enable New Sign Ups": "Habilitar Nuevos Registros",
"Enabled": "Habilitado",
"Enabled": "Activado",
"Enter {{role}} message here": "Introduzca el mensaje {{role}} aquí",
"Enter API Key": "Ingrese la clave API",
"Enter Chunk Overlap": "Ingresar superposición de fragmentos",
@ -145,11 +146,12 @@
"Export All Chats (All Users)": "Exportar todos los chats (Todos los usuarios)",
"Export Chats": "Exportar Chats",
"Export Documents Mapping": "Exportar el mapeo de documentos",
"Export Modelfiles": "Exportal Modelfiles",
"Export Modelfiles": "Exportar Modelfiles",
"Export Prompts": "Exportar Prompts",
"Failed to read clipboard contents": "No se pudo leer el contenido del portapapeles",
"File Mode": "Modo de archivo",
"File not found.": "Archivo no encontrado.",
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Se detectó suplantación de huellas: No se pueden usar las iniciales como avatar. Por defecto se utiliza la imagen de perfil predeterminada.",
"Focus chat input": "Enfoca la entrada del chat",
"Format your variables using square brackets like this:": "Formatee sus variables usando corchetes así:",
"From (Base Model)": "Desde (Modelo Base)",
@ -193,8 +195,11 @@
"MMMM DD, YYYY": "MMMM DD, YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "El modelo '{{modelName}}' se ha descargado correctamente.",
"Model '{{modelTag}}' is already in queue for downloading.": "El modelo '{{modelTag}}' ya está en cola para descargar.",
"Model {{embedding_model}} update complete!": "¡La actualizacón del modelo {{embedding_model}} fué completada!",
"Model {{embedding_model}} update failed or not required!": "¡La actualización del modelo {{embedding_model}} falló o no es requerida!",
"Model {{modelId}} not found": "El modelo {{modelId}} no fue encontrado",
"Model {{modelName}} already exists.": "El modelo {{modelName}} ya existe.",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Se detectó la ruta del sistema de archivos del modelo. Se requiere el nombre corto del modelo para la actualización, no se puede continuar.",
"Model Name": "Nombre del modelo",
"Model not selected": "Modelo no seleccionado",
"Model Tag Name": "Nombre de la etiqueta del modelo",
@ -215,11 +220,11 @@
"New Password": "Nueva Contraseña",
"Not sure what to add?": "¿No estás seguro de qué añadir?",
"Not sure what to write? Switch to": "¿No estás seguro de qué escribir? Cambia a",
"Off": "Apagado",
"Off": "Desactivado",
"Okay, Let's Go!": "Okay, Let's Go!",
"Ollama Base URL": "URL base de Ollama",
"Ollama Version": "Version de Ollama",
"On": "Encendido",
"On": "Activado",
"Only": "Solamente",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Sólo se permiten caracteres alfanuméricos y guiones en la cadena de comando.",
"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.": "¡Ups! ¡Agárrate fuerte! Tus archivos todavía están en el horno de procesamiento. Los estamos cocinando a la perfección. Tenga paciencia y le avisaremos una vez que estén listos.",
@ -227,7 +232,7 @@
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "¡Ups! Estás utilizando un método no compatible (solo frontend). Sirve la WebUI desde el backend.",
"Open": "Abrir",
"Open AI": "Open AI",
"Open AI (Dall-E)": "",
"Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Abrir nuevo chat",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "Clave de OpenAI API",
@ -243,7 +248,7 @@
"Prompt Content": "Contenido del Prompt",
"Prompt suggestions": "Sugerencias de Prompts",
"Prompts": "Prompts",
"Pull a model from Ollama.com": "Extraer un modelo de Ollama.com",
"Pull a model from Ollama.com": "Halar un modelo de Ollama.com",
"Pull Progress": "Progreso de extracción",
"Query Params": "Parámetros de consulta",
"RAG Template": "Plantilla de RAG",
@ -256,7 +261,7 @@
"Request Mode": "Modo de petición",
"Reset Vector Storage": "Restablecer almacenamiento vectorial",
"Response AutoCopy to Clipboard": "Copiar respuesta automáticamente al portapapeles",
"Role": "personalizados",
"Role": "Rol",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Guardar",
@ -332,7 +337,10 @@
"TTS Settings": "Configuración de TTS",
"Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL",
"Uh-oh! There was an issue connecting to {{provider}}.": "¡UH oh! Hubo un problema al conectarse a {{provider}}.",
"Understand that updating or changing your embedding model requires reset of the vector database and re-import of all documents. You have been warned!": "Comprenda que actualizar o cambiar su modelo de embedding requiere restablecer la base de datos de vectores y volver a importar todos los documentos. ¡Usted ha sido advertido!",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipo de archivo desconocido '{{file_type}}', pero se acepta y se trata como texto sin formato",
"Update": "Actualizar",
"Update embedding model {{embedding_model}}": "Actualizar modelo de embedding {{embedding_model}}",
"Update password": "Actualiza contraseña",
"Upload a GGUF model": "Sube un modelo GGUF",
"Upload files": "Subir archivos",
@ -340,6 +348,7 @@
"URL Mode": "Modo de URL",
"Use '#' in the prompt input to load and select your documents.": "Utilice '#' en el prompt para cargar y seleccionar sus documentos.",
"Use Gravatar": "Usar Gravatar",
"Use Initials": "Usar Iniciales",
"user": "usuario",
"User Permissions": "Permisos de usuario",
"Users": "Usuarios",
@ -347,7 +356,7 @@
"Valid time units:": "Unidades válidas de tiempo:",
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable para reemplazarlos con el contenido del portapapeles.",
"Version": "Version",
"Version": "Versión",
"Web": "Web",
"WebUI Add-ons": "WebUI Add-ons",
"WebUI Settings": "Configuración del WebUI",

View file

@ -52,13 +52,17 @@
"title": "Dutch (Netherlands)"
},
{
"code": "pt-PT",
"title": "Portuguese (Portugal)"
"code": "pl-PL",
"title": "Polish"
},
{
"code": "pt-BR",
"title": "Portuguese (Brazil)"
},
{
"code": "pt-PT",
"title": "Portuguese (Portugal)"
},
{
"code": "ru-RU",
"title": "Russian (Russia)"

View file

@ -0,0 +1,372 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' lub '-1' dla bez wygaśnięcia.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(np. `sh webui.sh --api`)",
"(latest)": "(najnowszy)",
"{{modelName}} is thinking...": "{{modelName}} myśli...",
"{{webUIName}} Backend Required": "Backend {{webUIName}} wymagane",
"a user": "użytkownik",
"About": "O nas",
"Account": "Konto",
"Action": "Akcja",
"Add a model": "Dodaj model",
"Add a model tag name": "Dodaj nazwę tagu modelu",
"Add a short description about what this modelfile does": "Dodaj krótki opis tego, co robi ten plik modelu",
"Add a short title for this prompt": "Dodaj krótki tytuł tego polecenia",
"Add a tag": "Dodaj tag",
"Add Docs": "Dodaj dokumenty",
"Add Files": "Dodaj pliki",
"Add message": "Dodaj wiadomość",
"add tags": "dodaj tagi",
"Adjusting these settings will apply changes universally to all users.": "Dostosowanie tych ustawień spowoduje zastosowanie zmian uniwersalnie do wszystkich użytkowników.",
"admin": "admin",
"Admin Panel": "Panel administracyjny",
"Admin Settings": "Ustawienia administratora",
"Advanced Parameters": "Zaawansowane parametry",
"all": "wszyscy",
"All Users": "Wszyscy użytkownicy",
"Allow": "Pozwól",
"Allow Chat Deletion": "Pozwól na usuwanie czatu",
"alphanumeric characters and hyphens": "znaki alfanumeryczne i myślniki",
"Already have an account?": "Masz już konto?",
"an assistant": "asystent",
"and": "i",
"API Base URL": "Podstawowy adres URL interfejsu API",
"API Key": "Klucz API",
"API RPM": "Pakiet API RPM",
"are allowed - Activate this command by typing": "są dozwolone - Aktywuj to polecenie, wpisując",
"Are you sure?": "Jesteś pewien?",
"Audio": "Dźwięk",
"Auto-playback response": "Odtwarzanie automatyczne odpowiedzi",
"Auto-send input after 3 sec.": "Wysyłanie automatyczne po 3 sek.",
"AUTOMATIC1111 Base URL": "Podstawowy adres URL AUTOMATIC1111",
"AUTOMATIC1111 Base URL is required.": "Podstawowy adres URL AUTOMATIC1111 jest wymagany.",
"available!": "dostępny!",
"Back": "Wstecz",
"Builder Mode": "Tryb budowniczego",
"Cancel": "Anuluj",
"Categories": "Kategorie",
"Change Password": "Zmień hasło",
"Chat": "Czat",
"Chat History": "Historia czatu",
"Chat History is off for this browser.": "Historia czatu jest wyłączona dla tej przeglądarki.",
"Chats": "Czaty",
"Check Again": "Sprawdź ponownie",
"Check for updates": "Sprawdź aktualizacje",
"Checking for updates...": "Sprawdzanie aktualizacji...",
"Choose a model before saving...": "Wybierz model przed zapisaniem...",
"Chunk Overlap": "Zachodzenie bloku",
"Chunk Params": "Parametry bloku",
"Chunk Size": "Rozmiar bloku",
"Click here for help.": "Kliknij tutaj, aby uzyskać pomoc.",
"Click here to check other modelfiles.": "Kliknij tutaj, aby sprawdzić inne pliki modelowe.",
"Click here to select": "Kliknij tutaj, aby wybrać",
"Click here to select documents.": "Kliknij tutaj, aby wybrać dokumenty.",
"click here.": "kliknij tutaj.",
"Click on the user role button to change a user's role.": "Kliknij przycisk roli użytkownika, aby zmienić rolę użytkownika.",
"Close": "Zamknij",
"Collection": "Kolekcja",
"Command": "Polecenie",
"Confirm Password": "Potwierdź hasło",
"Connections": "Połączenia",
"Content": "Zawartość",
"Context Length": "Długość kontekstu",
"Conversation Mode": "Tryb rozmowy",
"Copy last code block": "Skopiuj ostatni blok kodu",
"Copy last response": "Skopiuj ostatnią odpowiedź",
"Copying to clipboard was successful!": "Kopiowanie do schowka zakończone powodzeniem!",
"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':": "Utwórz zwięzłą frazę składającą się z 3-5 słów jako nagłówek dla następującego zapytania, ściśle przestrzegając limitu od 3 do 5 słów i unikając użycia słowa 'tytuł':",
"Create a modelfile": "Utwórz plik modelu",
"Create Account": "Utwórz konto",
"Created at": "Utworzono o",
"Created by": "Utworzono przez",
"Current Model": "Bieżący model",
"Current Password": "Bieżące hasło",
"Custom": "Niestandardowy",
"Customize Ollama models for a specific purpose": "Dostosuj modele Ollama do określonego celu",
"Dark": "Ciemny",
"Database": "Baza danych",
"DD/MM/YYYY HH:mm": "DD/MM/RRRR GG:MM",
"Default": "Domyślny",
"Default (Automatic1111)": "Domyślny (Automatyczny1111)",
"Default (Web API)": "Domyślny (Interfejs API)",
"Default model updated": "Domyślny model zaktualizowany",
"Default Prompt Suggestions": "Domyślne sugestie promptów",
"Default User Role": "Domyślna rola użytkownika",
"delete": "Usuń",
"Delete a model": "Usuń model",
"Delete chat": "Usuń czat",
"Delete Chats": "Usuń czaty",
"Deleted {{deleteModelTag}}": "Usunięto {{deleteModelTag}}",
"Deleted {tagName}": "Usunięto {tagName}",
"Description": "Opis",
"Notifications": "Powiadomienia",
"Disabled": "Wyłączone",
"Discover a modelfile": "Odkryj plik modelu",
"Discover a prompt": "Odkryj prompt",
"Discover, download, and explore custom prompts": "Odkryj, pobierz i eksploruj niestandardowe prompty",
"Discover, download, and explore model presets": "Odkryj, pobierz i eksploruj ustawienia modeli",
"Display the username instead of You in the Chat": "Wyświetl nazwę użytkownika zamiast Ty w czacie",
"Document": "Dokument",
"Document Settings": "Ustawienia dokumentu",
"Documents": "Dokumenty",
"does not make any external connections, and your data stays securely on your locally hosted server.": "nie nawiązuje żadnych zewnętrznych połączeń, a Twoje dane pozostają bezpiecznie na Twoim lokalnie hostowanym serwerze.",
"Don't Allow": "Nie zezwalaj",
"Don't have an account?": "Nie masz konta?",
"Download as a File": "Pobierz jako plik",
"Download Database": "Pobierz bazę danych",
"Drop any files here to add to the conversation": "Upuść pliki tutaj, aby dodać do rozmowy",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "np. '30s', '10m'. Poprawne jednostki czasu to 's', 'm', 'h'.",
"Edit Doc": "Edytuj dokument",
"Edit User": "Edytuj użytkownika",
"Email": "Email",
"Embedding model: {{embedding_model}}": "Osadzony model: {{embedding_model}}",
"Enable Chat History": "Włącz historię czatu",
"Enable New Sign Ups": "Włącz nowe rejestracje",
"Enabled": "Włączone",
"Enter {{role}} message here": "Wprowadź wiadomość {{role}} tutaj",
"Enter API Key": "Wprowadź klucz API",
"Enter Chunk Overlap": "Wprowadź zakchodzenie bloku",
"Enter Chunk Size": "Wprowadź rozmiar bloku",
"Enter Image Size (e.g. 512x512)": "Wprowadź rozmiar obrazu (np. 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "Wprowadź bazowy adres URL LiteLLM API (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "Wprowadź klucz API LiteLLM (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "Wprowadź API LiteLLM RPM(litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "Wprowadź model LiteLLM (litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "Wprowadź maksymalną liczbę tokenów (litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "Wprowadź tag modelu (np. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Wprowadź liczbę kroków (np. 50)",
"Enter stop sequence": "Wprowadź sekwencję zatrzymania",
"Enter Top K": "Wprowadź Top K",
"Enter URL (e.g. http://127.0.0.1:7860/)": "Wprowadź adres URL (np. http://127.0.0.1:7860/)",
"Enter Your Email": "Wprowadź swój adres email",
"Enter Your Full Name": "Wprowadź swoje imię i nazwisko",
"Enter Your Password": "Wprowadź swoje hasło",
"Experimental": "Eksperymentalne",
"Export All Chats (All Users)": "Eksportuj wszystkie czaty (wszyscy użytkownicy)",
"Export Chats": "Eksportuj czaty",
"Export Documents Mapping": "Eksportuj mapowanie dokumentów",
"Export Modelfiles": "Eksportuj pliki modeli",
"Export Prompts": "Eksportuj prompty",
"Failed to read clipboard contents": "Nie udało się odczytać zawartości schowka",
"File Mode": "Tryb pliku",
"File not found.": "Plik nie został znaleziony.",
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Wykryto podszywanie się pod odcisk palca: Nie można używać inicjałów jako awatara. Przechodzenie do domyślnego obrazu profilowego.",
"Focus chat input": "Skoncentruj na czacie",
"Format your variables using square brackets like this:": "Formatuj swoje zmienne, używając nawiasów kwadratowych, np.",
"From (Base Model)": "Z (Model Podstawowy)",
"Full Screen Mode": "Tryb pełnoekranowy",
"General": "Ogólne",
"General Settings": "Ogólne ustawienia",
"Hello, {{name}}": "Witaj, {{nazwa}}",
"Hide": "Ukryj",
"Hide Additional Params": "Ukryj dodatkowe parametry",
"How can I help you today?": "Jak mogę Ci dzisiaj pomóc?",
"Image Generation (Experimental)": "Generowanie obrazu (eksperymentalne)",
"Image Generation Engine": "Silnik generowania obrazu",
"Image Settings": "Ustawienia obrazu",
"Images": "Obrazy",
"Import Chats": "Importuj rozmowy",
"Import Documents Mapping": "Importuj mapowanie dokumentów",
"Import Modelfiles": "Importuj pliki modeli",
"Import Prompts": "Importuj prompty",
"Include `--api` flag when running stable-diffusion-webui": "Dołącz flagę `--api` podczas uruchamiania stable-diffusion-webui",
"Interface": "Interfejs",
"join our Discord for help.": "Dołącz do naszego Discorda po pomoc.",
"JSON": "JSON",
"JWT Expiration": "Wygaśnięcie JWT",
"JWT Token": "Token JWT",
"Keep Alive": "Zachowaj łączność",
"Keyboard shortcuts": "Skróty klawiszowe",
"Language": "Język",
"Light": "Jasny",
"Listening...": "Nasłuchiwanie...",
"LLMs can make mistakes. Verify important information.": "LLMy mogą popełniać błędy. Zweryfikuj ważne informacje.",
"Made by OpenWebUI Community": "Stworzone przez społeczność OpenWebUI",
"Make sure to enclose them with": "Upewnij się, że są one zamknięte w",
"Manage LiteLLM Models": "Zarządzaj modelami LiteLLM",
"Manage Models": "Zarządzaj modelami",
"Manage Ollama Models": "Zarządzaj modelami Ollama",
"Max Tokens": "Maksymalna liczba tokenów",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maksymalnie 3 modele można pobierać jednocześnie. Spróbuj ponownie później.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "MMMM DD, YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "Model '{{nazwaModelu}}' został pomyślnie pobrany.",
"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{nazwaModelu}}' jest już w kolejce do pobrania.",
"Model {{embedding_model}} update complete!": "Aktualizacja modelu {{embedding_model}} zakończona pomyślnie!",
"Model {{embedding_model}} update failed or not required!": "Model {{embedding_model}} aktualizacja nie powiodła się lub nie jest wymagana!",
"Model {{modelId}} not found": "Model {{modelId}} nie został znaleziony",
"Model {{modelName}} already exists.": "Model {{modelName}} już istnieje.",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Wykryto ścieżkę systemu plików modelu. Wymagana jest krótka nazwa modelu do aktualizacji, nie można kontynuować.",
"Model Name": "Nazwa modelu",
"Model not selected": "Model nie został wybrany",
"Model Tag Name": "Nazwa tagu modelu",
"Model Whitelisting": "Whitelisting modelu",
"Model(s) Whitelisted": "Model(e) dodane do listy białej",
"Modelfile": "Plik modelu",
"Modelfile Advanced Settings": "Zaawansowane ustawienia pliku modelu",
"Modelfile Content": "Zawartość pliku modelu",
"Modelfiles": "Pliki modeli",
"Models": "Modele",
"My Documents": "Moje dokumenty",
"My Modelfiles": "Moje pliki modeli",
"My Prompts": "Moje prompty",
"Name": "Nazwa",
"Name Tag": "Etykieta nazwy",
"Name your modelfile": "Nadaj nazwę swojemu plikowi modelu",
"New Chat": "Nowy czat",
"New Password": "Nowe hasło",
"Not sure what to add?": "Nie wiesz, co dodać?",
"Not sure what to write? Switch to": "Nie wiesz, co napisać? Przełącz się na",
"Off": "Wyłączony",
"Okay, Let's Go!": "Okej, zaczynamy!",
"Ollama Base URL": "Adres bazowy URL Ollama",
"Ollama Version": "Wersja Ollama",
"On": "Włączony",
"Only": "Tylko",
"Only alphanumeric characters and hyphens are allowed in the command string.": "W poleceniu dozwolone są tylko znaki alfanumeryczne i myślniki.",
"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.": "Ups! Trzymaj się! Twoje pliki są wciąż w procesie obróbki. Gotujemy je do perfekcji. Prosimy o cierpliwość, poinformujemy Cię, gdy będą gotowe.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Ups! Wygląda na to, że URL jest nieprawidłowy. Sprawdź jeszcze raz i spróbuj ponownie.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ups! Używasz nieobsługiwaniej metody (tylko interfejs front-end). Proszę obsłużyć interfejs WebUI z poziomu backendu.",
"Open": "Otwórz",
"Open AI": "Open AI",
"Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Otwórz nowy czat",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "Klucz API OpenAI",
"OpenAI API Key is required.": "Klucz API OpenAI jest wymagany.",
"or": "lub",
"Parameters": "Parametry",
"Password": "Hasło",
"PDF Extract Images (OCR)": "PDF Wyodrębnij obrazy (OCR)",
"pending": "oczekujące",
"Permission denied when accessing microphone: {{error}}": "Odmowa dostępu do mikrofonu: {{error}}",
"Playground": "Plac zabaw",
"Profile": "Profil",
"Prompt Content": "Zawartość prompta",
"Prompt suggestions": "Sugestie prompta",
"Prompts": "Prompty",
"Pull a model from Ollama.com": "Pobierz model z Ollama.com",
"Pull Progress": "Postęp pobierania",
"Query Params": "Parametry zapytania",
"RAG Template": "Szablon RAG",
"Raw Format": "Format bez obróbki",
"Record voice": "Nagraj głos",
"Redirecting you to OpenWebUI Community": "Przekierowujemy Cię do społeczności OpenWebUI",
"Release Notes": "Notatki wydania",
"Repeat Last N": "Powtórz ostatnie N",
"Repeat Penalty": "Kara za powtórzenie",
"Request Mode": "Tryb żądania",
"Reset Vector Storage": "Resetuj przechowywanie wektorów",
"Response AutoCopy to Clipboard": "Automatyczne kopiowanie odpowiedzi do schowka",
"Role": "Rola",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Zapisz",
"Save & Create": "Zapisz i utwórz",
"Save & Submit": "Zapisz i wyślij",
"Save & Update": "Zapisz i zaktualizuj",
"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": "Bezpośrednie zapisywanie dzienników czatu w pamięci przeglądarki nie jest już obsługiwane. Prosimy o pobranie i usunięcie dzienników czatu, klikając poniższy przycisk. Nie martw się, możesz łatwo ponownie zaimportować dzienniki czatu do backendu za pomocą",
"Scan": "Skanuj",
"Scan complete!": "Skanowanie zakończone!",
"Scan for documents from {{path}}": "Skanuj dokumenty z {{path}}",
"Search": "Szukaj",
"Search Documents": "Szukaj dokumentów",
"Search Prompts": "Szukaj promptów",
"See readme.md for instructions": "Zajrzyj do readme.md po instrukcje",
"See what's new": "Zobacz co nowego",
"Seed": "Seed",
"Select a mode": "Wybierz tryb",
"Select a model": "Wybierz model",
"Select an Ollama instance": "Wybierz instancję Ollama",
"Send a Message": "Wyślij Wiadomość",
"Send message": "Wyślij wiadomość",
"Server connection verified": "Połączenie z serwerem zweryfikowane",
"Set as default": "Ustaw jako domyślne",
"Set Default Model": "Ustaw domyślny model",
"Set Image Size": "Ustaw rozmiar obrazu",
"Set Steps": "Ustaw kroki",
"Set Title Auto-Generation Model": "Ustaw model automatycznego generowania tytułów",
"Set Voice": "Ustaw głos",
"Settings": "Ustawienia",
"Settings saved successfully!": "Ustawienia zapisane pomyślnie!",
"Share to OpenWebUI Community": "Dziel się z społecznością OpenWebUI",
"short-summary": "Krótkie podsumowanie",
"Show": "Pokaż",
"Show Additional Params": "Pokaż dodatkowe parametry",
"Show shortcuts": "Pokaż skróty",
"sidebar": "Panel boczny",
"Sign in": "Zaloguj się",
"Sign Out": "Wyloguj się",
"Sign up": "Zarejestruj się",
"Speech recognition error: {{error}}": "Błąd rozpoznawania mowy: {{error}}",
"Speech-to-Text Engine": "Silnik mowy na tekst",
"SpeechRecognition API is not supported in this browser.": "API Rozpoznawania Mowy nie jest obsługiwane w tej przeglądarce.",
"Stop Sequence": "Zatrzymaj sekwencję",
"STT Settings": "Ustawienia STT",
"Submit": "Zatwierdź",
"Success": "Sukces",
"Successfully updated.": "Pomyślnie zaktualizowano.",
"Sync All": "Synchronizuj wszystko",
"System": "System",
"System Prompt": "Prompt systemowy",
"Tags": "Tagi",
"Temperature": "Temperatura",
"Template": "Szablon",
"Text Completion": "Uzupełnienie tekstu",
"Text-to-Speech Engine": "Silnik tekstu na mowę",
"Tfs Z": "Tfs Z",
"Theme": "Motyw",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "To zapewnia, że Twoje cenne rozmowy są bezpiecznie zapisywane w bazie danych backendowej. Dziękujemy!",
"This setting does not sync across browsers or devices.": "To ustawienie nie synchronizuje się między przeglądarkami ani urządzeniami.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Porada: Aktualizuj wiele zmiennych kolejno, naciskając klawisz tabulatora w polu wprowadzania czatu po każdej zmianie.",
"Title": "Tytuł",
"Title Auto-Generation": "Automatyczne generowanie tytułu",
"Title Generation Prompt": "Prompt generowania tytułu",
"to": "do",
"To access the available model names for downloading,": "Aby uzyskać dostęp do dostępnych nazw modeli do pobrania,",
"To access the GGUF models available for downloading,": "Aby uzyskać dostęp do dostępnych modeli GGUF do pobrania,",
"to chat input.": "do pola wprowadzania czatu.",
"Toggle settings": "Przełącz ustawienia",
"Toggle sidebar": "Przełącz panel boczny",
"Top K": "Najlepsze K",
"Top P": "Najlepsze P",
"Trouble accessing Ollama?": "Problemy z dostępem do Ollama?",
"TTS Settings": "Ustawienia TTS",
"Type Hugging Face Resolve (Download) URL": "Wprowadź adres URL do pobrania z Hugging Face",
"Uh-oh! There was an issue connecting to {{provider}}.": "O nie! Wystąpił problem z połączeniem z {{provider}}.",
"Understand that updating or changing your embedding model requires reset of the vector database and re-import of all documents. You have been warned!": "Zrozum, że aktualizacja lub zmiana modelu osadzania wymaga zresetowania bazy wektorów i ponownego zaimportowania wszystkich dokumentów. Zostałeś ostrzeżony!",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Nieznany typ pliku '{{file_type}}', ale akceptowany i traktowany jako zwykły tekst",
"Update": "Aktualizacja",
"Update embedding model {{embedding_model}}": "Aktualizuj modelu osadzania {{embedding_model}}",
"Update password": "Aktualizacja hasła",
"Upload a GGUF model": "Prześlij model GGUF",
"Upload files": "Prześlij pliki",
"Upload Progress": "Postęp przesyłania",
"URL Mode": "Tryb adresu URL",
"Use '#' in the prompt input to load and select your documents.": "Użyj '#' w polu wprowadzania polecenia, aby załadować i wybrać swoje dokumenty.",
"Use Gravatar": "Użyj Gravatara",
"Use Initials": "Użyj inicjałów",
"user": "użytkownik",
"User Permissions": "Uprawnienia użytkownika",
"Users": "Użytkownicy",
"Utilize": "Wykorzystaj",
"Valid time units:": "Poprawne jednostki czasu:",
"variable": "zmienna",
"variable to have them replaced with clipboard content.": "zmienna która zostanie zastąpiona zawartością schowka.",
"Version": "Wersja",
"Web": "Sieć",
"WebUI Add-ons": "Dodatki do interfejsu WebUI",
"WebUI Settings": "Ustawienia interfejsu WebUI",
"WebUI will make requests to": "Interfejs sieciowy będzie wysyłał żądania do",
"Whats New in": "Co nowego w",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Kiedy historia jest wyłączona, nowe rozmowy na tej przeglądarce nie będą widoczne w historii na żadnym z twoich urządzeń.",
"Whisper (Local)": "Whisper (Lokalnie)",
"Write a prompt suggestion (e.g. Who are you?)": "Napisz sugestię do polecenia (np. Kim jesteś?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "Napisz podsumowanie w 50 słowach, które podsumowuje [temat lub słowo kluczowe].",
"You": "Ty",
"You're a helpful assistant.": "Jesteś pomocnym asystentem.",
"You're now logged in.": "Jesteś teraz zalogowany."
}

View file

@ -86,7 +86,7 @@
"Customize Ollama models for a specific purpose": "Personalize os modelos Ollama para um propósito específico",
"Dark": "Escuro",
"Database": "Banco de dados",
"DD/MM/YYYY HH:mm": "DD/MM/AAAA HH:mm",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "Padrão",
"Default (Automatic1111)": "Padrão (Automatic1111)",
"Default (Web API)": "Padrão (API Web)",

View file

@ -55,9 +55,9 @@
"Check for updates": "Kiểm tra cập nhật",
"Checking for updates...": "Đang kiểm tra cập nhật...",
"Choose a model before saving...": "Chọn mô hình trước khi lưu...",
"Chunk Overlap": "Kích thước chồng lấn (overlap)",
"Chunk Overlap": "Chồng lấn (overlap)",
"Chunk Params": "Cài đặt số lượng ký tự cho khối ký tự (chunk)",
"Chunk Size": "Kích thc khối (size)",
"Chunk Size": "Kích thước khối (size)",
"Click here for help.": "Bấm vào đây để được trợ giúp.",
"Click here to check other modelfiles.": "Bấm vào đây để kiểm tra các tệp mô tả mô hình (modelfiles) khác.",
"Click here to select": "Bấm vào đây để chọn",
@ -65,7 +65,7 @@
"click here.": "bấm vào đây.",
"Click on the user role button to change a user's role.": "Bấm vào nút trong cột VAI TRÒ để thay đổi quyền của người sử dụng.",
"Close": "Đóng",
"Collection": "Bộ sưu tập",
"Collection": "Tổng hợp mọi tài liệu",
"Command": "Lệnh",
"Confirm Password": "Xác nhận Mật khẩu",
"Connections": "Kết nối",
@ -76,7 +76,7 @@
"Copy last response": "Sao chép phản hồi cuối cùng",
"Copying to clipboard was successful!": "Sao chép vào clipboard thành công!",
"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':": "Tạo một cụm từ súc tích, 3-5 từ làm tiêu đề cho truy vấn sau, tuân thủ nghiêm ngặt giới hạn 3-5 từ và tránh sử dụng từ 'tiêu đề':",
"Create a modelfile": "Tạo tệp mô tả mô hình",
"Create a modelfile": "Tạo tệp mô tả cho mô hình",
"Create Account": "Tạo Tài khoản",
"Created at": "Được tạo vào lúc",
"Created by": "Được tạo bởi",
@ -347,7 +347,7 @@
"Valid time units:": "Đơn vị thời gian hợp lệ:",
"variable": "biến",
"variable to have them replaced with clipboard content.": "biến để có chúng được thay thế bằng nội dung clipboard.",
"Version": "Phiên bản",
"Version": "Version",
"Web": "Web",
"WebUI Add-ons": "Tiện ích WebUI",
"WebUI Settings": "Cài đặt WebUI",

View file

@ -467,3 +467,52 @@ export const blobToFile = (blob, fileName) => {
const file = new File([blob], fileName, { type: blob.type });
return file;
};
// promptTemplate replaces any occurrences of the following in the template with the prompt
// {{prompt}} will be replaced with the prompt
// {{prompt:start:<length>}} will be replaced with the first <length> characters of the prompt
// {{prompt:end:<length>}} will be replaced with the last <length> characters of the prompt
// Character length is used as we don't have the ability to tokenize the prompt
export const promptTemplate = (template: string, prompt: string) => {
template = template.replace(/{{prompt}}/g, prompt);
// Replace all instances of {{prompt:start:<length>}} with the first <length> characters of the prompt
const startRegex = /{{prompt:start:(\d+)}}/g;
let startMatch: RegExpMatchArray | null;
while ((startMatch = startRegex.exec(template)) !== null) {
const length = parseInt(startMatch[1]);
template = template.replace(startMatch[0], prompt.substring(0, length));
}
// Replace all instances of {{prompt:end:<length>}} with the last <length> characters of the prompt
const endRegex = /{{prompt:end:(\d+)}}/g;
let endMatch: RegExpMatchArray | null;
while ((endMatch = endRegex.exec(template)) !== null) {
const length = parseInt(endMatch[1]);
template = template.replace(endMatch[0], prompt.substring(prompt.length - length));
}
return template;
};
export const approximateToHumanReadable = (nanoseconds: number) => {
const seconds = Math.floor((nanoseconds / 1e9) % 60);
const minutes = Math.floor((nanoseconds / 6e10) % 60);
const hours = Math.floor((nanoseconds / 3.6e12) % 24);
const results: string[] = [];
if (seconds >= 0) {
results.push(`${seconds}s`);
}
if (minutes > 0) {
results.push(`${minutes}m`);
}
if (hours > 0) {
results.push(`${hours}h`);
}
return results.reverse().join(' ');
};

View file

@ -106,11 +106,6 @@
// IndexedDB Not Found
}
console.log();
await models.set(await getModels());
await tick();
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
await modelfiles.set(await getModelfiles(localStorage.token));

View file

@ -849,9 +849,6 @@
shareEnabled={messages.length > 0}
{chat}
{initNewChat}
{tags}
{addTag}
{deleteTag}
/>
<div class="flex flex-col flex-auto">
<div

View file

@ -12,6 +12,7 @@
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
import EditUserModal from '$lib/components/admin/EditUserModal.svelte';
import SettingsModal from '$lib/components/admin/SettingsModal.svelte';
import Pagination from '$lib/components/common/Pagination.svelte';
const i18n = getContext('i18n');
@ -21,6 +22,8 @@
let search = '';
let selectedUser = null;
let page = 1;
let showSettingsModal = false;
let showEditUserModal = false;
@ -159,15 +162,17 @@
</tr>
</thead>
<tbody>
{#each users.filter((user) => {
if (search === '') {
return true;
} else {
let name = user.name.toLowerCase();
const query = search.toLowerCase();
return name.includes(query);
}
}) as user}
{#each users
.filter((user) => {
if (search === '') {
return true;
} else {
let name = user.name.toLowerCase();
const query = search.toLowerCase();
return name.includes(query);
}
})
.slice((page - 1) * 20, page * 20) as user}
<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700 text-xs">
<td class="px-3 py-2 min-w-[7rem] w-28">
<button
@ -270,6 +275,8 @@
<div class=" text-gray-500 text-xs mt-2 text-right">
{$i18n.t("Click on the user role button to change a user's role.")}
</div>
<Pagination bind:page count={users.length} />
</div>
</div>
</div>

View file

@ -30,14 +30,12 @@
getTagsById,
updateChatById
} from '$lib/apis/chats';
import { queryCollection, queryDoc } from '$lib/apis/rag';
import { generateOpenAIChatCompletion, generateTitle } from '$lib/apis/openai';
import MessageInput from '$lib/components/chat/MessageInput.svelte';
import Messages from '$lib/components/chat/Messages.svelte';
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte';
import { RAGTemplate } from '$lib/utils/rag';
import {
LITELLM_API_BASE_URL,
OPENAI_API_BASE_URL,
@ -877,9 +875,6 @@
goto('/');
}}
{tags}
{addTag}
{deleteTag}
/>
<div class="flex flex-col flex-auto">
<div