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'}
-
-