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'} - - + {: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 @@
-
- v{WEBUI_VERSION} +
+ + v{WEBUI_VERSION} + -
+
-
{$i18n.t('Name')}
+
{$i18n.t('Name')}
-
- +
+ +

-
-
-
-
{$i18n.t('JWT Token')}
-
+
+
{$i18n.t('API keys')}
+ +
-
-
- - - + {#if showAPIKeys} +
+
+
+
{$i18n.t('JWT Token')}
- -
-
-
-
-
{$i18n.t('API Key')}
-
- -
- {#if APIKey} +
+
+
+
+
{$i18n.t('API Key')}
+
+ +
+ {#if APIKey} +
+ + + +
- + + + + + {:else} + - - {:else} - - {/if} + Create new secret key + {/if} +
-
+ {/if}
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 @@
-
{$i18n.t('Desktop Notifications')}
+
{$i18n.t('Notifications')}
-
-
{$i18n.t('or')}
- -
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 @@
{ 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; -