Merge branch 'dev' into embedding-model-fix-and-manual-update

This commit is contained in:
lainedfles 2024-04-08 14:57:54 -06:00 committed by GitHub
commit 506a061387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1906 additions and 520 deletions

View file

@ -10,3 +10,7 @@ OPENAI_API_KEY=''
# DO NOT TRACK # DO NOT TRACK
SCARF_NO_ANALYTICS=true SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true DO_NOT_TRACK=true
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
LITELLM_LOCAL_MODEL_COST_MAP="True"

View file

@ -57,3 +57,14 @@ jobs:
path: . path: .
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Docker build workflow
uses: actions/github-script@v7
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'docker-build.yaml',
ref: 'v${{ steps.get_version.outputs.version }}',
})

View file

@ -1,8 +1,8 @@
# name: Create and publish Docker images with specific build args
name: Create and publish a Docker image
# Configures this workflow to run every time a change is pushed to the branch called `release`. # Configures this workflow to run every time a change is pushed to the branch called `release`.
on: on:
workflow_dispatch:
push: push:
branches: branches:
- main - main
@ -23,7 +23,7 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
#
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -41,12 +41,11 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images - name: Extract metadata for Docker images (default latest tag)
id: meta id: meta-latest
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This configuration dynamically generates tags based on the branch, tag, commit, and custom suffix for lite version.
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=tag type=ref,event=tag
@ -56,11 +55,29 @@ jobs:
flavor: | flavor: |
latest=${{ github.ref == 'refs/heads/main' }} latest=${{ github.ref == 'refs/heads/main' }}
- name: Build and push Docker image - name: Build and push Docker image (latest)
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta-latest.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta-latest.outputs.labels }}
- name: Build and push Docker image with CUDA
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cuda
build-args: USE_CUDA=true
- name: Build and push Docker image with Ollama
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:ollama
build-args: USE_OLLAMA=true

View file

@ -1,78 +1,116 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Initialize device type args
# use build args in the docker build commmand with --build-arg="BUILDARG=true"
ARG USE_CUDA=false
ARG USE_OLLAMA=false
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
ARG USE_CUDA_VER=cu121
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
# IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
ARG USE_EMBEDDING_MODEL=all-MiniLM-L6-v2
FROM node:alpine as build ######## WebUI frontend ########
FROM node:21-alpine3.19 as build
WORKDIR /app WORKDIR /app
# wget embedding model weight from alpine (does not exist from slim-buster)
RUN wget "https://chroma-onnx-models.s3.amazonaws.com/all-MiniLM-L6-v2/onnx.tar.gz" -O - | \
tar -xzf - -C /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci RUN npm ci
COPY . . COPY . .
RUN npm run build RUN npm run build
######## WebUI backend ########
FROM python:3.11-slim-bookworm as base FROM python:3.11-slim-bookworm as base
ENV ENV=prod # Use args
ENV PORT "" ARG USE_CUDA
ARG USE_OLLAMA
ARG USE_CUDA_VER
ARG USE_EMBEDDING_MODEL
ENV OLLAMA_BASE_URL "/ollama" ## Basis ##
ENV ENV=prod \
PORT=8080 \
# pass build args to the build
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
USE_CUDA_DOCKER=${USE_CUDA} \
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL}
ENV OPENAI_API_BASE_URL "" ## Basis URL Config ##
ENV OPENAI_API_KEY "" ENV OLLAMA_BASE_URL="/ollama" \
OPENAI_API_BASE_URL=""
ENV WEBUI_SECRET_KEY "" ## API Key and Security Config ##
ENV WEBUI_AUTH_TRUSTED_EMAIL_HEADER "" ENV OPENAI_API_KEY="" \
WEBUI_SECRET_KEY="" \
SCARF_NO_ANALYTICS=true \
DO_NOT_TRACK=true
ENV SCARF_NO_ANALYTICS true # Use locally bundled version of the LiteLLM cost map json
ENV DO_NOT_TRACK true # to avoid repetitive startup connections
ENV LITELLM_LOCAL_MODEL_COST_MAP="True"
######## Preloaded models ########
# whisper TTS Settings
ENV WHISPER_MODEL="base"
ENV WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
# RAG Embedding Model Settings #### Other models #########################################################
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers ## whisper TTS model settings ##
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard ENV WHISPER_MODEL="base" \
# for better persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB) WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
# IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2"
# device type for whisper tts and embbeding models - "cpu" (default), "cuda" (nvidia gpu and CUDA required) or "mps" (apple silicon) - choosing this right can lead to better performance
ENV RAG_EMBEDDING_MODEL_DEVICE_TYPE="cpu"
ENV RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models"
ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR
######## Preloaded models ######## ## RAG Embedding model settings ##
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
#### Other models ##########################################################
WORKDIR /app/backend WORKDIR /app/backend
# install python dependencies # install python dependencies
COPY ./backend/requirements.txt ./requirements.txt COPY ./backend/requirements.txt ./requirements.txt
RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y RUN if [ "$USE_CUDA" = "true" ]; then \
# If you use CUDA the whisper and embedding modell 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 && \
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 && \
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
RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir
RUN pip3 install -r requirements.txt --no-cache-dir
# Install pandoc and netcat RUN if [ "$USE_OLLAMA" = "true" ]; then \
# RUN python -c "import pypandoc; pypandoc.download_pandoc()" apt-get update && \
RUN apt-get update \ # Install pandoc and netcat
&& apt-get install -y pandoc netcat-openbsd \ apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
&& rm -rf /var/lib/apt/lists/* # for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# install helper tools
apt-get install -y --no-install-recommends curl && \
# install ollama
curl -fsSL https://ollama.com/install.sh | sh && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
else \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
fi
# preload embedding model
RUN python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device=os.environ['RAG_EMBEDDING_MODEL_DEVICE_TYPE'])"
# preload tts model
RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='auto', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"
# copy embedding weight from build # copy embedding weight from build
RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 # RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
# copy built frontend files # copy built frontend files
COPY --from=build /app/build /app/build COPY --from=build /app/build /app/build
@ -82,4 +120,6 @@ COPY --from=build /app/package.json /app/package.json
# copy backend files # copy backend files
COPY ./backend . COPY ./backend .
EXPOSE 8080
CMD [ "bash", "start.sh"] CMD [ "bash", "start.sh"]

View file

@ -113,6 +113,65 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
- After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄 - After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
- **If you want to customize your build with additional args**, use this commands:
> [!NOTE]
> If you only want to use Open WebUI with Ollama included or CUDA acelleration it's recomented to use our official images with the tags :cuda or :with-ollama
> If you want a combination of both or more customisation options like a different embedding model and/or CUDA version you need to build the image yourself following the instructions below.
**For the build:**
```bash
docker build -t open-webui
```
Optional build ARGS (use them in the docker build command below if needed):
e.g.
```bash
--build-arg="USE_EMBEDDING_MODEL=intfloat/multilingual-e5-large"
```
For "intfloat/multilingual-e5-large" custom embedding model (default is all-MiniLM-L6-v2), only works with [sentence transforer models](https://huggingface.co/models?library=sentence-transformers). Current [Leaderbord](https://huggingface.co/spaces/mteb/leaderboard) of embedding models.
```bash
--build-arg="USE_OLLAMA=true"
```
For including ollama in the image.
```bash
--build-arg="USE_CUDA=true"
```
To use CUDA exeleration for the embedding and whisper models.
> [!NOTE]
> You need to install the [Nvidia CUDA container toolkit](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/) on your machine to be able to set CUDA as the Docker engine. Only works with Linux - use WSL for Windows!
```bash
--build-arg="USE_CUDA_VER=cu117"
```
For CUDA 11 (default is CUDA 12)
**To run the image:**
- **If you DID NOT use the USE_CUDA=true build ARG**, use this command:
```bash
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
- **If you DID use the USE_CUDA=true build ARG**, use this command:
```bash
docker run --gpus all -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
- After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
#### Open WebUI: Server Connection Error #### Open WebUI: Server Connection Error
If you're experiencing connection issues, its often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`. If you're experiencing connection issues, its often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.

View file

@ -28,6 +28,7 @@ from config import (
UPLOAD_DIR, UPLOAD_DIR,
WHISPER_MODEL, WHISPER_MODEL,
WHISPER_MODEL_DIR, WHISPER_MODEL_DIR,
DEVICE_TYPE,
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -42,6 +43,10 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
log.info(f"whisper_device_type: {whisper_device_type}")
@app.post("/transcribe") @app.post("/transcribe")
def transcribe( def transcribe(
@ -66,7 +71,7 @@ def transcribe(
model = WhisperModel( model = WhisperModel(
WHISPER_MODEL, WHISPER_MODEL,
device="auto", device=whisper_device_type,
compute_type="int8", compute_type="int8",
download_root=WHISPER_MODEL_DIR, download_root=WHISPER_MODEL_DIR,
) )

View file

@ -215,7 +215,8 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
if len(responses) > 0: 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("-")[0].split("."))),
) )
return {"version": lowest_version["version"]} return {"version": lowest_version["version"]}

View file

@ -58,8 +58,8 @@ from config import (
UPLOAD_DIR, UPLOAD_DIR,
DOCS_DIR, DOCS_DIR,
RAG_EMBEDDING_MODEL, RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_DEVICE_TYPE,
RAG_EMBEDDING_MODEL_AUTO_UPDATE, RAG_EMBEDDING_MODEL_AUTO_UPDATE,
DEVICE_TYPE,
CHROMA_CLIENT, CHROMA_CLIENT,
CHUNK_SIZE, CHUNK_SIZE,
CHUNK_OVERLAP, CHUNK_OVERLAP,
@ -86,7 +86,7 @@ app.state.TOP_K = 4
app.state.sentence_transformer_ef = ( app.state.sentence_transformer_ef = (
embedding_functions.SentenceTransformerEmbeddingFunction( embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=app.state.RAG_EMBEDDING_MODEL_PATH, model_name=app.state.RAG_EMBEDDING_MODEL_PATH,
device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, device=DEVICE_TYPE,
) )
) )
@ -154,7 +154,7 @@ async def update_embedding_model(
app.state.sentence_transformer_ef = ( app.state.sentence_transformer_ef = (
embedding_functions.SentenceTransformerEmbeddingFunction( embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=app.state.RAG_EMBEDDING_MODEL_PATH, model_name=app.state.RAG_EMBEDDING_MODEL_PATH,
device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, device=DEVICE_TYPE,
) )
) )
except Exception as e: except Exception as e:
@ -471,25 +471,11 @@ def store_doc(
log.info(f"file.content_type: {file.content_type}") log.info(f"file.content_type: {file.content_type}")
try: try:
is_valid_filename = True
unsanitized_filename = file.filename unsanitized_filename = file.filename
if re.search(r'[\\/:"\*\?<>|\n\t ]', unsanitized_filename) is not None: filename = os.path.basename(unsanitized_filename)
is_valid_filename = False
unvalidated_file_path = f"{UPLOAD_DIR}/{unsanitized_filename}" file_path = f"{UPLOAD_DIR}/{filename}"
dereferenced_file_path = str(Path(unvalidated_file_path).resolve(strict=False))
if not dereferenced_file_path.startswith(UPLOAD_DIR):
is_valid_filename = False
if is_valid_filename:
file_path = dereferenced_file_path
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(),
)
filename = file.filename
contents = file.file.read() contents = file.file.read()
with open(file_path, "wb") as f: with open(file_path, "wb") as f:
f.write(contents) f.write(contents)
@ -500,7 +486,7 @@ def store_doc(
collection_name = calculate_sha256(f)[:63] collection_name = calculate_sha256(f)[:63]
f.close() f.close()
loader, known_type = get_loader(file.filename, file.content_type, file_path) loader, known_type = get_loader(filename, file.content_type, file_path)
data = loader.load() data = loader.load()
try: try:

View file

@ -86,6 +86,7 @@ class SignupForm(BaseModel):
name: str name: str
email: str email: str
password: str password: str
profile_image_url: Optional[str] = "/user.png"
class AuthsTable: class AuthsTable:
@ -94,7 +95,12 @@ class AuthsTable:
self.db.create_tables([Auth]) self.db.create_tables([Auth])
def insert_new_auth( def insert_new_auth(
self, email: str, password: str, name: str, role: str = "pending" self,
email: str,
password: str,
name: str,
profile_image_url: str = "/user.png",
role: str = "pending",
) -> Optional[UserModel]: ) -> Optional[UserModel]:
log.info("insert_new_auth") log.info("insert_new_auth")
@ -105,7 +111,7 @@ class AuthsTable:
) )
result = Auth.create(**auth.model_dump()) result = Auth.create(**auth.model_dump())
user = Users.insert_new_user(id, name, email, role) user = Users.insert_new_user(id, name, email, profile_image_url, role)
if result and user: if result and user:
return user return user

View file

@ -206,6 +206,18 @@ class ChatTable:
except: except:
return None return None
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.share_id == id)
if chat:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
else:
return None
except:
return None
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
try: try:
chat = Chat.get(Chat.id == id, Chat.user_id == user_id) chat = Chat.get(Chat.id == id, Chat.user_id == user_id)

View file

@ -31,7 +31,7 @@ class UserModel(BaseModel):
name: str name: str
email: str email: str
role: str = "pending" role: str = "pending"
profile_image_url: str = "/user.png" profile_image_url: str
timestamp: int # timestamp in epoch timestamp: int # timestamp in epoch
api_key: Optional[str] = None api_key: Optional[str] = None
@ -59,7 +59,12 @@ class UsersTable:
self.db.create_tables([User]) self.db.create_tables([User])
def insert_new_user( def insert_new_user(
self, id: str, name: str, email: str, role: str = "pending" self,
id: str,
name: str,
email: str,
profile_image_url: str = "/user.png",
role: str = "pending",
) -> Optional[UserModel]: ) -> Optional[UserModel]:
user = UserModel( user = UserModel(
**{ **{
@ -67,7 +72,7 @@ class UsersTable:
"name": name, "name": name,
"email": email, "email": email,
"role": role, "role": role,
"profile_image_url": "/user.png", "profile_image_url": profile_image_url,
"timestamp": int(time.time()), "timestamp": int(time.time()),
} }
) )

View file

@ -163,7 +163,11 @@ async def signup(request: Request, form_data: SignupForm):
) )
hashed = get_password_hash(form_data.password) hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth( user = Auths.insert_new_auth(
form_data.email.lower(), hashed, form_data.name, role form_data.email.lower(),
hashed,
form_data.name,
form_data.profile_image_url,
role,
) )
if user: if user:

View file

@ -251,6 +251,14 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
@router.get("/share/{share_id}", response_model=Optional[ChatResponse]) @router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)): async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
if user.role == "pending":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
chat = Chats.get_chat_by_id(share_id) chat = Chats.get_chat_by_id(share_id)
if chat: if chat:

View file

@ -1,16 +1,11 @@
from fastapi import APIRouter, UploadFile, File, BackgroundTasks from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from starlette.responses import StreamingResponse, FileResponse from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel from pydantic import BaseModel
from fpdf import FPDF
import markdown import markdown
import requests
import os
import aiohttp
import json
from utils.utils import get_admin_user from utils.utils import get_admin_user
@ -18,7 +13,7 @@ from utils.misc import calculate_sha256, get_gravatar_url
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
from typing import List
router = APIRouter() router = APIRouter()
@ -41,6 +36,59 @@ async def get_html_from_markdown(
return {"html": markdown.markdown(form_data.md)} return {"html": markdown.markdown(form_data.md)}
class ChatForm(BaseModel):
title: str
messages: List[dict]
@router.post("/pdf")
async def download_chat_as_pdf(
form_data: ChatForm,
):
pdf = FPDF()
pdf.add_page()
STATIC_DIR = "./static"
FONTS_DIR = f"{STATIC_DIR}/fonts"
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
pdf.set_font("NotoSans", size=12)
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"])
pdf.set_auto_page_break(auto=True, margin=15)
# Adjust the effective page width for multi_cell
effective_page_width = (
pdf.w - 2 * pdf.l_margin - 10
) # Subtracted an additional 10 for extra padding
# Add chat messages
for message in form_data.messages:
role = message["role"]
content = message["content"]
pdf.set_font("NotoSans", "B", size=14) # Bold for the role
pdf.multi_cell(effective_page_width, 10, f"{role.upper()}", 0, "L")
pdf.ln(1) # Extra space between messages
pdf.set_font("NotoSans", size=10) # Regular for content
pdf.multi_cell(effective_page_width, 6, content, 0, "L")
pdf.ln(1.5) # Extra space between messages
# Save the pdf with name .pdf
pdf_bytes = pdf.output()
return Response(
content=bytes(pdf_bytes),
media_type="application/pdf",
headers={"Content-Disposition": f"attachment;filename=chat.pdf"},
)
@router.get("/db/download") @router.get("/db/download")
async def download_db(user=Depends(get_admin_user)): async def download_db(user=Depends(get_admin_user)):

View file

@ -257,6 +257,7 @@ OLLAMA_API_BASE_URL = os.environ.get(
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
K8S_FLAG = os.environ.get("K8S_FLAG", "") K8S_FLAG = os.environ.get("K8S_FLAG", "")
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
OLLAMA_BASE_URL = ( OLLAMA_BASE_URL = (
@ -266,9 +267,13 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
) )
if ENV == "prod": if ENV == "prod":
if OLLAMA_BASE_URL == "/ollama": if OLLAMA_BASE_URL == "/ollama" and not K8S_FLAG:
if USE_OLLAMA_DOCKER.lower() == "true":
# if you use all-in-one docker container (Open WebUI + Ollama)
# with the docker build arg USE_OLLAMA=true (--build-arg="USE_OLLAMA=true") this only works with http://localhost:11434
OLLAMA_BASE_URL = "http://localhost:11434"
else:
OLLAMA_BASE_URL = "http://host.docker.internal:11434" OLLAMA_BASE_URL = "http://host.docker.internal:11434"
elif K8S_FLAG: elif K8S_FLAG:
OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434" OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434"
@ -391,13 +396,21 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" 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) # 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_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2") RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2")
# device type ebbeding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
RAG_EMBEDDING_MODEL_DEVICE_TYPE = os.environ.get(
"RAG_EMBEDDING_MODEL_DEVICE_TYPE", "cpu"
)
RAG_EMBEDDING_MODEL_AUTO_UPDATE = False RAG_EMBEDDING_MODEL_AUTO_UPDATE = False
if os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true": if os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true":
RAG_EMBEDDING_MODEL_AUTO_UPDATE = True RAG_EMBEDDING_MODEL_AUTO_UPDATE = True
# device type ebbeding 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":
DEVICE_TYPE = "cuda"
else:
DEVICE_TYPE = "cpu"
CHROMA_CLIENT = chromadb.PersistentClient( CHROMA_CLIENT = chromadb.PersistentClient(
path=CHROMA_DATA_PATH, path=CHROMA_DATA_PATH,
settings=Settings(allow_reset=True, anonymized_telemetry=False), settings=Settings(allow_reset=True, anonymized_telemetry=False),

View file

@ -42,6 +42,8 @@ xlrd
opencv-python-headless opencv-python-headless
rapidocr-onnxruntime rapidocr-onnxruntime
fpdf2
faster-whisper faster-whisper
PyJWT PyJWT

View file

@ -7,16 +7,26 @@ KEY_FILE=.webui_secret_key
PORT="${PORT:-8080}" PORT="${PORT:-8080}"
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
echo No WEBUI_SECRET_KEY provided echo "No WEBUI_SECRET_KEY provided"
if ! [ -e "$KEY_FILE" ]; then if ! [ -e "$KEY_FILE" ]; then
echo Generating WEBUI_SECRET_KEY echo "Generating WEBUI_SECRET_KEY"
# Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one. # Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one.
echo $(head -c 12 /dev/random | base64) > $KEY_FILE echo $(head -c 12 /dev/random | base64) > "$KEY_FILE"
fi fi
echo Loading WEBUI_SECRET_KEY from $KEY_FILE echo "Loading WEBUI_SECRET_KEY from $KEY_FILE"
WEBUI_SECRET_KEY=`cat $KEY_FILE` WEBUI_SECRET_KEY=$(cat "$KEY_FILE")
fi
if [ "$USE_OLLAMA_DOCKER" = "true" ]; then
echo "USE_OLLAMA is set to true, starting ollama serve."
ollama serve &
fi
if [ "$USE_CUDA_DOCKER" = "true" ]; then
echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
fi fi
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*' WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,8 @@
services:
ollama:
devices:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
image: ollama/ollama:${OLLAMA_DOCKER_TAG-rocm}
environment:
- 'HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION-11.0.0}'

View file

@ -8,7 +8,7 @@ services:
pull_policy: always pull_policy: always
tty: true tty: true
restart: unless-stopped restart: unless-stopped
image: ollama/ollama:latest image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest}
open-webui: open-webui:
build: build:
@ -16,7 +16,7 @@ services:
args: args:
OLLAMA_BASE_URL: '/ollama' OLLAMA_BASE_URL: '/ollama'
dockerfile: Dockerfile dockerfile: Dockerfile
image: ghcr.io/open-webui/open-webui:main image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
container_name: open-webui container_name: open-webui
volumes: volumes:
- open-webui:/app/backend/data - open-webui:/app/backend/data

View file

@ -7,7 +7,7 @@ ollama
{{- end -}} {{- end -}}
{{- define "ollama.url" -}} {{- define "ollama.url" -}}
{{- printf "http://%s.%s.svc.cluster.local:%d/api" (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 }}
{{- define "chart.name" -}} {{- define "chart.name" -}}

View file

@ -58,7 +58,12 @@ export const userSignIn = async (email: string, password: string) => {
return res; return res;
}; };
export const userSignUp = async (name: string, email: string, password: string) => { export const userSignUp = async (
name: string,
email: string,
password: string,
profile_image_url: string
) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, { const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
@ -69,7 +74,8 @@ export const userSignUp = async (name: string, email: string, password: string)
body: JSON.stringify({ body: JSON.stringify({
name: name, name: name,
email: email, email: email,
password: password password: password,
profile_image_url: profile_image_url
}) })
}) })
.then(async (res) => { .then(async (res) => {

View file

@ -22,6 +22,32 @@ export const getGravatarUrl = async (email: string) => {
return res; return res;
}; };
export const downloadChatAsPDF = async (chat: object) => {
let error = null;
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: chat.title,
messages: chat.messages
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.blob();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
return blob;
};
export const getHTMLFromMarkdown = async (md: string) => { export const getHTMLFromMarkdown = async (md: string) => {
let error = null; let error = null;

View file

@ -295,6 +295,13 @@
const dropZone = document.querySelector('body'); const dropZone = document.querySelector('body');
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
console.log('Escape');
dragged = false;
}
};
const onDragOver = (e) => { const onDragOver = (e) => {
e.preventDefault(); e.preventDefault();
dragged = true; dragged = true;
@ -350,11 +357,15 @@
dragged = false; dragged = false;
}; };
window.addEventListener('keydown', handleKeyDown);
dropZone?.addEventListener('dragover', onDragOver); dropZone?.addEventListener('dragover', onDragOver);
dropZone?.addEventListener('drop', onDrop); dropZone?.addEventListener('drop', onDrop);
dropZone?.addEventListener('dragleave', onDragLeave); dropZone?.addEventListener('dragleave', onDragLeave);
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown);
dropZone?.removeEventListener('dragover', onDragOver); dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop); dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave); dropZone?.removeEventListener('dragleave', onDragLeave);

View file

@ -107,12 +107,8 @@
await sendPrompt(userPrompt, userMessageId, chatId); await sendPrompt(userPrompt, userMessageId, chatId);
}; };
const confirmEditResponseMessage = async (messageId, content) => { const updateChatMessages = async () => {
history.messages[messageId].originalContent = history.messages[messageId].content;
history.messages[messageId].content = content;
await tick(); await tick();
await updateChatById(localStorage.token, chatId, { await updateChatById(localStorage.token, chatId, {
messages: messages, messages: messages,
history: history history: history
@ -121,15 +117,20 @@
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
}; };
const rateMessage = async (messageId, rating) => { const confirmEditResponseMessage = async (messageId, content) => {
history.messages[messageId].rating = rating; history.messages[messageId].originalContent = history.messages[messageId].content;
await tick(); history.messages[messageId].content = content;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
await chats.set(await getChatList(localStorage.token)); await updateChatMessages();
};
const rateMessage = async (messageId, rating) => {
history.messages[messageId].annotation = {
...history.messages[messageId].annotation,
rating: rating
};
await updateChatMessages();
}; };
const showPreviousMessage = async (message) => { const showPreviousMessage = async (message) => {
@ -338,6 +339,7 @@
siblings={history.messages[message.parentId]?.childrenIds ?? []} siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length} isLastMessage={messageIdx + 1 === messages.length}
{readOnly} {readOnly}
{updateChatMessages}
{confirmEditResponseMessage} {confirmEditResponseMessage}
{showPreviousMessage} {showPreviousMessage}
{showNextMessage} {showNextMessage}

View file

@ -0,0 +1,117 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
export let show = false;
export let message;
const LIKE_REASONS = [
`Accurate information`,
`Followed instructions perfectly`,
`Showcased creativity`,
`Positive attitude`,
`Attention to detail`,
`Thorough explanation`,
`Other`
];
const DISLIKE_REASONS = [
`Don't like the style`,
`Not factually correct`,
`Didn't fully follow instructions`,
`Refused when it shouldn't have`,
`Being Lazy`,
`Other`
];
let reasons = [];
let selectedReason = null;
let comment = '';
$: if (message.annotation.rating === 1) {
reasons = LIKE_REASONS;
} else if (message.annotation.rating === -1) {
reasons = DISLIKE_REASONS;
}
onMount(() => {
selectedReason = message.annotation.reason;
comment = message.annotation.comment;
});
const submitHandler = () => {
console.log('submitHandler');
message.annotation.reason = selectedReason;
message.annotation.comment = comment;
dispatch('submit');
toast.success('Thanks for your feedback!');
show = false;
};
</script>
<div class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850">
<div class="flex justify-between items-center">
<div class=" text-sm">Tell us more:</div>
<button
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
{#if reasons.length > 0}
<div class="flex flex-wrap gap-2 text-sm mt-2.5">
{#each reasons as reason}
<button
class="px-3.5 py-1 border dark:border-gray-850 dark:hover:bg-gray-850 {selectedReason ===
reason
? 'dark:bg-gray-800'
: ''} transition rounded-lg"
on:click={() => {
selectedReason = reason;
}}
>
{reason}
</button>
{/each}
</div>
{/if}
<div class="mt-2">
<textarea
bind:value={comment}
class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl"
placeholder="Feel free to add specific details"
rows="2"
/>
</div>
<div class="mt-2 flex justify-end">
<button
class=" bg-emerald-700 text-white text-sm font-medium rounded-lg px-3.5 py-1.5"
on:click={() => {
submitHandler();
}}
>
Submit
</button>
</div>
</div>

View file

@ -30,6 +30,7 @@
import Image from '$lib/components/common/Image.svelte'; import Image from '$lib/components/common/Image.svelte';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import RateComment from './RateComment.svelte';
export let modelfiles = []; export let modelfiles = [];
export let message; export let message;
@ -39,6 +40,7 @@
export let readOnly = false; export let readOnly = false;
export let updateChatMessages: Function;
export let confirmEditResponseMessage: Function; export let confirmEditResponseMessage: Function;
export let showPreviousMessage: Function; export let showPreviousMessage: Function;
export let showNextMessage: Function; export let showNextMessage: Function;
@ -60,6 +62,8 @@
let loadingSpeech = false; let loadingSpeech = false;
let generatingImage = false; let generatingImage = false;
let showRateComment = false;
$: tokens = marked.lexer(sanitizeResponseContent(message.content)); $: tokens = marked.lexer(sanitizeResponseContent(message.content));
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
@ -536,11 +540,13 @@
<button <button
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1 : 'invisible group-hover:visible'} p-1 rounded {message?.annotation
?.rating === 1
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition" : ''} dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
rateMessage(message.id, 1); rateMessage(message.id, 1);
showRateComment = true;
}} }}
> >
<svg <svg
@ -563,11 +569,13 @@
<button <button
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1 : 'invisible group-hover:visible'} p-1 rounded {message?.annotation
?.rating === -1
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition" : ''} dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
rateMessage(message.id, -1); rateMessage(message.id, -1);
showRateComment = true;
}} }}
> >
<svg <svg
@ -824,6 +832,16 @@
{/if} {/if}
</div> </div>
{/if} {/if}
{#if showRateComment}
<RateComment
bind:show={showRateComment}
bind:message
on:submit={() => {
updateChatMessages();
}}
/>
{/if}
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -7,6 +7,7 @@
import UpdatePassword from './Account/UpdatePassword.svelte'; import UpdatePassword from './Account/UpdatePassword.svelte';
import { getGravatarUrl } from '$lib/apis/utils'; import { getGravatarUrl } from '$lib/apis/utils';
import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import Plus from '$lib/components/icons/Plus.svelte'; import Plus from '$lib/components/icons/Plus.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
@ -18,6 +19,8 @@
let profileImageUrl = ''; let profileImageUrl = '';
let name = ''; let name = '';
let showAPIKeys = false;
let showJWTToken = false; let showJWTToken = false;
let JWTTokenCopied = false; let JWTTokenCopied = false;
@ -28,6 +31,12 @@
let profileImageInputElement: HTMLInputElement; let profileImageInputElement: HTMLInputElement;
const submitHandler = async () => { const submitHandler = async () => {
if (name !== $user.name) {
if (profileImageUrl === generateInitialsImage($user.name) || profileImageUrl === '') {
profileImageUrl = generateInitialsImage(name);
}
}
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch( const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
(error) => { (error) => {
toast.error(error); toast.error(error);
@ -125,11 +134,12 @@
}} }}
/> />
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div> <div class="space-y-1">
<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> -->
<div class="flex space-x-5"> <div class="flex space-x-5">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="self-center"> <div class="self-center mt-2">
<button <button
class="relative rounded-full dark:bg-gray-700" class="relative rounded-full dark:bg-gray-700"
type="button" type="button"
@ -138,9 +148,9 @@
}} }}
> >
<img <img
src={profileImageUrl !== '' ? profileImageUrl : '/user.png'} src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)}
alt="profile" alt="profile"
class=" rounded-full w-16 h-16 object-cover" class=" rounded-full size-16 object-cover"
/> />
<div <div
@ -161,23 +171,56 @@
</div> </div>
</button> </button>
</div> </div>
</div>
<div class="flex-1 flex flex-col self-center gap-0.5">
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('Profile Image')}</div>
<div>
<button <button
class=" text-xs text-gray-600" class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-full px-4 py-0.5 bg-gray-100 dark:bg-gray-850"
on:click={async () => {
if (canvasPixelTest()) {
profileImageUrl = generateInitialsImage(name);
} else {
toast.info(
$i18n.t(
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
),
{
duration: 1000 * 10
}
);
}
}}>{$i18n.t('Use Initials')}</button
>
<button
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-full px-4 py-0.5 bg-gray-100 dark:bg-gray-850"
on:click={async () => { on:click={async () => {
const url = await getGravatarUrl($user.email); const url = await getGravatarUrl($user.email);
profileImageUrl = url; profileImageUrl = url;
}}>{$i18n.t('Use Gravatar')}</button }}>{$i18n.t('Use Gravatar')}</button
> >
<button
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg px-2 py-1"
on:click={async () => {
profileImageUrl = '/user.png';
}}>{$i18n.t('Remove')}</button
>
</div>
</div>
</div> </div>
<div class="flex-1"> <div class="pt-0.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div> <div class=" mb-1 text-xs font-medium">{$i18n.t('Name')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
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"
type="text" type="text"
bind:value={name} bind:value={name}
required required
@ -187,11 +230,24 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-4" /> <div class="py-0.5">
<UpdatePassword /> <UpdatePassword />
</div>
<hr class=" dark:border-gray-700 my-4" /> <hr class=" dark:border-gray-700 my-4" />
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">{$i18n.t('API keys')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showAPIKeys = !showAPIKeys;
}}>{showAPIKeys ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
{#if showAPIKeys}
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="justify-between w-full"> <div class="justify-between w-full">
<div class="flex justify-between w-full"> <div class="flex justify-between w-full">
@ -201,14 +257,14 @@
<div class="flex mt-2"> <div class="flex mt-2">
<div class="flex w-full"> <div class="flex w-full">
<input <input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
type={showJWTToken ? 'text' : 'password'} type={showJWTToken ? 'text' : 'password'}
value={localStorage.token} value={localStorage.token}
disabled disabled
/> />
<button <button
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800" class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850"
on:click={() => { on:click={() => {
showJWTToken = !showJWTToken; showJWTToken = !showJWTToken;
}} }}
@ -248,7 +304,7 @@
</div> </div>
<button <button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
on:click={() => { on:click={() => {
copyToClipboard(localStorage.token); copyToClipboard(localStorage.token);
JWTTokenCopied = true; JWTTokenCopied = true;
@ -301,14 +357,14 @@
{#if APIKey} {#if APIKey}
<div class="flex w-full"> <div class="flex w-full">
<input <input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
type={showAPIKey ? 'text' : 'password'} type={showAPIKey ? 'text' : 'password'}
value={APIKey} value={APIKey}
disabled disabled
/> />
<button <button
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800" class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850"
on:click={() => { on:click={() => {
showAPIKey = !showAPIKey; showAPIKey = !showAPIKey;
}} }}
@ -348,7 +404,7 @@
</div> </div>
<button <button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
on:click={() => { on:click={() => {
copyToClipboard(APIKey); copyToClipboard(APIKey);
APIKeyCopied = true; APIKeyCopied = true;
@ -393,7 +449,7 @@
<Tooltip content="Create new key"> <Tooltip content="Create new key">
<button <button
class=" px-1.5 py-1 dark:hover:bg-gray-800transition rounded-lg" class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
on:click={() => { on:click={() => {
createAPIKeyHandler(); createAPIKeyHandler();
}} }}
@ -416,7 +472,7 @@
</Tooltip> </Tooltip>
{:else} {:else}
<button <button
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition" class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
on:click={() => { on:click={() => {
createAPIKeyHandler(); createAPIKeyHandler();
}} }}
@ -429,6 +485,7 @@
</div> </div>
</div> </div>
</div> </div>
{/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">

View file

@ -185,7 +185,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">{$i18n.t('Desktop Notifications')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Notifications')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"

View file

@ -7,6 +7,7 @@
export let show = true; export let show = true;
export let size = 'md'; export let size = 'md';
let modalElement = null;
let mounted = false; let mounted = false;
const sizeToWidth = (size) => { const sizeToWidth = (size) => {
@ -19,14 +20,23 @@
} }
}; };
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
console.log('Escape');
show = false;
}
};
onMount(() => { onMount(() => {
mounted = true; mounted = true;
}); });
$: if (mounted) { $: if (mounted) {
if (show) { if (show) {
window.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} else { } else {
window.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'unset'; document.body.style.overflow = 'unset';
} }
} }
@ -36,6 +46,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
bind:this={modalElement}
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain" class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
in:fade={{ duration: 10 }} in:fade={{ duration: 10 }}
on:click={() => { on:click={() => {

View file

@ -11,6 +11,8 @@
import Dropdown from '$lib/components/common/Dropdown.svelte'; import Dropdown from '$lib/components/common/Dropdown.svelte';
import Tags from '$lib/components/common/Tags.svelte'; import Tags from '$lib/components/common/Tags.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import { downloadChatAsPDF } from '$lib/apis/utils';
export let shareEnabled: boolean = false; export let shareEnabled: boolean = false;
export let shareHandler: Function; export let shareHandler: Function;
@ -25,7 +27,7 @@
export let onClose: Function = () => {}; export let onClose: Function = () => {};
const downloadChatAsTxt = async () => { const downloadTxt = async () => {
const _chat = chat.chat; const _chat = chat.chat;
console.log('download', chat); console.log('download', chat);
@ -40,54 +42,29 @@
saveAs(blob, `chat-${_chat.title}.txt`); saveAs(blob, `chat-${_chat.title}.txt`);
}; };
const downloadChatAsPdf = async () => { const downloadPdf = async () => {
const _chat = chat.chat; const _chat = chat.chat;
console.log('download', chat); console.log('download', chat);
const doc = new jsPDF(); const blob = await downloadChatAsPDF(_chat);
// Initialize y-coordinate for text placement // Create a URL for the blob
let yPos = 10; const url = window.URL.createObjectURL(blob);
const pageHeight = doc.internal.pageSize.height;
// Function to check if new text exceeds the current page height // Create a link element to trigger the download
function checkAndAddNewPage() { const a = document.createElement('a');
if (yPos > pageHeight - 10) { a.href = url;
doc.addPage(); a.download = `chat-${_chat.title}.pdf`;
yPos = 10; // Reset yPos for the new page
}
}
// Function to add text with specific style // Append the link to the body and click it programmatically
function addStyledText(text, isTitle = false) { document.body.appendChild(a);
// Set font style and size based on the parameters a.click();
doc.setFont('helvetica', isTitle ? 'bold' : 'normal');
doc.setFontSize(isTitle ? 12 : 10);
const textMargin = 7; // Remove the link from the body
document.body.removeChild(a);
// Split text into lines to ensure it fits within the page width // Revoke the URL to release memory
const lines = doc.splitTextToSize(text, 180); // Adjust the width as needed window.URL.revokeObjectURL(url);
lines.forEach((line) => {
checkAndAddNewPage(); // Check if we need a new page before adding more text
doc.text(line, 10, yPos);
yPos += textMargin; // Increment yPos for the next line
});
// Add extra space after a block of text
yPos += 2;
}
_chat.messages.forEach((message, i) => {
// Add user text in bold
doc.setFont('helvetica', 'normal', 'bold');
addStyledText(message.role.toUpperCase(), { isTitle: true });
addStyledText(message.content);
});
doc.save(`chat-${_chat.title}.pdf`);
}; };
</script> </script>
@ -193,7 +170,7 @@
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => { on:click={() => {
downloadChatAsTxt(); downloadTxt();
}} }}
> >
<div class="flex items-center line-clamp-1">Plain text (.txt)</div> <div class="flex items-center line-clamp-1">Plain text (.txt)</div>
@ -202,7 +179,7 @@
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => { on:click={() => {
downloadChatAsPdf(); downloadPdf();
}} }}
> >
<div class="flex items-center line-clamp-1">PDF document (.pdf)</div> <div class="flex items-center line-clamp-1">PDF document (.pdf)</div>

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}",
"Deleted {tagName}": "Изтрито {tagName}", "Deleted {tagName}": "Изтрито {tagName}",
"Description": "Описание", "Description": "Описание",
"Desktop Notifications": "Десктоп Известия", "Notifications": "Десктоп Известия",
"Disabled": "Деактивиран", "Disabled": "Деактивиран",
"Discover a modelfile": "Откриване на модфайл", "Discover a modelfile": "Откриване на модфайл",
"Discover a prompt": "Откриване на промпт", "Discover a prompt": "Откриване на промпт",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}",
"Deleted {tagName}": "Esborrat {tagName}", "Deleted {tagName}": "Esborrat {tagName}",
"Description": "Descripció", "Description": "Descripció",
"Desktop Notifications": "Notificacions d'Escriptori", "Notifications": "Notificacions d'Escriptori",
"Disabled": "Desactivat", "Disabled": "Desactivat",
"Discover a modelfile": "Descobreix un fitxer de model", "Discover a modelfile": "Descobreix un fitxer de model",
"Discover a prompt": "Descobreix un prompt", "Discover a prompt": "Descobreix un prompt",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht",
"Deleted {tagName}": "{tagName} gelöscht", "Deleted {tagName}": "{tagName} gelöscht",
"Description": "Beschreibung", "Description": "Beschreibung",
"Desktop Notifications": "Desktop-Benachrichtigungen", "Notifications": "Desktop-Benachrichtigungen",
"Disabled": "Deaktiviert", "Disabled": "Deaktiviert",
"Discover a modelfile": "Eine Modelfiles entdecken", "Discover a modelfile": "Eine Modelfiles entdecken",
"Discover a prompt": "Einen Prompt entdecken", "Discover a prompt": "Einen Prompt entdecken",

View file

@ -0,0 +1,64 @@
{
"analyze": "analyse",
"analyzed": "analysed",
"analyzes": "analyses",
"apologize": "apologise",
"apologized": "apologised",
"apologizes": "apologises",
"apologizing": "apologising",
"canceled": "cancelled",
"canceling": "cancelling",
"capitalize": "capitalise",
"capitalized": "capitalised",
"capitalizes": "capitalises",
"center": "centre",
"centered": "centred",
"color": "colour",
"colorize": "colourise",
"customize": "customise",
"customizes": "customises",
"defense": "defence",
"dialog": "dialogue",
"emphasize": "emphasise",
"emphasized": "emphasised",
"emphasizes": "emphasises",
"favor": "favour",
"favorable": "favourable",
"favorite": "favourite",
"favoritism": "favouritism",
"labor": "labour",
"labored": "laboured",
"laboring": "labouring",
"maximize": "maximise",
"maximizes": "maximises",
"minimize": "minimise",
"minimizes": "minimises",
"neighbor": "neighbour",
"neighborhood": "neighbourhood",
"offense": "offence",
"organize": "organise",
"organizes": "organises",
"personalize": "personalise",
"personalizes": "personalises",
"program": "programme",
"programmed": "programmed",
"programs": "programmes",
"quantization": "quantisation",
"quantize": "quantise",
"randomize": "randomise",
"randomizes": "randomises",
"realize": "realise",
"realizes": "realises",
"recognize": "recognise",
"recognizes": "recognises",
"summarize": "summarise",
"summarizes": "summarises",
"theater": "theatre",
"theaters": "theatres",
"toward": "towards",
"traveled": "travelled",
"traveler": "traveller",
"traveling": "travelling",
"utilize": "utilise",
"utilizes": "utilises"
}

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "", "Deleted {{deleteModelTag}}": "",
"Deleted {tagName}": "", "Deleted {tagName}": "",
"Description": "", "Description": "",
"Desktop Notifications": "", "Notifications": "",
"Disabled": "", "Disabled": "",
"Discover a modelfile": "", "Discover a modelfile": "",
"Discover a prompt": "", "Discover a prompt": "",
@ -151,6 +151,7 @@
"Failed to read clipboard contents": "", "Failed to read clipboard contents": "",
"File Mode": "", "File Mode": "",
"File not found.": "", "File not found.": "",
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
"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)": "",
@ -347,6 +348,7 @@
"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": "", "Use Gravatar": "",
"Use Initials": "",
"user": "", "user": "",
"User Permissions": "", "User Permissions": "",
"Users": "", "Users": "",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "Se borró {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "Se borró {{deleteModelTag}}",
"Deleted {tagName}": "Se borró {tagName}", "Deleted {tagName}": "Se borró {tagName}",
"Description": "Descripción", "Description": "Descripción",
"Desktop Notifications": "Notificaciones", "Notifications": "Notificaciones",
"Disabled": "Desactivado", "Disabled": "Desactivado",
"Discover a modelfile": "Descubre un modelfile", "Discover a modelfile": "Descubre un modelfile",
"Discover a prompt": "Descubre un Prompt", "Discover a prompt": "Descubre un Prompt",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} پاک شد", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} پاک شد",
"Deleted {tagName}": "{tagName} حذف شد", "Deleted {tagName}": "{tagName} حذف شد",
"Description": "توضیحات", "Description": "توضیحات",
"Desktop Notifications": "اعلان", "Notifications": "اعلان",
"Disabled": "غیرفعال", "Disabled": "غیرفعال",
"Discover a modelfile": "فایل مدل را کشف کنید", "Discover a modelfile": "فایل مدل را کشف کنید",
"Discover a prompt": "یک اعلان را کشف کنید", "Discover a prompt": "یک اعلان را کشف کنید",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé",
"Deleted {tagName}": "{tagName} supprimé", "Deleted {tagName}": "{tagName} supprimé",
"Description": "Description", "Description": "Description",
"Desktop Notifications": "Notifications de bureau", "Notifications": "Notifications de bureau",
"Disabled": "Désactivé", "Disabled": "Désactivé",
"Discover a modelfile": "Découvrir un fichier de modèle", "Discover a modelfile": "Découvrir un fichier de modèle",
"Discover a prompt": "Découvrir un prompt", "Discover a prompt": "Découvrir un prompt",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé",
"Deleted {tagName}": "{tagName} supprimé", "Deleted {tagName}": "{tagName} supprimé",
"Description": "Description", "Description": "Description",
"Desktop Notifications": "Notifications de bureau", "Notifications": "Notifications de bureau",
"Disabled": "Désactivé", "Disabled": "Désactivé",
"Discover a modelfile": "Découvrir un fichier de modèle", "Discover a modelfile": "Découvrir un fichier de modèle",
"Discover a prompt": "Découvrir un prompt", "Discover a prompt": "Découvrir un prompt",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "Eliminato {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "Eliminato {{deleteModelTag}}",
"Deleted {tagName}": "Eliminato {tagName}", "Deleted {tagName}": "Eliminato {tagName}",
"Description": "Descrizione", "Description": "Descrizione",
"Desktop Notifications": "Notifiche desktop", "Notifications": "Notifiche desktop",
"Disabled": "Disabilitato", "Disabled": "Disabilitato",
"Discover a modelfile": "Scopri un file modello", "Discover a modelfile": "Scopri un file modello",
"Discover a prompt": "Scopri un prompt", "Discover a prompt": "Scopri un prompt",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} を削除しました", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} を削除しました",
"Deleted {tagName}": "{tagName} を削除しました", "Deleted {tagName}": "{tagName} を削除しました",
"Description": "説明", "Description": "説明",
"Desktop Notifications": "デスクトップ通知", "Notifications": "デスクトップ通知",
"Disabled": "無効", "Disabled": "無効",
"Discover a modelfile": "モデルファイルを見つける", "Discover a modelfile": "モデルファイルを見つける",
"Discover a prompt": "プロンプトを見つける", "Discover a prompt": "プロンプトを見つける",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨",
"Deleted {tagName}": "{tagName} 삭제됨", "Deleted {tagName}": "{tagName} 삭제됨",
"Description": "설명", "Description": "설명",
"Desktop Notifications": "알림", "Notifications": "알림",
"Disabled": "비활성화", "Disabled": "비활성화",
"Discover a modelfile": "모델파일 검색", "Discover a modelfile": "모델파일 검색",
"Discover a prompt": "프롬프트 검색", "Discover a prompt": "프롬프트 검색",

View file

@ -15,6 +15,10 @@
"code": "de-DE", "code": "de-DE",
"title": "Deutsch" "title": "Deutsch"
}, },
{
"code": "en-GB",
"title": "English (GB)"
},
{ {
"code": "es-ES", "code": "es-ES",
"title": "Spanish" "title": "Spanish"
@ -51,10 +55,18 @@
"code": "pt-PT", "code": "pt-PT",
"title": "Portuguese (Portugal)" "title": "Portuguese (Portugal)"
}, },
{
"code": "pt-BR",
"title": "Portuguese (Brazil)"
},
{ {
"code": "ru-RU", "code": "ru-RU",
"title": "Russian (Russia)" "title": "Russian (Russia)"
}, },
{
"code": "tr-TR",
"title": "Turkish"
},
{ {
"code": "uk-UA", "code": "uk-UA",
"title": "Ukrainian" "title": "Ukrainian"

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} is verwijderd", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} is verwijderd",
"Deleted {tagName}": "{tagName} is verwijderd", "Deleted {tagName}": "{tagName} is verwijderd",
"Description": "Beschrijving", "Description": "Beschrijving",
"Desktop Notifications": "Desktop Notificaties", "Notifications": "Desktop Notificaties",
"Disabled": "Uitgeschakeld", "Disabled": "Uitgeschakeld",
"Discover a modelfile": "Ontdek een modelfile", "Discover a modelfile": "Ontdek een modelfile",
"Discover a prompt": "Ontdek een prompt", "Discover a prompt": "Ontdek een prompt",

View file

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 's' ou '-1' para não expirar.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(por exemplo, `sh webui.sh --api`)",
"(latest)": "(mais recente)",
"{{modelName}} is thinking...": "{{modelName}} está pensando...",
"{{webUIName}} Backend Required": "{{webUIName}} Backend Necessário",
"a user": "um usuário",
"About": "Sobre",
"Account": "Conta",
"Action": "Ação",
"Add a model": "Adicionar um modelo",
"Add a model tag name": "Adicionar um nome de tag de modelo",
"Add a short description about what this modelfile does": "Adicione uma breve descrição sobre o que este arquivo de modelo faz",
"Add a short title for this prompt": "Adicione um título curto para este prompt",
"Add a tag": "Adicionar uma tag",
"Add Docs": "Adicionar Documentos",
"Add Files": "Adicionar Arquivos",
"Add message": "Adicionar mensagem",
"add tags": "adicionar tags",
"Adjusting these settings will apply changes universally to all users.": "Ajustar essas configurações aplicará alterações universalmente a todos os usuários.",
"admin": "administrador",
"Admin Panel": "Painel do Administrador",
"Admin Settings": "Configurações do Administrador",
"Advanced Parameters": "Parâmetros Avançados",
"all": "todos",
"All Users": "Todos os Usuários",
"Allow": "Permitir",
"Allow Chat Deletion": "Permitir Exclusão de Bate-papo",
"alphanumeric characters and hyphens": "caracteres alfanuméricos e hífens",
"Already have an account?": "Já tem uma conta?",
"an assistant": "um assistente",
"and": "e",
"API Base URL": "URL Base da API",
"API Key": "Chave da API",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "são permitidos - Ative este comando digitando",
"Are you sure?": "Tem certeza?",
"Audio": "Áudio",
"Auto-playback response": "Reprodução automática da resposta",
"Auto-send input after 3 sec.": "Enviar entrada automaticamente após 3 segundos.",
"AUTOMATIC1111 Base URL": "URL Base do AUTOMATIC1111",
"AUTOMATIC1111 Base URL is required.": "A URL Base do AUTOMATIC1111 é obrigatória.",
"available!": "disponível!",
"Back": "Voltar",
"Builder Mode": "Modo de Construtor",
"Cancel": "Cancelar",
"Categories": "Categorias",
"Change Password": "Alterar Senha",
"Chat": "Bate-papo",
"Chat History": "Histórico de Bate-papo",
"Chat History is off for this browser.": "O histórico de bate-papo está desativado para este navegador.",
"Chats": "Bate-papos",
"Check Again": "Verifique novamente",
"Check for updates": "Verificar atualizações",
"Checking for updates...": "Verificando atualizações...",
"Choose a model before saving...": "Escolha um modelo antes de salvar...",
"Chunk Overlap": "Sobreposição de Fragmento",
"Chunk Params": "Parâmetros de Fragmento",
"Chunk Size": "Tamanho do Fragmento",
"Click here for help.": "Clique aqui para obter ajuda.",
"Click here to check other modelfiles.": "Clique aqui para verificar outros arquivos de modelo.",
"Click here to select": "Clique aqui para selecionar",
"Click here to select documents.": "Clique aqui para selecionar documentos.",
"click here.": "clique aqui.",
"Click on the user role button to change a user's role.": "Clique no botão de função do usuário para alterar a função de um usuário.",
"Close": "Fechar",
"Collection": "Coleção",
"Command": "Comando",
"Confirm Password": "Confirmar Senha",
"Connections": "Conexões",
"Content": "Conteúdo",
"Context Length": "Comprimento do Contexto",
"Conversation Mode": "Modo de Conversa",
"Copy last code block": "Copiar último bloco de código",
"Copy last response": "Copiar última resposta",
"Copying to clipboard was successful!": "Cópia para a área de transferência bem-sucedida!",
"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':": "Crie uma frase concisa de 3 a 5 palavras como cabeçalho para a seguinte consulta, aderindo estritamente ao limite de 3 a 5 palavras e evitando o uso da palavra 'título':",
"Create a modelfile": "Criar um arquivo de modelo",
"Create Account": "Criar Conta",
"Created at": "Criado em",
"Created by": "Criado por",
"Current Model": "Modelo Atual",
"Current Password": "Senha Atual",
"Custom": "Personalizado",
"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",
"Default": "Padrão",
"Default (Automatic1111)": "Padrão (Automatic1111)",
"Default (Web API)": "Padrão (API Web)",
"Default model updated": "Modelo padrão atualizado",
"Default Prompt Suggestions": "Sugestões de Prompt Padrão",
"Default User Role": "Função de Usuário Padrão",
"delete": "excluir",
"Delete a model": "Excluir um modelo",
"Delete chat": "Excluir bate-papo",
"Delete Chats": "Excluir Bate-papos",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído",
"Deleted {tagName}": "{tagName} excluído",
"Description": "Descrição",
"Notifications": "Notificações da Área de Trabalho",
"Disabled": "Desativado",
"Discover a modelfile": "Descobrir um arquivo de modelo",
"Discover a prompt": "Descobrir um prompt",
"Discover, download, and explore custom prompts": "Descubra, baixe e explore prompts personalizados",
"Discover, download, and explore model presets": "Descubra, baixe e explore predefinições de modelo",
"Display the username instead of You in the Chat": "Exibir o nome de usuário em vez de Você no Bate-papo",
"Document": "Documento",
"Document Settings": "Configurações de Documento",
"Documents": "Documentos",
"does not make any external connections, and your data stays securely on your locally hosted server.": "não faz conexões externas e seus dados permanecem seguros em seu servidor hospedado localmente.",
"Don't Allow": "Não Permitir",
"Don't have an account?": "Não tem uma conta?",
"Download as a File": "Baixar como Arquivo",
"Download Database": "Baixar Banco de Dados",
"Drop any files here to add to the conversation": "Solte os arquivos aqui para adicionar à conversa",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "por exemplo, '30s', '10m'. Unidades de tempo válidas são 's', 'm', 'h'.",
"Edit Doc": "Editar Documento",
"Edit User": "Editar Usuário",
"Email": "E-mail",
"Enable Chat History": "Ativar Histórico de Bate-papo",
"Enable New Sign Ups": "Ativar Novas Inscrições",
"Enabled": "Ativado",
"Enter {{role}} message here": "Digite a mensagem de {{role}} aqui",
"Enter API Key": "Digite a Chave da API",
"Enter Chunk Overlap": "Digite a Sobreposição de Fragmento",
"Enter Chunk Size": "Digite o Tamanho do Fragmento",
"Enter Image Size (e.g. 512x512)": "Digite o Tamanho da Imagem (por exemplo, 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "Digite a URL Base da API LiteLLM (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "Digite a Chave da API LiteLLM (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "Digite o RPM da API LiteLLM (litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "Digite o Modelo LiteLLM (litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "Digite o Máximo de Tokens (litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "Digite a tag do modelo (por exemplo, {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Digite o Número de Etapas (por exemplo, 50)",
"Enter stop sequence": "Digite a sequência de parada",
"Enter Top K": "Digite o Top K",
"Enter URL (e.g. http://127.0.0.1:7860/)": "Digite a URL (por exemplo, http://127.0.0.1:7860/)",
"Enter Your Email": "Digite seu E-mail",
"Enter Your Full Name": "Digite seu Nome Completo",
"Enter Your Password": "Digite sua Senha",
"Experimental": "Experimental",
"Export All Chats (All Users)": "Exportar Todos os Bate-papos (Todos os Usuários)",
"Export Chats": "Exportar Bate-papos",
"Export Documents Mapping": "Exportar Mapeamento de Documentos",
"Export Modelfiles": "Exportar Arquivos de Modelo",
"Export Prompts": "Exportar Prompts",
"Failed to read clipboard contents": "Falha ao ler o conteúdo da área de transferência",
"File Mode": "Modo de Arquivo",
"File not found.": "Arquivo não encontrado.",
"Focus chat input": "Focar entrada de bate-papo",
"Format your variables using square brackets like this:": "Formate suas variáveis usando colchetes como este:",
"From (Base Model)": "De (Modelo Base)",
"Full Screen Mode": "Modo de Tela Cheia",
"General": "Geral",
"General Settings": "Configurações Gerais",
"Hello, {{name}}": "Olá, {{name}}",
"Hide": "Ocultar",
"Hide Additional Params": "Ocultar Parâmetros Adicionais",
"How can I help you today?": "Como posso ajudá-lo hoje?",
"Image Generation (Experimental)": "Geração de Imagens (Experimental)",
"Image Generation Engine": "Mecanismo de Geração de Imagens",
"Image Settings": "Configurações de Imagem",
"Images": "Imagens",
"Import Chats": "Importar Bate-papos",
"Import Documents Mapping": "Importar Mapeamento de Documentos",
"Import Modelfiles": "Importar Arquivos de Modelo",
"Import Prompts": "Importar Prompts",
"Include `--api` flag when running stable-diffusion-webui": "Inclua a flag `--api` ao executar stable-diffusion-webui",
"Interface": "Interface",
"join our Discord for help.": "junte-se ao nosso Discord para obter ajuda.",
"JSON": "JSON",
"JWT Expiration": "Expiração JWT",
"JWT Token": "Token JWT",
"Keep Alive": "Manter Vivo",
"Keyboard shortcuts": "Atalhos de teclado",
"Language": "Idioma",
"Light": "Claro",
"Listening...": "Ouvindo...",
"LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.",
"Made by OpenWebUI Community": "Feito pela Comunidade OpenWebUI",
"Make sure to enclose them with": "Certifique-se de colocá-los entre",
"Manage LiteLLM Models": "Gerenciar Modelos LiteLLM",
"Manage Models": "Gerenciar Modelos",
"Manage Ollama Models": "Gerenciar Modelos Ollama",
"Max Tokens": "Máximo de Tokens",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Máximo de 3 modelos podem ser baixados simultaneamente. Tente novamente mais tarde.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "MMMM DD, AAAA",
"Model '{{modelName}}' has been successfully downloaded.": "O modelo '{{modelName}}' foi baixado com sucesso.",
"Model '{{modelTag}}' is already in queue for downloading.": "O modelo '{{modelTag}}' já está na fila para download.",
"Model {{modelId}} not found": "Modelo {{modelId}} não encontrado",
"Model {{modelName}} already exists.": "O modelo {{modelName}} já existe.",
"Model Name": "Nome do Modelo",
"Model not selected": "Modelo não selecionado",
"Model Tag Name": "Nome da Tag do Modelo",
"Model Whitelisting": "Lista de Permissões de Modelo",
"Model(s) Whitelisted": "Modelo(s) na Lista de Permissões",
"Modelfile": "Arquivo de Modelo",
"Modelfile Advanced Settings": "Configurações Avançadas do Arquivo de Modelo",
"Modelfile Content": "Conteúdo do Arquivo de Modelo",
"Modelfiles": "Arquivos de Modelo",
"Models": "Modelos",
"My Documents": "Meus Documentos",
"My Modelfiles": "Meus Arquivos de Modelo",
"My Prompts": "Meus Prompts",
"Name": "Nome",
"Name Tag": "Nome da Tag",
"Name your modelfile": "Nomeie seu arquivo de modelo",
"New Chat": "Novo Bate-papo",
"New Password": "Nova Senha",
"Not sure what to add?": "Não tem certeza do que adicionar?",
"Not sure what to write? Switch to": "Não tem certeza do que escrever? Mude para",
"Off": "Desligado",
"Okay, Let's Go!": "Ok, Vamos Lá!",
"Ollama Base URL": "URL Base do Ollama",
"Ollama Version": "Versão do Ollama",
"On": "Ligado",
"Only": "Somente",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Somente caracteres alfanuméricos e hífens são permitidos na string 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.": "Opa! Aguente firme! Seus arquivos ainda estão no forno de processamento. Estamos cozinhando-os com perfeição. Por favor, seja paciente e avisaremos quando estiverem prontos.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Opa! Parece que a URL é inválida. Verifique novamente e tente outra vez.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Opa! Você está usando um método não suportado (somente frontend). Por favor, sirva o WebUI a partir do backend.",
"Open": "Abrir",
"Open AI": "OpenAI",
"Open AI (Dall-E)": "OpenAI (Dall-E)",
"Open new chat": "Abrir novo bate-papo",
"OpenAI API": "API OpenAI",
"OpenAI API Key": "Chave da API OpenAI",
"OpenAI API Key is required.": "A Chave da API OpenAI é obrigatória.",
"or": "ou",
"Parameters": "Parâmetros",
"Password": "Senha",
"PDF Extract Images (OCR)": "Extrair Imagens de PDF (OCR)",
"pending": "pendente",
"Permission denied when accessing microphone: {{error}}": "Permissão negada ao acessar o microfone: {{error}}",
"Playground": "Playground",
"Profile": "Perfil",
"Prompt Content": "Conteúdo do Prompt",
"Prompt suggestions": "Sugestões de Prompt",
"Prompts": "Prompts",
"Pull a model from Ollama.com": "Extrair um modelo do Ollama.com",
"Pull Progress": "Progresso da Extração",
"Query Params": "Parâmetros de Consulta",
"RAG Template": "Modelo RAG",
"Raw Format": "Formato Bruto",
"Record voice": "Gravar voz",
"Redirecting you to OpenWebUI Community": "Redirecionando você para a Comunidade OpenWebUI",
"Release Notes": "Notas de Lançamento",
"Repeat Last N": "Repetir Últimos N",
"Repeat Penalty": "Penalidade de Repetição",
"Request Mode": "Modo de Solicitação",
"Reset Vector Storage": "Redefinir Armazenamento de Vetor",
"Response AutoCopy to Clipboard": "Cópia Automática da Resposta para a Área de Transferência",
"Role": "Função",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Salvar",
"Save & Create": "Salvar e Criar",
"Save & Submit": "Salvar e Enviar",
"Save & Update": "Salvar e Atualizar",
"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": "Salvar logs de bate-papo diretamente no armazenamento do seu navegador não é mais suportado. Reserve um momento para baixar e excluir seus logs de bate-papo clicando no botão abaixo. Não se preocupe, você pode facilmente reimportar seus logs de bate-papo para o backend através de",
"Scan": "Digitalizar",
"Scan complete!": "Digitalização concluída!",
"Scan for documents from {{path}}": "Digitalizar documentos de {{path}}",
"Search": "Pesquisar",
"Search Documents": "Pesquisar Documentos",
"Search Prompts": "Pesquisar Prompts",
"See readme.md for instructions": "Consulte readme.md para obter instruções",
"See what's new": "Veja o que há de novo",
"Seed": "Semente",
"Select a mode": "Selecione um modo",
"Select a model": "Selecione um modelo",
"Select an Ollama instance": "Selecione uma instância Ollama",
"Send a Message": "Enviar uma Mensagem",
"Send message": "Enviar mensagem",
"Server connection verified": "Conexão com o servidor verificada",
"Set as default": "Definir como padrão",
"Set Default Model": "Definir Modelo Padrão",
"Set Image Size": "Definir Tamanho da Imagem",
"Set Steps": "Definir Etapas",
"Set Title Auto-Generation Model": "Definir Modelo de Geração Automática de Título",
"Set Voice": "Definir Voz",
"Settings": "Configurações",
"Settings saved successfully!": "Configurações salvas com sucesso!",
"Share to OpenWebUI Community": "Compartilhar com a Comunidade OpenWebUI",
"short-summary": "resumo-curto",
"Show": "Mostrar",
"Show Additional Params": "Mostrar Parâmetros Adicionais",
"Show shortcuts": "Mostrar",
"sidebar": "barra lateral",
"Sign in": "Entrar",
"Sign Out": "Sair",
"Sign up": "Inscrever-se",
"Speech recognition error: {{error}}": "Erro de reconhecimento de fala: {{error}}",
"Speech-to-Text Engine": "Mecanismo de Fala para Texto",
"SpeechRecognition API is not supported in this browser.": "A API SpeechRecognition não é suportada neste navegador.",
"Stop Sequence": "Sequência de Parada",
"STT Settings": "Configurações STT",
"Submit": "Enviar",
"Success": "Sucesso",
"Successfully updated.": "Atualizado com sucesso.",
"Sync All": "Sincronizar Tudo",
"System": "Sistema",
"System Prompt": "Prompt do Sistema",
"Tags": "Tags",
"Temperature": "Temperatura",
"Template": "Modelo",
"Text Completion": "Complemento de Texto",
"Text-to-Speech Engine": "Mecanismo de Texto para Fala",
"Tfs Z": "Tfs Z",
"Theme": "Tema",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Isso garante que suas conversas valiosas sejam salvas com segurança em seu banco de dados de backend. Obrigado!",
"This setting does not sync across browsers or devices.": "Esta configuração não sincroniza entre navegadores ou dispositivos.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Dica: Atualize vários slots de variáveis consecutivamente pressionando a tecla Tab na entrada de bate-papo após cada substituição.",
"Title": "Título",
"Title Auto-Generation": "Geração Automática de Título",
"Title Generation Prompt": "Prompt de Geração de Título",
"to": "para",
"To access the available model names for downloading,": "Para acessar os nomes de modelo disponíveis para download,",
"To access the GGUF models available for downloading,": "Para acessar os modelos GGUF disponíveis para download,",
"to chat input.": "para a entrada de bate-papo.",
"Toggle settings": "Alternar configurações",
"Toggle sidebar": "Alternar barra lateral",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Problemas para acessar o Ollama?",
"TTS Settings": "Configurações TTS",
"Type Hugging Face Resolve (Download) URL": "Digite a URL do Hugging Face Resolve (Download)",
"Uh-oh! There was an issue connecting to {{provider}}.": "Opa! Houve um problema ao conectar-se a {{provider}}.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipo de arquivo desconhecido '{{file_type}}', mas aceitando e tratando como texto simples",
"Update password": "Atualizar senha",
"Upload a GGUF model": "Carregar um modelo GGUF",
"Upload files": "Carregar arquivos",
"Upload Progress": "Progresso do Carregamento",
"URL Mode": "Modo de URL",
"Use '#' in the prompt input to load and select your documents.": "Use '#' na entrada do prompt para carregar e selecionar seus documentos.",
"Use Gravatar": "Usar Gravatar",
"user": "usuário",
"User Permissions": "Permissões do Usuário",
"Users": "Usuários",
"Utilize": "Utilizar",
"Valid time units:": "Unidades de tempo válidas:",
"variable": "variável",
"variable to have them replaced with clipboard content.": "variável para que sejam substituídos pelo conteúdo da área de transferência.",
"Version": "Versão",
"Web": "Web",
"WebUI Add-ons": "Complementos WebUI",
"WebUI Settings": "Configurações WebUI",
"WebUI will make requests to": "WebUI fará solicitações para",
"Whats New in": "O que há de novo em",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Quando o histórico está desativado, novos bate-papos neste navegador não aparecerão em seu histórico em nenhum dos seus dispositivos.",
"Whisper (Local)": "Whisper (Local)",
"Write a prompt suggestion (e.g. Who are you?)": "Escreva uma sugestão de prompt (por exemplo, Quem é você?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "Escreva um resumo em 50 palavras que resuma [tópico ou palavra-chave].",
"You": "Você",
"You're a helpful assistant.": "Você é um assistente útil.",
"You're now logged in.": "Você está conectado agora."
}

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído",
"Deleted {tagName}": "{tagName} excluído", "Deleted {tagName}": "{tagName} excluído",
"Description": "Descrição", "Description": "Descrição",
"Desktop Notifications": "Notificações da Área de Trabalho", "Notifications": "Notificações da Área de Trabalho",
"Disabled": "Desativado", "Disabled": "Desativado",
"Discover a modelfile": "Descobrir um arquivo de modelo", "Discover a modelfile": "Descobrir um arquivo de modelo",
"Discover a prompt": "Descobrir um prompt", "Discover a prompt": "Descobrir um prompt",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "Удалено {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "Удалено {{deleteModelTag}}",
"Deleted {tagName}": "Удалено {tagName}", "Deleted {tagName}": "Удалено {tagName}",
"Description": "Описание", "Description": "Описание",
"Desktop Notifications": "Уведомления на рабочем столе", "Notifications": "Уведомления на рабочем столе",
"Disabled": "Отключено", "Disabled": "Отключено",
"Discover a modelfile": "Найти файл модели", "Discover a modelfile": "Найти файл модели",
"Discover a prompt": "Найти промт", "Discover a prompt": "Найти промт",

View file

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' veya süresiz için '-1'.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(örn. `sh webui.sh --api`)",
"(latest)": "(en son)",
"{{modelName}} is thinking...": "{{modelName}} düşünüyor...",
"{{webUIName}} Backend Required": "{{webUIName}} Arkayüz Gerekli",
"a user": "bir kullanıcı",
"About": "Hakkında",
"Account": "Hesap",
"Action": "Eylem",
"Add a model": "Bir model ekleyin",
"Add a model tag name": "Bir model etiket adı ekleyin",
"Add a short description about what this modelfile does": "Bu model dosyasının ne yaptığı hakkında kısa bir açıklama ekleyin",
"Add a short title for this prompt": "Bu prompt için kısa bir başlık ekleyin",
"Add a tag": "Bir etiket ekleyin",
"Add Docs": "Dökümanlar Ekle",
"Add Files": "Dosyalar Ekle",
"Add message": "Mesaj ekle",
"add tags": "etiketler ekle",
"Adjusting these settings will apply changes universally to all users.": "Bu ayarları ayarlamak değişiklikleri tüm kullanıcılara evrensel olarak uygular.",
"admin": "yönetici",
"Admin Panel": "Yönetici Paneli",
"Admin Settings": "Yönetici Ayarları",
"Advanced Parameters": "Gelişmiş Parametreler",
"all": "tümü",
"All Users": "Tüm Kullanıcılar",
"Allow": "İzin ver",
"Allow Chat Deletion": "Sohbet Silmeye İzin Ver",
"alphanumeric characters and hyphens": "alfanumerik karakterler ve tireler",
"Already have an account?": "Zaten bir hesabınız mı var?",
"an assistant": "bir asistan",
"and": "ve",
"API Base URL": "API Temel URL",
"API Key": "API Anahtarı",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "izin verilir - Bu komutu yazarak etkinleştirin",
"Are you sure?": "Emin misiniz?",
"Audio": "Ses",
"Auto-playback response": "Yanıtı otomatik oynatma",
"Auto-send input after 3 sec.": "3 saniye sonra otomatik olarak gönder",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Temel URL",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Temel URL gereklidir.",
"available!": "mevcut!",
"Back": "Geri",
"Builder Mode": "Oluşturucu Modu",
"Cancel": "İptal",
"Categories": "Kategoriler",
"Change Password": "Parola Değiştir",
"Chat": "Sohbet",
"Chat History": "Sohbet Geçmişi",
"Chat History is off for this browser.": "Bu tarayıcı için sohbet geçmişi kapalı.",
"Chats": "Sohbetler",
"Check Again": "Tekrar Kontrol Et",
"Check for updates": "Güncellemeleri kontrol et",
"Checking for updates...": "Güncellemeler kontrol ediliyor...",
"Choose a model before saving...": "Kaydetmeden önce bir model seçin...",
"Chunk Overlap": "Chunk Çakışması",
"Chunk Params": "Chunk Parametreleri",
"Chunk Size": "Chunk Boyutu",
"Click here for help.": "Yardım için buraya tıklayın.",
"Click here to check other modelfiles.": "Diğer model dosyalarını kontrol etmek için buraya tıklayın.",
"Click here to select": "Seçmek için buraya tıklayın",
"Click here to select documents.": "Belgeleri seçmek için buraya tıklayın.",
"click here.": "buraya tıklayın.",
"Click on the user role button to change a user's role.": "Bir kullanıcının rolünü değiştirmek için kullanıcı rolü düğmesine tıklayın.",
"Close": "Kapat",
"Collection": "Koleksiyon",
"Command": "Komut",
"Confirm Password": "Parolayı Onayla",
"Connections": "Bağlantılar",
"Content": "İçerik",
"Context Length": "Bağlam Uzunluğu",
"Conversation Mode": "Sohbet Modu",
"Copy last code block": "Son kod bloğunu kopyala",
"Copy last response": "Son yanıtı kopyala",
"Copying to clipboard was successful!": "Panoya kopyalama başarılı!",
"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':": "Aşağıdaki sorgu için başlık olarak 3-5 kelimelik kısa ve öz bir ifade oluşturun, 3-5 kelime sınırına kesinlikle uyun ve 'başlık' kelimesini kullanmaktan kaçının:",
"Create a modelfile": "Bir model dosyası oluştur",
"Create Account": "Hesap Oluştur",
"Created at": "Oluşturulma tarihi",
"Created by": "Oluşturan",
"Current Model": "Mevcut Model",
"Current Password": "Mevcut Parola",
"Custom": "Özel",
"Customize Ollama models for a specific purpose": "Ollama modellerini belirli bir amaç için özelleştirin",
"Dark": "Koyu",
"Database": "Veritabanı",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "Varsayılan",
"Default (Automatic1111)": "Varsayılan (Automatic1111)",
"Default (Web API)": "Varsayılan (Web API)",
"Default model updated": "Varsayılan model güncellendi",
"Default Prompt Suggestions": "Varsayılan Prompt Önerileri",
"Default User Role": "Varsayılan Kullanıcı Rolü",
"delete": "sil",
"Delete a model": "Bir modeli sil",
"Delete chat": "Sohbeti sil",
"Delete Chats": "Sohbetleri Sil",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} silindi",
"Deleted {tagName}": "{tagName} silindi",
"Description": "Açıklama",
"Notifications": "Bildirimler",
"Disabled": "Devre Dışı",
"Discover a modelfile": "Bir model dosyası keşfedin",
"Discover a prompt": "Bir prompt keşfedin",
"Discover, download, and explore custom prompts": "Özel promptları keşfedin, indirin ve inceleyin",
"Discover, download, and explore model presets": "Model ön ayarlarını keşfedin, indirin ve inceleyin",
"Display the username instead of You in the Chat": "Sohbet'te Siz yerine kullanıcı adını göster",
"Document": "Belge",
"Document Settings": "Belge Ayarları",
"Documents": "Belgeler",
"does not make any external connections, and your data stays securely on your locally hosted server.": "herhangi bir harici bağlantı yapmaz ve verileriniz güvenli bir şekilde yerel olarak barındırılan sunucunuzda kalır.",
"Don't Allow": "İzin Verme",
"Don't have an account?": "Hesabınız yok mu?",
"Download as a File": "Dosya olarak indir",
"Download Database": "Veritabanını İndir",
"Drop any files here to add to the conversation": "Sohbete eklemek istediğiniz dosyaları buraya bırakın",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "örn. '30s', '10m'. Geçerli zaman birimleri 's', 'm', 'h'.",
"Edit Doc": "Belgeyi Düzenle",
"Edit User": "Kullanıcıyı Düzenle",
"Email": "E-posta",
"Enable Chat History": "Sohbet Geçmişini Etkinleştir",
"Enable New Sign Ups": "Yeni Kayıtları Etkinleştir",
"Enabled": "Etkin",
"Enter {{role}} message here": "Buraya {{role}} mesajını girin",
"Enter API Key": "API Anahtarını Girin",
"Enter Chunk Overlap": "Chunk Örtüşmesini Girin",
"Enter Chunk Size": "Chunk Boyutunu Girin",
"Enter Image Size (e.g. 512x512)": "Görüntü Boyutunu Girin (örn. 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "LiteLLM API Ana URL'sini Girin (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "LiteLLM API Anahtarını Girin (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM'ini Girin (litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "LiteLLM Modelini Girin (litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "Maksimum Token Sayısını Girin (litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "Model etiketini girin (örn. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Adım Sayısını Girin (örn. 50)",
"Enter stop sequence": "Durdurma dizisini girin",
"Enter Top K": "Top K'yı girin",
"Enter URL (e.g. http://127.0.0.1:7860/)": "URL'yi Girin (örn. http://127.0.0.1:7860/)",
"Enter Your Email": "E-postanızı Girin",
"Enter Your Full Name": "Tam Adınızı Girin",
"Enter Your Password": "Parolanızı Girin",
"Experimental": "Deneysel",
"Export All Chats (All Users)": "Tüm Sohbetleri Dışa Aktar (Tüm Kullanıcılar)",
"Export Chats": "Sohbetleri Dışa Aktar",
"Export Documents Mapping": "Belge Eşlemesini Dışa Aktar",
"Export Modelfiles": "Model Dosyalarını Dışa Aktar",
"Export Prompts": "Promptları Dışa Aktar",
"Failed to read clipboard contents": "Pano içeriği okunamadı",
"File Mode": "Dosya Modu",
"File not found.": "Dosya bulunamadı.",
"Focus chat input": "Sohbet girişine odaklan",
"Format your variables using square brackets like this:": "Değişkenlerinizi şu şekilde kare parantezlerle biçimlendirin:",
"From (Base Model)": "(Temel Model)'den",
"Full Screen Mode": "Tam Ekran Modu",
"General": "Genel",
"General Settings": "Genel Ayarlar",
"Hello, {{name}}": "Merhaba, {{name}}",
"Hide": "Gizle",
"Hide Additional Params": "Ek Parametreleri Gizle",
"How can I help you today?": "Bugün size nasıl yardımcı olabilirim?",
"Image Generation (Experimental)": "Görüntü Oluşturma (Deneysel)",
"Image Generation Engine": "Görüntü Oluşturma Motoru",
"Image Settings": "Görüntü Ayarları",
"Images": "Görüntüler",
"Import Chats": "Sohbetleri İçe Aktar",
"Import Documents Mapping": "Belge Eşlemesini İçe Aktar",
"Import Modelfiles": "Model Dosyalarını İçe Aktar",
"Import Prompts": "Promptları İçe Aktar",
"Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui çalıştırılırken `--api` bayrağını dahil edin",
"Interface": "Arayüz",
"join our Discord for help.": "yardım için Discord'umuza katılın.",
"JSON": "JSON",
"JWT Expiration": "JWT Bitişi",
"JWT Token": "JWT Token",
"Keep Alive": "Canlı Tut",
"Keyboard shortcuts": "Klavye kısayolları",
"Language": "Dil",
"Light": "Açık",
"Listening...": "Dinleniyor...",
"LLMs can make mistakes. Verify important information.": "LLM'ler hata yapabilir. Önemli bilgileri doğrulayın.",
"Made by OpenWebUI Community": "OpenWebUI Topluluğu tarafından yapılmıştır",
"Make sure to enclose them with": "Değişkenlerinizi şu şekilde biçimlendirin:",
"Manage LiteLLM Models": "LiteLLM Modellerini Yönet",
"Manage Models": "Modelleri Yönet",
"Manage Ollama Models": "Ollama Modellerini Yönet",
"Max Tokens": "Maksimum Token",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Aynı anda en fazla 3 model indirilebilir. Lütfen daha sonra tekrar deneyin.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "DD MMMM YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' başarıyla indirildi.",
"Model '{{modelTag}}' is already in queue for downloading.": "'{{modelTag}}' zaten indirme sırasında.",
"Model {{modelId}} not found": "{{modelId}} bulunamadı",
"Model {{modelName}} already exists.": "{{modelName}} zaten mevcut.",
"Model Name": "Model Adı",
"Model not selected": "Model seçilmedi",
"Model Tag Name": "Model Etiket Adı",
"Model Whitelisting": "Model Beyaz Listeye Alma",
"Model(s) Whitelisted": "Model(ler) Beyaz Listeye Alındı",
"Modelfile": "Model Dosyası",
"Modelfile Advanced Settings": "Model Dosyası Gelişmiş Ayarları",
"Modelfile Content": "Model Dosyası İçeriği",
"Modelfiles": "Model Dosyaları",
"Models": "Modeller",
"My Documents": "Belgelerim",
"My Modelfiles": "Model Dosyalarım",
"My Prompts": "Promptlarım",
"Name": "Ad",
"Name Tag": "Ad Etiketi",
"Name your modelfile": "Model dosyanıza ad verin",
"New Chat": "Yeni Sohbet",
"New Password": "Yeni Parola",
"Not sure what to add?": "Ne ekleyeceğinizden emin değil misiniz?",
"Not sure what to write? Switch to": "Ne yazacağınızdan emin değil misiniz? Şuraya geçin",
"Off": "Kapalı",
"Okay, Let's Go!": "Tamam, Hadi Başlayalım!",
"Ollama Base URL": "Ollama Temel URL",
"Ollama Version": "Ollama Sürümü",
"On": "Açık",
"Only": "Yalnızca",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Komut dizisinde yalnızca alfasayısal karakterler ve tireler kabul edilir.",
"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.": "Hop! Biraz sabırlı ol! Dosyaların hala hazırlama fırınında. Onları ağzınıza layık olana kadar pişiriyoruz :) Lütfen sabırlı olun; hazır olduklarında size haber vereceğiz.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Hop! URL geçersiz gibi görünüyor. Lütfen tekrar kontrol edin ve yeniden deneyin.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hop! Desteklenmeyen bir yöntem kullanıyorsunuz (yalnızca önyüz). Lütfen WebUI'yi arkayüzden sunun.",
"Open": "Aç",
"Open AI": "Open AI",
"Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Yeni sohbet aç",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "OpenAI API Anahtarı",
"OpenAI API Key is required.": "OpenAI API Anahtarı gereklidir.",
"or": "veya",
"Parameters": "Parametreler",
"Password": "Parola",
"PDF Extract Images (OCR)": "PDF Görüntülerini Çıkart (OCR)",
"pending": "beklemede",
"Permission denied when accessing microphone: {{error}}": "Mikrofona erişim izni reddedildi: {{error}}",
"Playground": "Oyun Alanı",
"Profile": "Profil",
"Prompt Content": "Prompt İçeriği",
"Prompt suggestions": "Prompt önerileri",
"Prompts": "Promptlar",
"Pull a model from Ollama.com": "Ollama.com'dan bir model çekin",
"Pull Progress": "Çekme İlerlemesi",
"Query Params": "Sorgu Parametreleri",
"RAG Template": "RAG Şablonu",
"Raw Format": "Ham Format",
"Record voice": "Ses kaydı yap",
"Redirecting you to OpenWebUI Community": "OpenWebUI Topluluğuna yönlendiriliyorsunuz",
"Release Notes": "Sürüm Notları",
"Repeat Last N": "Son N'yi Tekrar Et",
"Repeat Penalty": "Tekrar Cezası",
"Request Mode": "İstek Modu",
"Reset Vector Storage": "Vektör Depolamayı Sıfırla",
"Response AutoCopy to Clipboard": "Yanıtı Panoya Otomatik Kopyala",
"Role": "Rol",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Kaydet",
"Save & Create": "Kaydet ve Oluştur",
"Save & Submit": "Kaydet ve Gönder",
"Save & Update": "Kaydet ve Güncelle",
"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": "Sohbet kayıtlarının doğrudan tarayıcınızın depolama alanına kaydedilmesi artık desteklenmemektedir. Lütfen aşağıdaki butona tıklayarak sohbet kayıtlarınızı indirmek ve silmek için bir dakikanızı ayırın. Endişelenmeyin, sohbet günlüklerinizi arkayüze kolayca yeniden aktarabilirsiniz:",
"Scan": "Tarama",
"Scan complete!": "Tarama tamamlandı!",
"Scan for documents from {{path}}": "{{path}} dizininden belgeleri tarayın",
"Search": "Ara",
"Search Documents": "Belgeleri Ara",
"Search Prompts": "Prompt Ara",
"See readme.md for instructions": "Yönergeler için readme.md dosyasına bakın",
"See what's new": "Yeniliklere göz atın",
"Seed": "Seed",
"Select a mode": "Bir mod seç",
"Select a model": "Bir model seç",
"Select an Ollama instance": "Bir Ollama örneği seçin",
"Send a Message": "Bir Mesaj Gönder",
"Send message": "Mesaj gönder",
"Server connection verified": "Sunucu bağlantısı doğrulandı",
"Set as default": "Varsayılan olarak ayarla",
"Set Default Model": "Varsayılan Modeli Ayarla",
"Set Image Size": "Görüntü Boyutunu Ayarla",
"Set Steps": "Adımları Ayarla",
"Set Title Auto-Generation Model": "Otomatik Başlık Oluşturma Modelini Ayarla",
"Set Voice": "Ses Ayarla",
"Settings": "Ayarlar",
"Settings saved successfully!": "Ayarlar başarıyla kaydedildi!",
"Share to OpenWebUI Community": "OpenWebUI Topluluğu ile Paylaş",
"short-summary": "kısa-özet",
"Show": "Göster",
"Show Additional Params": "Ek Parametreleri Göster",
"Show shortcuts": "Kısayolları göster",
"sidebar": "kenar çubuğu",
"Sign in": "Oturum aç",
"Sign Out": ıkış Yap",
"Sign up": "Kaydol",
"Speech recognition error: {{error}}": "Konuşma tanıma hatası: {{error}}",
"Speech-to-Text Engine": "Konuşmadan Metne Motoru",
"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API bu tarayıcıda desteklenmiyor.",
"Stop Sequence": "Diziyi Durdur",
"STT Settings": "STT Ayarları",
"Submit": "Gönder",
"Success": "Başarılı",
"Successfully updated.": "Başarıyla güncellendi.",
"Sync All": "Tümünü Senkronize Et",
"System": "Sistem",
"System Prompt": "Sistem Promptu",
"Tags": "Etiketler",
"Temperature": "Temperature",
"Template": "Şablon",
"Text Completion": "Metin Tamamlama",
"Text-to-Speech Engine": "Metinden Sese Motoru",
"Tfs Z": "Tfs Z",
"Theme": "Tema",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Bu, önemli konuşmalarınızın güvenli bir şekilde arkayüz veritabanınıza kaydedildiğini garantiler. Teşekkür ederiz!",
"This setting does not sync across browsers or devices.": "Bu ayar tarayıcılar veya cihazlar arasında senkronize edilmez.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "İpucu: Her değiştirmeden sonra sohbet girişinde tab tuşuna basarak birden fazla değişken yuvasını art arda güncelleyin.",
"Title": "Başlık",
"Title Auto-Generation": "Otomatik Başlık Oluşturma",
"Title Generation Prompt": "Başlık Oluşturma Promptu",
"to": "için",
"To access the available model names for downloading,": "İndirilebilir mevcut model adlarına erişmek için,",
"To access the GGUF models available for downloading,": "İndirilebilir mevcut GGUF modellerine erişmek için,",
"to chat input.": "sohbet girişine.",
"Toggle settings": "Ayarları Aç/Kapat",
"Toggle sidebar": "Kenar Çubuğunu Aç/Kapat",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Ollama'ya erişmede sorun mu yaşıyorsunuz?",
"TTS Settings": "TTS Ayarları",
"Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (Download) URL'sini Yazın",
"Uh-oh! There was an issue connecting to {{provider}}.": "Ah! {{provider}}'a bağlanırken bir sorun oluştu.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Bilinmeyen Dosya Türü '{{file_type}}', ancak düz metin olarak kabul ediliyor ve işleniyor",
"Update password": "Parolayı Güncelle",
"Upload a GGUF model": "Bir GGUF modeli yükle",
"Upload files": "Dosyaları Yükle",
"Upload Progress": "Yükleme İlerlemesi",
"URL Mode": "URL Modu",
"Use '#' in the prompt input to load and select your documents.": "Belgelerinizi yüklemek ve seçmek için promptda '#' kullanın.",
"Use Gravatar": "Gravatar Kullan",
"user": "kullanıcı",
"User Permissions": "Kullanıcı İzinleri",
"Users": "Kullanıcılar",
"Utilize": "Kullan",
"Valid time units:": "Geçerli zaman birimleri:",
"variable": "değişken",
"variable to have them replaced with clipboard content.": "panodaki içerikle değiştirilmesi için değişken.",
"Version": "Sürüm",
"Web": "Web",
"WebUI Add-ons": "WebUI Eklentileri",
"WebUI Settings": "WebUI Ayarları",
"WebUI will make requests to": "WebUI, isteklerde bulunacak:",
"Whats New in": "Yenilikler:",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Geçmiş kapatıldığında, bu tarayıcıdaki yeni sohbetler hiçbir cihazınızdaki geçmişinizde görünmez.",
"Whisper (Local)": "Whisper (Yerel)",
"Write a prompt suggestion (e.g. Who are you?)": "Bir prompt önerisi yazın (örn. Sen kimsin?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "[Konuyu veya anahtar kelimeyi] özetleyen 50 kelimelik bir özet yazın.",
"You": "Siz",
"You're a helpful assistant.": "Sen yardımcı bir asistansın.",
"You're now logged in.": "Şimdi oturum açtınız."
}

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "Видалено {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "Видалено {{deleteModelTag}}",
"Deleted {tagName}": "Видалено {tagName}", "Deleted {tagName}": "Видалено {tagName}",
"Description": "Опис", "Description": "Опис",
"Desktop Notifications": "Сповіщення", "Notifications": "Сповіщення",
"Disabled": "Вимкнено", "Disabled": "Вимкнено",
"Discover a modelfile": "Знайти файл моделі", "Discover a modelfile": "Знайти файл моделі",
"Discover a prompt": "Знайти промт", "Discover a prompt": "Знайти промт",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "Đã xóa {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "Đã xóa {{deleteModelTag}}",
"Deleted {tagName}": "Đã xóa {tagName}", "Deleted {tagName}": "Đã xóa {tagName}",
"Description": "Mô tả", "Description": "Mô tả",
"Desktop Notifications": "Thông báo trên máy tính (Notification)", "Notifications": "Thông báo trên máy tính (Notification)",
"Disabled": "Đã vô hiệu hóa", "Disabled": "Đã vô hiệu hóa",
"Discover a modelfile": "Khám phá thêm các mô hình mới", "Discover a modelfile": "Khám phá thêm các mô hình mới",
"Discover a prompt": "Khám phá thêm prompt mới", "Discover a prompt": "Khám phá thêm prompt mới",

View file

@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "已删除{{deleteModelTag}}", "Deleted {{deleteModelTag}}": "已删除{{deleteModelTag}}",
"Deleted {tagName}": "已删除{tagName}", "Deleted {tagName}": "已删除{tagName}",
"Description": "描述", "Description": "描述",
"Desktop Notifications": "桌面通知", "Notifications": "桌面通知",
"Disabled": "禁用", "Disabled": "禁用",
"Discover a modelfile": "探索模型文件", "Discover a modelfile": "探索模型文件",
"Discover a prompt": "探索提示词", "Discover a prompt": "探索提示词",

View file

@ -101,7 +101,7 @@
"Deleted {{deleteModelTag}}": "已刪除 {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "已刪除 {{deleteModelTag}}",
"Deleted {tagName}": "已刪除 {tagName}", "Deleted {tagName}": "已刪除 {tagName}",
"Description": "描述", "Description": "描述",
"Desktop Notifications": "桌面通知", "Notifications": "桌面通知",
"Disabled": "已停用", "Disabled": "已停用",
"Discover a modelfile": "發現新 Modelfile", "Discover a modelfile": "發現新 Modelfile",
"Discover a prompt": "發現新提示詞", "Discover a prompt": "發現新提示詞",

View file

@ -111,6 +111,82 @@ export const getGravatarURL = (email) => {
return `https://www.gravatar.com/avatar/${hash}`; return `https://www.gravatar.com/avatar/${hash}`;
}; };
export const canvasPixelTest = () => {
// Test a 1x1 pixel to potentially identify browser/plugin fingerprint blocking or spoofing
// Inspiration: https://github.com/kkapsner/CanvasBlocker/blob/master/test/detectionTest.js
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.height = 1;
canvas.width = 1;
const imageData = new ImageData(canvas.width, canvas.height);
const pixelValues = imageData.data;
// Generate RGB test data
for (let i = 0; i < imageData.data.length; i += 1) {
if (i % 4 !== 3) {
pixelValues[i] = Math.floor(256 * Math.random());
} else {
pixelValues[i] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
// Read RGB data and fail if unmatched
for (let i = 0; i < p.length; i += 1) {
if (p[i] !== pixelValues[i]) {
console.log(
'canvasPixelTest: Wrong canvas pixel RGB value detected:',
p[i],
'at:',
i,
'expected:',
pixelValues[i]
);
console.log('canvasPixelTest: Canvas blocking or spoofing is likely');
return false;
}
}
return true;
};
export const generateInitialsImage = (name) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 100;
canvas.height = 100;
if (!canvasPixelTest()) {
console.log(
'generateInitialsImage: failed pixel test, fingerprint evasion is likely. Using default image.'
);
return '/user.png';
}
ctx.fillStyle = '#F39C12';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#FFFFFF';
ctx.font = '40px Helvetica';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const sanitizedName = name.trim();
const initials =
sanitizedName.length > 0
? sanitizedName[0] +
(sanitizedName.split(' ').length > 1
? sanitizedName[sanitizedName.lastIndexOf(' ') + 1]
: '')
: '';
ctx.fillText(initials.toUpperCase(), canvas.width / 2, canvas.height / 2);
return canvas.toDataURL();
};
export const copyToClipboard = (text) => { export const copyToClipboard = (text) => {
if (!navigator.clipboard) { if (!navigator.clipboard) {
const textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');

View file

@ -4,6 +4,8 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import dayjs from 'dayjs';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users';
@ -16,6 +18,7 @@
let loaded = false; let loaded = false;
let users = []; let users = [];
let search = '';
let selectedUser = null; let selectedUser = null;
let showSettingsModal = false; let showSettingsModal = false;
@ -80,20 +83,15 @@
<SettingsModal bind:show={showSettingsModal} /> <SettingsModal bind:show={showSettingsModal} />
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white font-mona"> <div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
{#if loaded} {#if loaded}
<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=" mx-auto w-full">
<div class="w-full"> <div class="w-full">
<div class=" flex flex-col justify-center"> <div class=" flex flex-col justify-center">
<div class=" px-5 pt-3">
<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">Dashboard</div>
{$i18n.t('All Users')}
<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"
>{users.length}</span
>
</div>
<div> <div>
<button <button
class="flex items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition" class="flex items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition"
@ -119,32 +117,64 @@
</button> </button>
</div> </div>
</div> </div>
<div class=" text-gray-500 text-xs mt-1">
{$i18n.t("Click on the user role button to change a user's role.")}
</div> </div>
<hr class=" my-3 dark:border-gray-600" /> <div class="px-5 flex text-sm gap-2.5">
<div class="py-3 border-b font-medium text-gray-100 cursor-pointer">Overview</div>
<!-- <div class="py-3 text-gray-300 cursor-pointer">Users</div> -->
</div>
<hr class=" mb-3 dark:border-gray-800" />
<div class="px-5">
<div class="mt-0.5 mb-3 flex justify-between">
<div class="flex text-lg font-medium px-0.5">
{$i18n.t('All Users')}
<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"
>{users.length}</span
>
</div>
<div class="">
<input
class=" w-60 rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Search')}
bind:value={search}
/>
</div>
</div>
<div class="scrollbar-hidden relative overflow-x-auto whitespace-nowrap"> <div class="scrollbar-hidden relative overflow-x-auto whitespace-nowrap">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto"> <table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto">
<thead <thead
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-850 dark:text-gray-400"
> >
<tr> <tr>
<th scope="col" class="px-3 py-2"> {$i18n.t('Role')} </th> <th scope="col" class="px-3 py-2"> {$i18n.t('Role')} </th>
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th> <th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
<th scope="col" class="px-3 py-2"> {$i18n.t('Email')} </th> <th scope="col" class="px-3 py-2"> {$i18n.t('Email')} </th>
<th scope="col" class="px-3 py-2"> {$i18n.t('Action')} </th> <th scope="col" class="px-3 py-2"> {$i18n.t('Created at')} </th>
<th scope="col" class="px-3 py-2 text-right" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each users as user} {#each users.filter((user) => {
if (search === '') {
return true;
} else {
let name = user.name.toLowerCase();
const query = search.toLowerCase();
return name.includes(query);
}
}) as user}
<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700 text-xs"> <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"> <td class="px-3 py-2 min-w-[7rem] w-28">
<button <button
class=" flex items-center gap-2 text-xs px-3 py-0.5 rounded-lg {user.role === class=" flex items-center gap-2 text-xs px-3 py-0.5 rounded-lg {user.role ===
'admin' && 'admin' &&
'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {user.role === 'user' && 'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {user.role ===
'user' &&
'text-green-600 dark:text-green-200 bg-green-200/30'} {user.role === 'text-green-600 dark:text-green-200 bg-green-200/30'} {user.role ===
'pending' && 'text-gray-600 dark:text-gray-200 bg-gray-200/30'}" 'pending' && 'text-gray-600 dark:text-gray-200 bg-gray-200/30'}"
on:click={() => { on:click={() => {
@ -179,8 +209,12 @@
</td> </td>
<td class=" px-3 py-2"> {user.email} </td> <td class=" px-3 py-2"> {user.email} </td>
<td class="px-3 py-2"> <td class=" px-3 py-2">
<div class="flex justify-start w-full"> {dayjs(user.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
</td>
<td class="px-3 py-2 text-right">
<div class="flex justify-end w-full">
<button <button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => { on:click={async () => {
@ -232,6 +266,11 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<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>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,6 +6,7 @@
import { WEBUI_NAME, config, user } from '$lib/stores'; import { WEBUI_NAME, config, user } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -36,10 +37,12 @@
}; };
const signUpHandler = async () => { const signUpHandler = async () => {
const sessionUser = await userSignUp(name, email, password).catch((error) => { const sessionUser = await userSignUp(name, email, password, generateInitialsImage(name)).catch(
(error) => {
toast.error(error); toast.error(error);
return null; return null;
}); }
);
await setSessionUser(sessionUser); await setSessionUser(sessionUser);
}; };