diff --git a/.env.example b/.env.example index 3d2aafc09..05854cd0f 100644 --- a/.env.example +++ b/.env.example @@ -9,4 +9,8 @@ OPENAI_API_KEY='' # DO NOT TRACK SCARF_NO_ANALYTICS=true -DO_NOT_TRACK=true \ No newline at end of file +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" diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 259f0c5ff..036bb97ae 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -57,3 +57,14 @@ jobs: path: . env: 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 }}', + }) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 6270d69af..44c9c654b 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -2,6 +2,7 @@ name: Create and publish Docker images with specific build args # Configures this workflow to run every time a change is pushed to the branch called `release`. on: + workflow_dispatch: push: branches: - main diff --git a/CHANGELOG.md b/CHANGELOG.md index e48f8dc7a..b1fd38b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.117] - 2024-04-03 + +### Added + +- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users. +- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries. +- 📄 **Chat Download as PDF**: Easily download chats in PDF format. +- 📝 **Improved Logging**: Enhancements to logging functionality. +- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header. + +### Fixed + +- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users. +- ⚪ **White Theme Styling**: Resolved styling issue with the white theme. +- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering. +- 🔒 **Security Patches**: Applied necessary security patches. + ## [0.1.116] - 2024-03-31 ### Added diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index b89d7bf52..5e19a8e36 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -215,7 +215,8 @@ async def get_ollama_versions(url_idx: Optional[int] = None): if len(responses) > 0: 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"]} diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index b9d70b0a9..08639866f 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -8,7 +8,7 @@ from fastapi import ( Form, ) from fastapi.middleware.cors import CORSMiddleware -import os, shutil, logging +import os, shutil, logging, re from pathlib import Path from typing import List @@ -438,25 +438,11 @@ def store_doc( log.info(f"file.content_type: {file.content_type}") try: - is_valid_filename = True unsanitized_filename = file.filename - if not unsanitized_filename.isascii(): - is_valid_filename = False + filename = os.path.basename(unsanitized_filename) - unvalidated_file_path = f"{UPLOAD_DIR}/{unsanitized_filename}" - dereferenced_file_path = str(Path(unvalidated_file_path).resolve(strict=False)) - if not dereferenced_file_path.startswith(UPLOAD_DIR): - is_valid_filename = False + file_path = f"{UPLOAD_DIR}/{filename}" - 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() with open(file_path, "wb") as f: f.write(contents) @@ -467,7 +453,7 @@ def store_doc( collection_name = calculate_sha256(f)[:63] 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() try: diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py index 069865036..a97312ff9 100644 --- a/backend/apps/web/models/auths.py +++ b/backend/apps/web/models/auths.py @@ -86,6 +86,7 @@ class SignupForm(BaseModel): name: str email: str password: str + profile_image_url: Optional[str] = "/user.png" class AuthsTable: @@ -94,7 +95,12 @@ class AuthsTable: self.db.create_tables([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]: log.info("insert_new_auth") @@ -105,7 +111,7 @@ class AuthsTable: ) 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: return user diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py index 95a673cb8..ef16ce731 100644 --- a/backend/apps/web/models/chats.py +++ b/backend/apps/web/models/chats.py @@ -206,6 +206,18 @@ class ChatTable: except: 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]: try: chat = Chat.get(Chat.id == id, Chat.user_id == user_id) diff --git a/backend/apps/web/models/users.py b/backend/apps/web/models/users.py index a01e595e5..7d1e182da 100644 --- a/backend/apps/web/models/users.py +++ b/backend/apps/web/models/users.py @@ -31,7 +31,7 @@ class UserModel(BaseModel): name: str email: str role: str = "pending" - profile_image_url: str = "/user.png" + profile_image_url: str timestamp: int # timestamp in epoch api_key: Optional[str] = None @@ -59,7 +59,12 @@ class UsersTable: self.db.create_tables([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]: user = UserModel( **{ @@ -67,7 +72,7 @@ class UsersTable: "name": name, "email": email, "role": role, - "profile_image_url": "/user.png", + "profile_image_url": profile_image_url, "timestamp": int(time.time()), } ) diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 293cb55b8..89d8c1c8f 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -163,7 +163,11 @@ async def signup(request: Request, form_data: SignupForm): ) hashed = get_password_hash(form_data.password) 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: diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index 660a0d7f6..2e2bb5b00 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -251,7 +251,15 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)): @router.get("/share/{share_id}", response_model=Optional[ChatResponse]) async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)): - chat = Chats.get_chat_by_id(share_id) + 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) if chat: return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) diff --git a/backend/apps/web/routers/utils.py b/backend/apps/web/routers/utils.py index 4b5ac8cfa..0ee75cfe6 100644 --- a/backend/apps/web/routers/utils.py +++ b/backend/apps/web/routers/utils.py @@ -1,14 +1,11 @@ -from fastapi import APIRouter, UploadFile, File, BackgroundTasks +from fastapi import APIRouter, UploadFile, File, Response from fastapi import Depends, HTTPException, status from starlette.responses import StreamingResponse, FileResponse - - from pydantic import BaseModel -import requests -import os -import aiohttp -import json + +from fpdf import FPDF +import markdown from utils.utils import get_admin_user @@ -16,7 +13,7 @@ from utils.misc import calculate_sha256, get_gravatar_url from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR from constants import ERROR_MESSAGES - +from typing import List router = APIRouter() @@ -28,6 +25,70 @@ async def get_gravatar( return get_gravatar_url(email) +class MarkdownForm(BaseModel): + md: str + + +@router.post("/markdown") +async def get_html_from_markdown( + form_data: MarkdownForm, +): + 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") async def download_db(user=Depends(get_admin_user)): diff --git a/backend/config.py b/backend/config.py index c1f0b590d..402a4183e 100644 --- a/backend/config.py +++ b/backend/config.py @@ -25,8 +25,9 @@ try: except ImportError: log.warning("dotenv not installed, skipping...") -WEBUI_NAME = "Open WebUI" +WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI") WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" + shutil.copyfile("../build/favicon.png", "./static/favicon.png") #################################### @@ -149,6 +150,7 @@ log.setLevel(SRC_LOG_LEVELS["CONFIG"]) #################################### CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "") + if CUSTOM_NAME: try: r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}") @@ -171,7 +173,9 @@ if CUSTOM_NAME: except Exception as e: log.exception(e) pass - +else: + if WEBUI_NAME != "Open WebUI": + WEBUI_NAME += " (Open WebUI)" #################################### # DATA/FRONTEND BUILD DIR diff --git a/backend/main.py b/backend/main.py index f2d2a1546..f574e7bab 100644 --- a/backend/main.py +++ b/backend/main.py @@ -84,7 +84,6 @@ app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST app.state.WEBHOOK_URL = WEBHOOK_URL - origins = ["*"] @@ -284,6 +283,20 @@ async def get_app_latest_release_version(): ) +@app.get("/manifest.json") +async def get_manifest_json(): + return { + "name": WEBUI_NAME, + "short_name": WEBUI_NAME, + "start_url": "/", + "display": "standalone", + "background_color": "#343541", + "theme_color": "#343541", + "orientation": "portrait-primary", + "icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}], + } + + app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/cache", StaticFiles(directory="data/cache"), name="cache") diff --git a/backend/requirements.txt b/backend/requirements.txt index 67213e54d..c815d93da 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,6 +18,8 @@ peewee-migrate bcrypt litellm==1.30.7 +boto3 + argon2-cffi apscheduler google-generativeai @@ -40,6 +42,8 @@ xlrd opencv-python-headless rapidocr-onnxruntime +fpdf2 + faster-whisper PyJWT diff --git a/backend/static/fonts/NotoSans-Bold.ttf b/backend/static/fonts/NotoSans-Bold.ttf new file mode 100644 index 000000000..d84248ed1 Binary files /dev/null and b/backend/static/fonts/NotoSans-Bold.ttf differ diff --git a/backend/static/fonts/NotoSans-Italic.ttf b/backend/static/fonts/NotoSans-Italic.ttf new file mode 100644 index 000000000..c40c3562c Binary files /dev/null and b/backend/static/fonts/NotoSans-Italic.ttf differ diff --git a/backend/static/fonts/NotoSans-Regular.ttf b/backend/static/fonts/NotoSans-Regular.ttf new file mode 100644 index 000000000..fa4cff505 Binary files /dev/null and b/backend/static/fonts/NotoSans-Regular.ttf differ diff --git a/backend/static/fonts/NotoSansJP-Regular.ttf b/backend/static/fonts/NotoSansJP-Regular.ttf new file mode 100644 index 000000000..1583096a2 Binary files /dev/null and b/backend/static/fonts/NotoSansJP-Regular.ttf differ diff --git a/backend/static/fonts/NotoSansKR-Regular.ttf b/backend/static/fonts/NotoSansKR-Regular.ttf new file mode 100644 index 000000000..1b14d3247 Binary files /dev/null and b/backend/static/fonts/NotoSansKR-Regular.ttf differ diff --git a/docker-compose.amdgpu.yaml b/docker-compose.amdgpu.yaml new file mode 100644 index 000000000..7a1295d94 --- /dev/null +++ b/docker-compose.amdgpu.yaml @@ -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}' \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index f69084b8a..9daba312a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: pull_policy: always tty: true restart: unless-stopped - image: ollama/ollama:latest + image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest} open-webui: build: @@ -16,7 +16,7 @@ services: args: OLLAMA_BASE_URL: '/ollama' 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 volumes: - open-webui:/app/backend/data diff --git a/kubernetes/helm/templates/_helpers.tpl b/kubernetes/helm/templates/_helpers.tpl index 0647a42ae..3f42735a6 100644 --- a/kubernetes/helm/templates/_helpers.tpl +++ b/kubernetes/helm/templates/_helpers.tpl @@ -7,7 +7,7 @@ ollama {{- end -}} {{- 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 }} {{- define "chart.name" -}} diff --git a/package-lock.json b/package-lock.json index bb07683be..cf1496859 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.1.116", + "version": "0.1.117", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.1.116", + "version": "0.1.117", "dependencies": { "@sveltejs/adapter-node": "^1.3.1", "async": "^3.2.5", @@ -19,6 +19,7 @@ "i18next-resources-to-backend": "^1.2.0", "idb": "^7.1.1", "js-sha256": "^0.10.1", + "jspdf": "^2.5.1", "katex": "^0.16.9", "marked": "^9.1.0", "svelte-sonner": "^0.3.19", @@ -1067,6 +1068,12 @@ "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", "dev": true }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "optional": true + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -1402,6 +1409,17 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -1459,6 +1477,15 @@ "dev": true, "optional": true }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1666,6 +1693,17 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1758,6 +1796,31 @@ } ] }, + "node_modules/canvg": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", + "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/canvg/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "optional": true + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1944,6 +2007,17 @@ "node": ">= 0.6" } }, + "node_modules/core-js": { + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz", + "integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==", + "hasInstallScript": true, + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -1964,6 +2038,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -2156,6 +2239,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.9.tgz", + "integrity": "sha512-iHtnxYMotKgOTvxIqq677JsKHvCOkAFqj9x8Mek2zdeHW1XjuFKwjpmZeMaXQRQ8AbJZDbcRz/+r1QhwvFtmQg==", + "optional": true + }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -2584,6 +2673,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3003,6 +3097,19 @@ "node": ">=12.0.0" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -3403,10 +3510,27 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jspdf": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", + "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==", + "dependencies": { + "@babel/runtime": "^7.14.0", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.4.8" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.2.0", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/katex": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", - "integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", + "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -3971,6 +4095,12 @@ "node": ">=8" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "optional": true + }, "node_modules/periscopic": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", @@ -4391,6 +4521,15 @@ "rimraf": "bin.js" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4494,6 +4633,15 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4814,6 +4962,15 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stream-composer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", @@ -5215,6 +5372,15 @@ "@types/estree": "*" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/symlink-or-copy": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", @@ -5353,6 +5519,15 @@ "streamx": "^2.12.5" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5583,6 +5758,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -5676,9 +5860,9 @@ } }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", diff --git a/package.json b/package.json index 9178a3f57..aec1bf2aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.1.116", + "version": "0.1.117", "private": true, "scripts": { "dev": "vite dev --host", @@ -53,6 +53,7 @@ "i18next-resources-to-backend": "^1.2.0", "idb": "^7.1.1", "js-sha256": "^0.10.1", + "jspdf": "^2.5.1", "katex": "^0.16.9", "marked": "^9.1.0", "svelte-sonner": "^0.3.19", diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts index 548a9418d..efeeff333 100644 --- a/src/lib/apis/auths/index.ts +++ b/src/lib/apis/auths/index.ts @@ -58,7 +58,12 @@ export const userSignIn = async (email: string, password: string) => { 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; 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({ name: name, email: email, - password: password + password: password, + profile_image_url: profile_image_url }) }) .then(async (res) => { diff --git a/src/lib/apis/utils/index.ts b/src/lib/apis/utils/index.ts index bcb554077..ef6b0d25e 100644 --- a/src/lib/apis/utils/index.ts +++ b/src/lib/apis/utils/index.ts @@ -22,6 +22,57 @@ export const getGravatarUrl = async (email: string) => { 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) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + md: md + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + return res.html; +}; + export const downloadDatabase = async (token: string) => { let error = null; diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index a23649d8b..eff65a254 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -295,6 +295,13 @@ const dropZone = document.querySelector('body'); + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + console.log('Escape'); + dragged = false; + } + }; + const onDragOver = (e) => { e.preventDefault(); dragged = true; @@ -350,11 +357,15 @@ dragged = false; }; + window.addEventListener('keydown', handleKeyDown); + dropZone?.addEventListener('dragover', onDragOver); dropZone?.addEventListener('drop', onDrop); dropZone?.addEventListener('dragleave', onDragLeave); return () => { + window.removeEventListener('keydown', handleKeyDown); + dropZone?.removeEventListener('dragover', onDragOver); dropZone?.removeEventListener('drop', onDrop); dropZone?.removeEventListener('dragleave', onDragLeave); diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 3888d764e..aa2fab2c0 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -17,7 +17,11 @@ import { config, settings } from '$lib/stores'; import { synthesizeOpenAISpeech } from '$lib/apis/openai'; import { imageGenerations } from '$lib/apis/images'; - import { extractSentences } from '$lib/utils'; + import { + extractSentences, + revertSanitizedResponseContent, + sanitizeResponseContent + } from '$lib/utils'; import Name from './Name.svelte'; import ProfileImage from './ProfileImage.svelte'; @@ -56,7 +60,7 @@ let loadingSpeech = false; let generatingImage = false; - $: tokens = marked.lexer(message.content); + $: tokens = marked.lexer(sanitizeResponseContent(message.content)); const renderer = new marked.Renderer(); @@ -405,8 +409,10 @@ {:else} {#each tokens as token} {#if token.type === 'code'} - <!-- {token.text} --> - <CodeBlock lang={token.lang} code={token.text} /> + <CodeBlock + lang={token.lang} + code={revertSanitizedResponseContent(token.text)} + /> {:else} {@html marked.parse(token.raw, { ...defaults, diff --git a/src/lib/components/chat/Settings/About.svelte b/src/lib/components/chat/Settings/About.svelte index 3b3d85df7..dad1f0ae6 100644 --- a/src/lib/components/chat/Settings/About.svelte +++ b/src/lib/components/chat/Settings/About.svelte @@ -6,6 +6,8 @@ import { compareVersion } from '$lib/utils'; import { onMount, getContext } from 'svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + const i18n = getContext('i18n'); let ollamaVersion = ''; @@ -51,8 +53,10 @@ </div> <div class="flex w-full justify-between items-center"> <div class="flex flex-col text-xs text-gray-700 dark:text-gray-200"> - <div> - v{WEBUI_VERSION} + <div class="flex gap-1"> + <Tooltip content={WEBUI_VERSION === '0.1.117' ? "🪖 We're just getting started." : ''}> + v{WEBUI_VERSION} + </Tooltip> <a href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}" @@ -126,7 +130,9 @@ </div> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> - {$i18n.t('Created by')} + {#if !$WEBUI_NAME.includes('Open WebUI')} + <span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> - + {/if}{$i18n.t('Created by')} <a class=" text-gray-500 dark:text-gray-300 font-medium" href="https://github.com/tjbck" diff --git a/src/lib/components/chat/Settings/Account.svelte b/src/lib/components/chat/Settings/Account.svelte index e0239fb0b..81652c0dc 100644 --- a/src/lib/components/chat/Settings/Account.svelte +++ b/src/lib/components/chat/Settings/Account.svelte @@ -7,6 +7,7 @@ import UpdatePassword from './Account/UpdatePassword.svelte'; import { getGravatarUrl } from '$lib/apis/utils'; + import { generateInitialsImage, canvasPixelTest } from '$lib/utils'; import { copyToClipboard } from '$lib/utils'; import Plus from '$lib/components/icons/Plus.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; @@ -18,6 +19,8 @@ let profileImageUrl = ''; let name = ''; + let showAPIKeys = false; + let showJWTToken = false; let JWTTokenCopied = false; @@ -28,6 +31,12 @@ let profileImageInputElement: HTMLInputElement; 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( (error) => { toast.error(error); @@ -125,59 +134,93 @@ }} /> - <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 flex-col"> - <div class="self-center"> - <button - class="relative rounded-full dark:bg-gray-700" - type="button" - on:click={() => { - profileImageInputElement.click(); - }} - > - <img - src={profileImageUrl !== '' ? profileImageUrl : '/user.png'} - alt="profile" - class=" rounded-full w-16 h-16 object-cover" - /> - - <div - class="absolute flex justify-center rounded-full bottom-0 left-0 right-0 top-0 h-full w-full overflow-hidden bg-gray-700 bg-fixed opacity-0 transition duration-300 ease-in-out hover:opacity-50" + <div class="flex space-x-5"> + <div class="flex flex-col"> + <div class="self-center mt-2"> + <button + class="relative rounded-full dark:bg-gray-700" + type="button" + on:click={() => { + profileImageInputElement.click(); + }} > - <div class="my-auto text-gray-100"> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - class="w-5 h-5" - > - <path - d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" - /> - </svg> - </div> - </div> - </button> - </div> - <button - class=" text-xs text-gray-600" - on:click={async () => { - const url = await getGravatarUrl($user.email); + <img + src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)} + alt="profile" + class=" rounded-full size-16 object-cover" + /> - profileImageUrl = url; - }}>{$i18n.t('Use Gravatar')}</button - > + <div + class="absolute flex justify-center rounded-full bottom-0 left-0 right-0 top-0 h-full w-full overflow-hidden bg-gray-700 bg-fixed opacity-0 transition duration-300 ease-in-out hover:opacity-50" + > + <div class="my-auto text-gray-100"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" + /> + </svg> + </div> + </div> + </button> + </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 + 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 () => { + const url = await getGravatarUrl($user.email); + + profileImageUrl = url; + }}>{$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 class="flex-1"> + <div class="pt-0.5"> <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"> <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" bind:value={name} required @@ -187,133 +230,46 @@ </div> </div> - <hr class=" dark:border-gray-700 my-4" /> - <UpdatePassword /> + <div class="py-0.5"> + <UpdatePassword /> + </div> <hr class=" dark:border-gray-700 my-4" /> - <div class="flex flex-col gap-4"> - <div class="justify-between w-full"> - <div class="flex justify-between w-full"> - <div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div> - </div> + <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> - <div class="flex mt-2"> - <div class="flex w-full"> - <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" - type={showJWTToken ? 'text' : 'password'} - value={localStorage.token} - disabled - /> - - <button - class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800" - on:click={() => { - showJWTToken = !showJWTToken; - }} - > - {#if showJWTToken} - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 16 16" - fill="currentColor" - class="w-4 h-4" - > - <path - fill-rule="evenodd" - d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z" - clip-rule="evenodd" - /> - <path - d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z" - /> - </svg> - {:else} - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 16 16" - fill="currentColor" - class="w-4 h-4" - > - <path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" /> - <path - fill-rule="evenodd" - d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" - clip-rule="evenodd" - /> - </svg> - {/if} - </button> + {#if showAPIKeys} + <div class="flex flex-col gap-4"> + <div class="justify-between w-full"> + <div class="flex justify-between w-full"> + <div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div> </div> - <button - class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg" - on:click={() => { - copyToClipboard(localStorage.token); - JWTTokenCopied = true; - setTimeout(() => { - JWTTokenCopied = false; - }, 2000); - }} - > - {#if JWTTokenCopied} - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - class="w-4 h-4" - > - <path - fill-rule="evenodd" - d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" - clip-rule="evenodd" - /> - </svg> - {:else} - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 16 16" - fill="currentColor" - class="w-4 h-4" - > - <path - fill-rule="evenodd" - d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z" - clip-rule="evenodd" - /> - <path - fill-rule="evenodd" - d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z" - clip-rule="evenodd" - /> - </svg> - {/if} - </button> - </div> - </div> - <div class="justify-between w-full"> - <div class="flex justify-between w-full"> - <div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div> - </div> - - <div class="flex mt-2"> - {#if APIKey} + <div class="flex mt-2"> <div class="flex w-full"> <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" - type={showAPIKey ? 'text' : 'password'} - value={APIKey} + 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'} + value={localStorage.token} disabled /> <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={() => { - showAPIKey = !showAPIKey; + showJWTToken = !showJWTToken; }} > - {#if showAPIKey} + {#if showJWTToken} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" @@ -348,16 +304,16 @@ </div> <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={() => { - copyToClipboard(APIKey); - APIKeyCopied = true; + copyToClipboard(localStorage.token); + JWTTokenCopied = true; setTimeout(() => { - APIKeyCopied = false; + JWTTokenCopied = false; }, 2000); }} > - {#if APIKeyCopied} + {#if JWTTokenCopied} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" @@ -390,45 +346,146 @@ </svg> {/if} </button> + </div> + </div> + <div class="justify-between w-full"> + <div class="flex justify-between w-full"> + <div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div> + </div> + + <div class="flex mt-2"> + {#if APIKey} + <div class="flex w-full"> + <input + 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'} + value={APIKey} + disabled + /> + + <button + class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850" + on:click={() => { + showAPIKey = !showAPIKey; + }} + > + {#if showAPIKey} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z" + clip-rule="evenodd" + /> + <path + d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z" + /> + </svg> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" /> + <path + fill-rule="evenodd" + d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" + clip-rule="evenodd" + /> + </svg> + {/if} + </button> + </div> - <Tooltip content="Create new key"> <button - class=" px-1.5 py-1 dark:hover:bg-gray-800transition rounded-lg" + class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" + on:click={() => { + copyToClipboard(APIKey); + APIKeyCopied = true; + setTimeout(() => { + APIKeyCopied = false; + }, 2000); + }} + > + {#if APIKeyCopied} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" + clip-rule="evenodd" + /> + </svg> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z" + clip-rule="evenodd" + /> + <path + fill-rule="evenodd" + d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z" + clip-rule="evenodd" + /> + </svg> + {/if} + </button> + + <Tooltip content="Create new key"> + <button + class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg" + on:click={() => { + createAPIKeyHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + class="size-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" + /> + </svg> + </button> + </Tooltip> + {:else} + <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-850 transition" on:click={() => { createAPIKeyHandler(); }} > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="2" - stroke="currentColor" - class="size-4" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" - /> - </svg> - </button> - </Tooltip> - {:else} - <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" - on:click={() => { - createAPIKeyHandler(); - }} - > - <Plus strokeWidth="2" className=" size-3.5" /> + <Plus strokeWidth="2" className=" size-3.5" /> - Create new secret key</button - > - {/if} + Create new secret key</button + > + {/if} + </div> </div> </div> - </div> + {/if} </div> <div class="flex justify-end pt-3 text-sm font-medium"> diff --git a/src/lib/components/chat/Settings/General.svelte b/src/lib/components/chat/Settings/General.svelte index 3b7126d85..3f6a79bd7 100644 --- a/src/lib/components/chat/Settings/General.svelte +++ b/src/lib/components/chat/Settings/General.svelte @@ -185,7 +185,7 @@ <div> <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 class="p-1 px-3 text-xs flex rounded transition" diff --git a/src/lib/components/chat/ShareChatModal.svelte b/src/lib/components/chat/ShareChatModal.svelte index 14945ab9d..4fa98947d 100644 --- a/src/lib/components/chat/ShareChatModal.svelte +++ b/src/lib/components/chat/ShareChatModal.svelte @@ -1,9 +1,6 @@ <script lang="ts"> import { getContext, onMount } from 'svelte'; - import fileSaver from 'file-saver'; - const { saveAs } = fileSaver; - import { toast } from 'svelte-sonner'; import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats'; import { chatId, modelfiles } from '$lib/stores'; @@ -55,29 +52,18 @@ ); }; - const downloadChat = async () => { - const _chat = chat.chat; - console.log('download', chat); - - const chatText = _chat.messages.reduce((a, message, i, arr) => { - return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`; - }, ''); - - let blob = new Blob([chatText], { - type: 'text/plain' - }); - - saveAs(blob, `chat-${_chat.title}.txt`); - }; - export let show = false; - onMount(async () => { - chatId.subscribe(async (value) => { - chat = await getChatById(localStorage.token, value); - console.log(chat); - }); - }); + $: if (show) { + (async () => { + if ($chatId) { + chat = await getChatById(localStorage.token, $chatId); + } else { + chat = null; + console.log(chat); + } + })(); + } </script> <Modal bind:show size="sm"> @@ -159,19 +145,6 @@ {/if} </button> </div> - <div class="flex gap-1 mt-1.5"> - <div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div> - <button - class=" text-right rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline" - type="button" - on:click={() => { - downloadChat(); - show = false; - }} - > - {$i18n.t('Download as a File')} - </button> - </div> </div> </div> </div> diff --git a/src/lib/components/common/Modal.svelte b/src/lib/components/common/Modal.svelte index 038fa48d8..776bfaaf9 100644 --- a/src/lib/components/common/Modal.svelte +++ b/src/lib/components/common/Modal.svelte @@ -7,6 +7,7 @@ export let show = true; export let size = 'md'; + let modalElement = null; let mounted = false; const sizeToWidth = (size) => { @@ -19,14 +20,23 @@ } }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + console.log('Escape'); + show = false; + } + }; + onMount(() => { mounted = true; }); $: if (mounted) { if (show) { + window.addEventListener('keydown', handleKeyDown); document.body.style.overflow = 'hidden'; } else { + window.removeEventListener('keydown', handleKeyDown); document.body.style.overflow = 'unset'; } } @@ -36,6 +46,7 @@ <!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-no-static-element-interactions --> <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" in:fade={{ duration: 10 }} on:click={() => { diff --git a/src/lib/components/layout/Navbar.svelte b/src/lib/components/layout/Navbar.svelte index 6bff2ed80..4f3806fc5 100644 --- a/src/lib/components/layout/Navbar.svelte +++ b/src/lib/components/layout/Navbar.svelte @@ -2,21 +2,13 @@ import { getContext } from 'svelte'; import { toast } from 'svelte-sonner'; - import { Separator } from 'bits-ui'; - import { getChatById, shareChatById } from '$lib/apis/chats'; import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores'; import { slide } from 'svelte/transition'; import ShareChatModal from '../chat/ShareChatModal.svelte'; - import TagInput from '../common/Tags/TagInput.svelte'; import ModelSelector from '../chat/ModelSelector.svelte'; import Tooltip from '../common/Tooltip.svelte'; - - import EllipsisVertical from '../icons/EllipsisVertical.svelte'; - import ChevronDown from '../icons/ChevronDown.svelte'; - import ChevronUpDown from '../icons/ChevronUpDown.svelte'; import Menu from './Navbar/Menu.svelte'; - import TagChatModal from '../chat/TagChatModal.svelte'; const i18n = getContext('i18n'); @@ -24,6 +16,7 @@ export let title: string = $WEBUI_NAME; export let shareEnabled: boolean = false; + export let chat; export let selectedModels; export let tags = []; @@ -33,63 +26,15 @@ export let showModelSelector = true; let showShareChatModal = false; - let showTagChatModal = false; + let showDownloadChatModal = false; </script> <ShareChatModal bind:show={showShareChatModal} /> -<!-- <TagChatModal bind:show={showTagChatModal} {tags} {deleteTag} {addTag} /> --> <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30"> <div class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'} w-full mx-auto px-3" > - <!-- {#if shareEnabled} - <div class="flex items-center w-full max-w-full"> - <div class=" flex-1 self-center font-medium line-clamp-1"> - <div> - {title != '' ? title : $WEBUI_NAME} - </div> - </div> - <div class="pl-2 self-center flex items-center"> - <div class=" mr-1"> - <Tags {tags} {deleteTag} {addTag} /> - </div> - - <Tooltip content="Share"> - <button - class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition" - on:click={async () => { - showShareChatModal = !showShareChatModal; - - // console.log(showShareChatModal); - }} - > - <div class=" m-auto self-center"> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 24 24" - fill="currentColor" - class="w-4 h-4" - > - <path - fill-rule="evenodd" - d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z" - clip-rule="evenodd" - /> - </svg> - </div> - </button> - </Tooltip> - </div> - </div> - {/if} --> - - <!-- <div class=" flex-1 self-center font-medium line-clamp-1"> - <div> - {title != '' ? title : $WEBUI_NAME} - </div> - </div> --> - <div class="flex items-center w-full max-w-full"> <div class="flex-1 overflow-hidden max-w-full"> {#if showModelSelector} @@ -132,10 +77,14 @@ </Tooltip> {:else} <Menu + {chat} {shareEnabled} shareHandler={() => { showShareChatModal = !showShareChatModal; }} + downloadHandler={() => { + showDownloadChatModal = !showDownloadChatModal; + }} {tags} {deleteTag} {addTag} diff --git a/src/lib/components/layout/Navbar/Menu.svelte b/src/lib/components/layout/Navbar/Menu.svelte index d27bd850d..65b8a35bb 100644 --- a/src/lib/components/layout/Navbar/Menu.svelte +++ b/src/lib/components/layout/Navbar/Menu.svelte @@ -1,23 +1,71 @@ <script lang="ts"> import { DropdownMenu } from 'bits-ui'; + + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { jsPDF } from 'jspdf'; + + import { showSettings } from '$lib/stores'; import { flyAndScale } from '$lib/utils/transitions'; import Dropdown from '$lib/components/common/Dropdown.svelte'; - import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; - import Pencil from '$lib/components/icons/Pencil.svelte'; - import Tooltip from '$lib/components/common/Tooltip.svelte'; - import { showSettings } from '$lib/stores'; import Tags from '$lib/components/common/Tags.svelte'; + import { WEBUI_BASE_URL } from '$lib/constants'; + import { downloadChatAsPDF } from '$lib/apis/utils'; export let shareEnabled: boolean = false; export let shareHandler: Function; + export let downloadHandler: Function; + // export let tagHandler: Function; + export let chat; export let tags; export let deleteTag: Function; export let addTag: Function; export let onClose: Function = () => {}; + + const downloadTxt = async () => { + const _chat = chat.chat; + console.log('download', chat); + + const chatText = _chat.messages.reduce((a, message, i, arr) => { + return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`; + }, ''); + + let blob = new Blob([chatText], { + type: 'text/plain' + }); + + saveAs(blob, `chat-${_chat.title}.txt`); + }; + + const downloadPdf = async () => { + const _chat = chat.chat; + console.log('download', chat); + + const blob = await downloadChatAsPDF(_chat); + + // Create a URL for the blob + const url = window.URL.createObjectURL(blob); + + // Create a link element to trigger the download + const a = document.createElement('a'); + a.href = url; + a.download = `chat-${_chat.title}.pdf`; + + // Append the link to the body and click it programmatically + document.body.appendChild(a); + a.click(); + + // Remove the link from the body + document.body.removeChild(a); + + // Revoke the URL to release memory + window.URL.revokeObjectURL(url); + }; </script> <Dropdown @@ -31,14 +79,14 @@ <div slot="content"> <DropdownMenu.Content - class="w-full max-w-[150px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" + class="w-full max-w-[200px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg" sideOffset={8} side="bottom" align="end" transition={flyAndScale} > <DropdownMenu.Item - class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer" + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md" on:click={async () => { await showSettings.set(!$showSettings); }} @@ -49,7 +97,7 @@ viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" - class="w-5 h-5" + class="size-4" > <path stroke-linecap="round" @@ -67,7 +115,7 @@ {#if shareEnabled} <DropdownMenu.Item - class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer" + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md" on:click={() => { shareHandler(); }} @@ -87,7 +135,59 @@ <div class="flex items-center">Share</div> </DropdownMenu.Item> - <hr class="border-gray-100 dark:border-gray-800 my-1" /> + <!-- <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer" + on:click={() => { + downloadHandler(); + }} + /> --> + <DropdownMenu.Sub> + <DropdownMenu.SubTrigger + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md" + > + <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="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" + /> + </svg> + + <div class="flex items-center">Download</div> + </DropdownMenu.SubTrigger> + <DropdownMenu.SubContent + class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg" + transition={flyAndScale} + sideOffset={8} + > + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md" + on:click={() => { + downloadTxt(); + }} + > + <div class="flex items-center line-clamp-1">Plain text (.txt)</div> + </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" + on:click={() => { + downloadPdf(); + }} + > + <div class="flex items-center line-clamp-1">PDF document (.pdf)</div> + </DropdownMenu.Item> + </DropdownMenu.SubContent> + </DropdownMenu.Sub> + + <hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" /> <div class="flex p-1"> <Tags {tags} {deleteTag} {addTag} /> diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 445dc927b..9537eed9b 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -581,7 +581,7 @@ <div class="py-2 w-full"> {#if $user.role === 'admin'} <button - class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition" + class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition" on:click={() => { goto('/admin'); showDropdown = false; @@ -607,7 +607,7 @@ </button> <button - class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition" + class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition" on:click={() => { goto('/playground'); showDropdown = false; @@ -634,7 +634,7 @@ {/if} <button - class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition" + class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition" on:click={async () => { await showSettings.set(true); showDropdown = false; @@ -669,7 +669,7 @@ <div class="py-2 w-full"> <button - class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition" + class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition" on:click={() => { localStorage.removeItem('token'); location.href = '/auth'; diff --git a/src/lib/i18n/locales/bg-BG/translation.json b/src/lib/i18n/locales/bg-BG/translation.json index 868d5ba3a..ff71b604b 100644 --- a/src/lib/i18n/locales/bg-BG/translation.json +++ b/src/lib/i18n/locales/bg-BG/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}", "Deleted {tagName}": "Изтрито {tagName}", "Description": "Описание", - "Desktop Notifications": "Десктоп Известия", + "Notifications": "Десктоп Известия", "Disabled": "Деактивиран", "Discover a modelfile": "Откриване на модфайл", "Discover a prompt": "Откриване на промпт", diff --git a/src/lib/i18n/locales/ca-ES/translation.json b/src/lib/i18n/locales/ca-ES/translation.json index 2e82372d9..9cc6f51c7 100644 --- a/src/lib/i18n/locales/ca-ES/translation.json +++ b/src/lib/i18n/locales/ca-ES/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}", "Deleted {tagName}": "Esborrat {tagName}", "Description": "Descripció", - "Desktop Notifications": "Notificacions d'Escriptori", + "Notifications": "Notificacions d'Escriptori", "Disabled": "Desactivat", "Discover a modelfile": "Descobreix un fitxer de model", "Discover a prompt": "Descobreix un prompt", diff --git a/src/lib/i18n/locales/de-DE/translation.json b/src/lib/i18n/locales/de-DE/translation.json index 23b8a45c0..940e7b59c 100644 --- a/src/lib/i18n/locales/de-DE/translation.json +++ b/src/lib/i18n/locales/de-DE/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht", "Deleted {tagName}": "{tagName} gelöscht", "Description": "Beschreibung", - "Desktop Notifications": "Desktop-Benachrichtigungen", + "Notifications": "Desktop-Benachrichtigungen", "Disabled": "Deaktiviert", "Discover a modelfile": "Eine Modelfiles entdecken", "Discover a prompt": "Einen Prompt entdecken", diff --git a/src/lib/i18n/locales/en-GB/translation.json b/src/lib/i18n/locales/en-GB/translation.json new file mode 100644 index 000000000..acc85c238 --- /dev/null +++ b/src/lib/i18n/locales/en-GB/translation.json @@ -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" +} diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index 251834e00..e11ef0131 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "", "Deleted {tagName}": "", "Description": "", - "Desktop Notifications": "", + "Notifications": "", "Disabled": "", "Discover a modelfile": "", "Discover a prompt": "", @@ -150,6 +150,7 @@ "Failed to read clipboard contents": "", "File Mode": "", "File not found.": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "", "Focus chat input": "", "Format your variables using square brackets like this:": "", "From (Base Model)": "", @@ -340,6 +341,7 @@ "URL Mode": "", "Use '#' in the prompt input to load and select your documents.": "", "Use Gravatar": "", + "Use Initials": "", "user": "", "User Permissions": "", "Users": "", diff --git a/src/lib/i18n/locales/es-ES/translation.json b/src/lib/i18n/locales/es-ES/translation.json index 1c8b68630..9e06d699e 100644 --- a/src/lib/i18n/locales/es-ES/translation.json +++ b/src/lib/i18n/locales/es-ES/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "Se borró {{deleteModelTag}}", "Deleted {tagName}": "Se borró {tagName}", "Description": "Descripción", - "Desktop Notifications": "Notificaciones", + "Notifications": "Notificaciones", "Disabled": "Desactivado", "Discover a modelfile": "Descubre un modelfile", "Discover a prompt": "Descubre un Prompt", diff --git a/src/lib/i18n/locales/fa-IR/translation.json b/src/lib/i18n/locales/fa-IR/translation.json index 5b66787c2..94e3d7f33 100644 --- a/src/lib/i18n/locales/fa-IR/translation.json +++ b/src/lib/i18n/locales/fa-IR/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "{{deleteModelTag}} پاک شد", "Deleted {tagName}": "{tagName} حذف شد", "Description": "توضیحات", - "Desktop Notifications": "اعلان", + "Notifications": "اعلان", "Disabled": "غیرفعال", "Discover a modelfile": "فایل مدل را کشف کنید", "Discover a prompt": "یک اعلان را کشف کنید", diff --git a/src/lib/i18n/locales/fr-CA/translation.json b/src/lib/i18n/locales/fr-CA/translation.json index 4d18f9ce8..c47dee07a 100644 --- a/src/lib/i18n/locales/fr-CA/translation.json +++ b/src/lib/i18n/locales/fr-CA/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", "Deleted {tagName}": "{tagName} supprimé", "Description": "Description", - "Desktop Notifications": "Notifications de bureau", + "Notifications": "Notifications de bureau", "Disabled": "Désactivé", "Discover a modelfile": "Découvrir un fichier de modèle", "Discover a prompt": "Découvrir un prompt", diff --git a/src/lib/i18n/locales/fr-FR/translation.json b/src/lib/i18n/locales/fr-FR/translation.json index b596ae3aa..6fe21b3c1 100644 --- a/src/lib/i18n/locales/fr-FR/translation.json +++ b/src/lib/i18n/locales/fr-FR/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", "Deleted {tagName}": "{tagName} supprimé", "Description": "Description", - "Desktop Notifications": "Notifications de bureau", + "Notifications": "Notifications de bureau", "Disabled": "Désactivé", "Discover a modelfile": "Découvrir un fichier de modèle", "Discover a prompt": "Découvrir un prompt", diff --git a/src/lib/i18n/locales/it-IT/translation.json b/src/lib/i18n/locales/it-IT/translation.json index 638d0255a..c565bf935 100644 --- a/src/lib/i18n/locales/it-IT/translation.json +++ b/src/lib/i18n/locales/it-IT/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "Eliminato {{deleteModelTag}}", "Deleted {tagName}": "Eliminato {tagName}", "Description": "Descrizione", - "Desktop Notifications": "Notifiche desktop", + "Notifications": "Notifiche desktop", "Disabled": "Disabilitato", "Discover a modelfile": "Scopri un file modello", "Discover a prompt": "Scopri un prompt", diff --git a/src/lib/i18n/locales/ja-JP/translation.json b/src/lib/i18n/locales/ja-JP/translation.json index 45c843c00..600e44d8e 100644 --- a/src/lib/i18n/locales/ja-JP/translation.json +++ b/src/lib/i18n/locales/ja-JP/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "{{deleteModelTag}} を削除しました", "Deleted {tagName}": "{tagName} を削除しました", "Description": "説明", - "Desktop Notifications": "デスクトップ通知", + "Notifications": "デスクトップ通知", "Disabled": "無効", "Discover a modelfile": "モデルファイルを見つける", "Discover a prompt": "プロンプトを見つける", diff --git a/src/lib/i18n/locales/ko-KR/translation.json b/src/lib/i18n/locales/ko-KR/translation.json index 87a9efe33..49c8f75fa 100644 --- a/src/lib/i18n/locales/ko-KR/translation.json +++ b/src/lib/i18n/locales/ko-KR/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨", "Deleted {tagName}": "{tagName} 삭제됨", "Description": "설명", - "Desktop Notifications": "알림", + "Notifications": "알림", "Disabled": "비활성화", "Discover a modelfile": "모델파일 검색", "Discover a prompt": "프롬프트 검색", diff --git a/src/lib/i18n/locales/languages.json b/src/lib/i18n/locales/languages.json index 691b68d28..e01a8f1f8 100644 --- a/src/lib/i18n/locales/languages.json +++ b/src/lib/i18n/locales/languages.json @@ -15,6 +15,10 @@ "code": "de-DE", "title": "Deutsch" }, + { + "code": "en-GB", + "title": "English (GB)" + }, { "code": "es-ES", "title": "Spanish" @@ -51,10 +55,18 @@ "code": "pt-PT", "title": "Portuguese (Portugal)" }, + { + "code": "pt-BR", + "title": "Portuguese (Brazil)" + }, { "code": "ru-RU", "title": "Russian (Russia)" }, + { + "code": "tr-TR", + "title": "Turkish" + }, { "code": "uk-UA", "title": "Ukrainian" diff --git a/src/lib/i18n/locales/nl-NL/translation.json b/src/lib/i18n/locales/nl-NL/translation.json index ff3194582..35f33ad0d 100644 --- a/src/lib/i18n/locales/nl-NL/translation.json +++ b/src/lib/i18n/locales/nl-NL/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "{{deleteModelTag}} is verwijderd", "Deleted {tagName}": "{tagName} is verwijderd", "Description": "Beschrijving", - "Desktop Notifications": "Desktop Notificaties", + "Notifications": "Desktop Notificaties", "Disabled": "Uitgeschakeld", "Discover a modelfile": "Ontdek een modelfile", "Discover a prompt": "Ontdek een prompt", diff --git a/src/lib/i18n/locales/pt-BR/translation.json b/src/lib/i18n/locales/pt-BR/translation.json new file mode 100644 index 000000000..9dd6b11a7 --- /dev/null +++ b/src/lib/i18n/locales/pt-BR/translation.json @@ -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", + "What’s 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." +} diff --git a/src/lib/i18n/locales/pt-PT/translation.json b/src/lib/i18n/locales/pt-PT/translation.json index 448bd86ac..80d16eb0f 100644 --- a/src/lib/i18n/locales/pt-PT/translation.json +++ b/src/lib/i18n/locales/pt-PT/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído", "Deleted {tagName}": "{tagName} excluído", "Description": "Descrição", - "Desktop Notifications": "Notificações da Área de Trabalho", + "Notifications": "Notificações da Área de Trabalho", "Disabled": "Desativado", "Discover a modelfile": "Descobrir um arquivo de modelo", "Discover a prompt": "Descobrir um prompt", diff --git a/src/lib/i18n/locales/ru-RU/translation.json b/src/lib/i18n/locales/ru-RU/translation.json index 0091bf98c..2ff0bfb9f 100644 --- a/src/lib/i18n/locales/ru-RU/translation.json +++ b/src/lib/i18n/locales/ru-RU/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "Удалено {{deleteModelTag}}", "Deleted {tagName}": "Удалено {tagName}", "Description": "Описание", - "Desktop Notifications": "Уведомления на рабочем столе", + "Notifications": "Уведомления на рабочем столе", "Disabled": "Отключено", "Discover a modelfile": "Найти файл модели", "Discover a prompt": "Найти промт", diff --git a/src/lib/i18n/locales/tr-TR/translation.json b/src/lib/i18n/locales/tr-TR/translation.json new file mode 100644 index 000000000..49b6ecc52 --- /dev/null +++ b/src/lib/i18n/locales/tr-TR/translation.json @@ -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": "Masaüstü Bildirimleri", + "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:", + "What’s 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." +} diff --git a/src/lib/i18n/locales/uk-UA/translation.json b/src/lib/i18n/locales/uk-UA/translation.json index 1f89b5801..664f0f8db 100644 --- a/src/lib/i18n/locales/uk-UA/translation.json +++ b/src/lib/i18n/locales/uk-UA/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "Видалено {{deleteModelTag}}", "Deleted {tagName}": "Видалено {tagName}", "Description": "Опис", - "Desktop Notifications": "Сповіщення", + "Notifications": "Сповіщення", "Disabled": "Вимкнено", "Discover a modelfile": "Знайти файл моделі", "Discover a prompt": "Знайти промт", diff --git a/src/lib/i18n/locales/vi-VN/translation.json b/src/lib/i18n/locales/vi-VN/translation.json index 130165756..1ef7fef8b 100644 --- a/src/lib/i18n/locales/vi-VN/translation.json +++ b/src/lib/i18n/locales/vi-VN/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "Đã xóa {{deleteModelTag}}", "Deleted {tagName}": "Đã xóa {tagName}", "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", "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", diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index 928c09694..1b2fe24de 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -100,7 +100,7 @@ "Deleted {{deleteModelTag}}": "已删除{{deleteModelTag}}", "Deleted {tagName}": "已删除{tagName}", "Description": "描述", - "Desktop Notifications": "桌面通知", + "Notifications": "桌面通知", "Disabled": "禁用", "Discover a modelfile": "探索模型文件", "Discover a prompt": "探索提示词", diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index b157e71ab..1dde0fed1 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -101,7 +101,7 @@ "Deleted {{deleteModelTag}}": "已刪除 {{deleteModelTag}}", "Deleted {tagName}": "已刪除 {tagName}", "Description": "描述", - "Desktop Notifications": "桌面通知", + "Notifications": "桌面通知", "Disabled": "已停用", "Discover a modelfile": "發現新 Modelfile", "Discover a prompt": "發現新提示詞", diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 90d5d3c38..e9a4e229b 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -31,6 +31,21 @@ export const getModels = async (token: string) => { // Helper functions ////////////////////////// +export const sanitizeResponseContent = (content: string) => { + return content + .replace(/<\|[a-z]*$/, '') + .replace(/<\|[a-z]+\|$/, '') + .replace(/<$/, '') + .replaceAll(/<\|[a-z]+\|>/g, ' ') + .replaceAll(/<br\s?\/?>/gi, '\n') + .replaceAll('<', '<') + .trim(); +}; + +export const revertSanitizedResponseContent = (content: string) => { + return content.replaceAll('<', '<'); +}; + export const capitalizeFirstLetter = (string) => { return string.charAt(0).toUpperCase() + string.slice(1); }; @@ -96,6 +111,82 @@ export const getGravatarURL = (email) => { 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) => { if (!navigator.clipboard) { const textArea = document.createElement('textarea'); diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 4e7ebfa30..1e7c03b38 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -847,6 +847,7 @@ bind:selectedModels bind:showModelSelector shareEnabled={messages.length > 0} + {chat} {initNewChat} {tags} {addTag} diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte index 79ea40c44..4b12346ea 100644 --- a/src/routes/(app)/admin/+page.svelte +++ b/src/routes/(app)/admin/+page.svelte @@ -4,6 +4,8 @@ import { goto } from '$app/navigation'; import { onMount, getContext } from 'svelte'; + import dayjs from 'dayjs'; + import { toast } from 'svelte-sonner'; import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; @@ -16,6 +18,7 @@ let loaded = false; let users = []; + let search = ''; let selectedUser = null; let showSettingsModal = false; @@ -80,157 +83,193 @@ <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} <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=" flex flex-col justify-center"> - <div class=" flex justify-between items-center"> - <div class="flex items-center text-2xl font-semibold"> - {$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> - <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" - type="button" - on:click={() => { - showSettingsModal = !showSettingsModal; - }} - > - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 16 16" - fill="currentColor" - class="w-4 h-4" + <div class=" px-5 pt-3"> + <div class=" flex justify-between items-center"> + <div class="flex items-center text-2xl font-semibold font-mona">Dashboard</div> + <div> + <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" + type="button" + on:click={() => { + showSettingsModal = !showSettingsModal; + }} > - <path - fill-rule="evenodd" - d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z" - clip-rule="evenodd" - /> - </svg> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z" + clip-rule="evenodd" + /> + </svg> - <div class=" text-xs">{$i18n.t('Admin Settings')}</div> - </button> + <div class=" text-xs">{$i18n.t('Admin Settings')}</div> + </button> + </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 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=" my-3 dark:border-gray-600" /> + <hr class=" mb-3 dark:border-gray-800" /> - <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"> - <thead - class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400" - > - <tr> - <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('Email')} </th> - <th scope="col" class="px-3 py-2"> {$i18n.t('Action')} </th> - </tr> - </thead> - <tbody> - {#each users as user} - <tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700 text-xs"> - <td class="px-3 py-2 min-w-[7rem] w-28"> - <button - class=" flex items-center gap-2 text-xs px-3 py-0.5 rounded-lg {user.role === - 'admin' && - '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 === - 'pending' && 'text-gray-600 dark:text-gray-200 bg-gray-200/30'}" - on:click={() => { - if (user.role === 'user') { - updateRoleHandler(user.id, 'admin'); - } else if (user.role === 'pending') { - updateRoleHandler(user.id, 'user'); - } else { - updateRoleHandler(user.id, 'pending'); - } - }} - > - <div - class="w-1 h-1 rounded-full {user.role === 'admin' && - 'bg-sky-600 dark:bg-sky-300'} {user.role === 'user' && - 'bg-green-600 dark:bg-green-300'} {user.role === 'pending' && - 'bg-gray-600 dark:bg-gray-300'}" - /> - {$i18n.t(user.role)}</button - > - </td> - <td class="px-3 py-2 font-medium text-gray-900 dark:text-white w-max"> - <div class="flex flex-row w-max"> - <img - class=" rounded-full w-6 h-6 object-cover mr-2.5" - src={user.profile_image_url} - alt="user" - /> + <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=" font-medium self-center">{user.name}</div> - </div> - </td> - <td class=" px-3 py-2"> {user.email} </td> + <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> - <td class="px-3 py-2"> - <div class="flex justify-start w-full"> - <button - 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 () => { - showEditUserModal = !showEditUserModal; - selectedUser = user; - }} - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="w-4 h-4" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" - /> - </svg> - </button> - - <button - 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 () => { - deleteUserHandler(user.id); - }} - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="w-4 h-4" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" - /> - </svg> - </button> - </div> - </td> + <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"> + <thead + class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400" + > + <tr> + <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('Email')} </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> - {/each} - </tbody> - </table> + </thead> + <tbody> + {#each users.filter((user) => { + if (search === '') { + return true; + } else { + let name = user.name.toLowerCase(); + const query = search.toLowerCase(); + return name.includes(query); + } + }) as user} + <tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700 text-xs"> + <td class="px-3 py-2 min-w-[7rem] w-28"> + <button + class=" flex items-center gap-2 text-xs px-3 py-0.5 rounded-lg {user.role === + 'admin' && + '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 === + 'pending' && 'text-gray-600 dark:text-gray-200 bg-gray-200/30'}" + on:click={() => { + if (user.role === 'user') { + updateRoleHandler(user.id, 'admin'); + } else if (user.role === 'pending') { + updateRoleHandler(user.id, 'user'); + } else { + updateRoleHandler(user.id, 'pending'); + } + }} + > + <div + class="w-1 h-1 rounded-full {user.role === 'admin' && + 'bg-sky-600 dark:bg-sky-300'} {user.role === 'user' && + 'bg-green-600 dark:bg-green-300'} {user.role === 'pending' && + 'bg-gray-600 dark:bg-gray-300'}" + /> + {$i18n.t(user.role)}</button + > + </td> + <td class="px-3 py-2 font-medium text-gray-900 dark:text-white w-max"> + <div class="flex flex-row w-max"> + <img + class=" rounded-full w-6 h-6 object-cover mr-2.5" + src={user.profile_image_url} + alt="user" + /> + + <div class=" font-medium self-center">{user.name}</div> + </div> + </td> + <td class=" px-3 py-2"> {user.email} </td> + + <td class=" px-3 py-2"> + {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 + 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 () => { + showEditUserModal = !showEditUserModal; + selectedUser = user; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" + /> + </svg> + </button> + + <button + 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 () => { + deleteUserHandler(user.id); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" + /> + </svg> + </button> + </div> + </td> + </tr> + {/each} + </tbody> + </table> + </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> diff --git a/src/routes/(app)/c/[id]/+page.svelte b/src/routes/(app)/c/[id]/+page.svelte index 74cf9df10..92cf2a107 100644 --- a/src/routes/(app)/c/[id]/+page.svelte +++ b/src/routes/(app)/c/[id]/+page.svelte @@ -865,6 +865,7 @@ <div class="min-h-screen max-h-screen w-full flex flex-col"> <Navbar {title} + {chat} bind:selectedModels bind:showModelSelector shareEnabled={messages.length > 0} diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index 2dc2a92b2..c7eab0db8 100644 --- a/src/routes/auth/+page.svelte +++ b/src/routes/auth/+page.svelte @@ -6,6 +6,7 @@ import { WEBUI_NAME, config, user } from '$lib/stores'; import { onMount, getContext } from 'svelte'; import { toast } from 'svelte-sonner'; + import { generateInitialsImage, canvasPixelTest } from '$lib/utils'; const i18n = getContext('i18n'); @@ -36,10 +37,12 @@ }; const signUpHandler = async () => { - const sessionUser = await userSignUp(name, email, password).catch((error) => { - toast.error(error); - return null; - }); + const sessionUser = await userSignUp(name, email, password, generateInitialsImage(name)).catch( + (error) => { + toast.error(error); + return null; + } + ); await setSessionUser(sessionUser); }; diff --git a/static/manifest.json b/static/manifest.json deleted file mode 100644 index ee019bd90..000000000 --- a/static/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "Open WebUI", - "short_name": "Open WebUI", - "start_url": "/", - "display": "standalone", - "background_color": "#343541", - "theme_color": "#343541", - "orientation": "portrait-primary", - "icons": [ - { - "src": "/favicon.png", - "type": "image/png", - "sizes": "844x884" - } - ] -}