diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..4c4cfa3b
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,32 @@
+## Pull Request Checklist
+
+- [ ] **Description:** Briefly describe the changes in this pull request.
+- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
+- [ ] **Documentation:** Have you updated relevant documentation?
+- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
+
+---
+
+## Description
+
+[Insert a brief description of the changes made in this pull request]
+
+---
+
+### Changelog Entry
+
+### Added
+
+- [List any new features or additions]
+
+### Fixed
+
+- [List any fixes or corrections]
+
+### Changed
+
+- [List any changes or updates]
+
+### Removed
+
+- [List any removed features or files]
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
new file mode 100644
index 00000000..fa3fa296
--- /dev/null
+++ b/.github/workflows/build-release.yml
@@ -0,0 +1,49 @@
+name: Release
+
+on:
+ push:
+ branches:
+ - main # or whatever branch you want to use
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Check for changes in package.json
+ run: |
+ git diff --cached --diff-filter=d package.json || {
+ echo "No changes to package.json"
+ exit 1
+ }
+
+ - name: Get version number from package.json
+ id: get_version
+ run: |
+ VERSION=$(jq -r '.version' package.json)
+ echo "::set-output name=version::$VERSION"
+
+ - name: Create GitHub release
+ uses: actions/github-script@v5
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const release = await github.rest.repos.createRelease({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ tag_name: `v${{ steps.get_version.outputs.version }}`,
+ name: `v${{ steps.get_version.outputs.version }}`,
+ body: 'Automatically created new release',
+ })
+ console.log(`Created release ${release.data.html_url}`)
+
+ - name: Upload package to GitHub release
+ uses: actions/upload-artifact@v3
+ with:
+ name: package
+ path: .
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..48a60634
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,25 @@
+# Changelog
+
+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.102] - 2024-02-22
+
+### Added
+
+- **πΌοΈ Image Generation**: Generate Images using the AUTOMATIC1111/stable-diffusion-webui API. You can set this up in Settings > Images.
+- **π Change title generation prompt**: Change the prompt used to generate titles for your chats. You can set this up in the Settings > Interface.
+- **π€ Change embedding model**: Change the embedding model used to generate embeddings for your chats in the Dockerfile. Use any sentence transformer model from huggingface.co.
+- **π’ CHANGELOG.md/Popup**: This popup will show you the latest changes.
+
+## [0.1.101] - 2024-02-22
+
+### Fixed
+
+- LaTex output formatting issue (#828)
+
+### Changed
+
+- Instead of having the previous 1.0.0-alpha.101, we switched to semantic versioning as a way to respect global conventions.
diff --git a/Dockerfile b/Dockerfile
index 03dccefe..7eb34dab 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -73,6 +73,8 @@ COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onn
# copy built frontend files
COPY --from=build /app/build /app/build
+COPY --from=build /app/CHANGELOG.md /app/CHANGELOG.md
+COPY --from=build /app/package.json /app/package.json
# copy backend files
COPY ./backend .
diff --git a/README.md b/README.md
index 421a9bd2..31df8c29 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
-ChatGPT-Style Web Interface for Ollama π¦
+User-friendly WebUI for LLMs, supported LLM runners include Ollama and OpenAI-compatible APIs.
![Open WebUI Demo](./demo.gif)
@@ -286,6 +286,8 @@ cp -RPp .env.example .env
# Building Frontend Using Node
npm i
npm run build
+# or for development (hot reload)
+# npm run dev
# or Building Frontend Using Bun
# bun install
@@ -295,6 +297,9 @@ npm run build
cd ./backend
pip install -r requirements.txt -U
sh start.sh
+# or for development (hot reload)
+# npm run build must have been run once before!
+# sh dev.sh
```
You should have Open WebUI up and running at http://localhost:8080/. Enjoy! π
diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py
index 998af3dd..39d3f96a 100644
--- a/backend/apps/images/main.py
+++ b/backend/apps/images/main.py
@@ -1,4 +1,4 @@
-import os
+import re
import requests
from fastapi import (
FastAPI,
@@ -34,6 +34,7 @@ app.add_middleware(
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.ENABLED = app.state.AUTOMATIC1111_BASE_URL != ""
+app.state.IMAGE_SIZE = "512x512"
@app.get("/enabled", response_model=bool)
@@ -74,6 +75,33 @@ async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_use
}
+class ImageSizeUpdateForm(BaseModel):
+ size: str
+
+
+@app.get("/size")
+async def get_image_size(user=Depends(get_admin_user)):
+ return {"IMAGE_SIZE": app.state.IMAGE_SIZE}
+
+
+@app.post("/size/update")
+async def update_image_size(
+ form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
+):
+ pattern = r"^\d+x\d+$" # Regular expression pattern
+ if re.match(pattern, form_data.size):
+ app.state.IMAGE_SIZE = form_data.size
+ return {
+ "IMAGE_SIZE": app.state.IMAGE_SIZE,
+ "status": True,
+ }
+ else:
+ raise HTTPException(
+ status_code=400,
+ detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
+ )
+
+
@app.get("/models")
def get_models(user=Depends(get_current_user)):
try:
@@ -140,7 +168,7 @@ def generate_image(
if form_data.model:
set_model_handler(form_data.model)
- width, height = tuple(map(int, form_data.size.split("x")))
+ width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
data = {
"prompt": form_data.prompt,
diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py
index 4176d567..83c10233 100644
--- a/backend/apps/rag/main.py
+++ b/backend/apps/rag/main.py
@@ -423,7 +423,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
] or file_ext in ["xls", "xlsx"]:
loader = UnstructuredExcelLoader(file_path)
- elif file_ext in known_source_ext or file_content_type.find("text/") >= 0:
+ elif file_ext in known_source_ext or (file_content_type and file_content_type.find("text/") >= 0):
loader = TextLoader(file_path)
else:
loader = TextLoader(file_path)
@@ -486,8 +486,8 @@ def store_doc(
@app.get("/scan")
def scan_docs_dir(user=Depends(get_admin_user)):
- try:
- for path in Path(DOCS_DIR).rglob("./**/*"):
+ for path in Path(DOCS_DIR).rglob("./**/*"):
+ try:
if path.is_file() and not path.name.startswith("."):
tags = extract_folders_after_data_docs(path)
filename = path.name
@@ -535,8 +535,8 @@ def scan_docs_dir(user=Depends(get_admin_user)):
),
)
- except Exception as e:
- print(e)
+ except Exception as e:
+ print(e)
return True
diff --git a/backend/config.py b/backend/config.py
index caf2cc45..fadae68c 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -1,11 +1,17 @@
import os
import chromadb
from chromadb import Settings
-from secrets import token_bytes
from base64 import b64encode
-from constants import ERROR_MESSAGES
+from bs4 import BeautifulSoup
+
from pathlib import Path
import json
+import markdown
+import requests
+import shutil
+
+from secrets import token_bytes
+from constants import ERROR_MESSAGES
try:
@@ -15,6 +21,8 @@ try:
except ImportError:
print("dotenv not installed, skipping...")
+WEBUI_NAME = "Open WebUI"
+shutil.copyfile("../build/favicon.png", "./static/favicon.png")
####################################
# ENV (dev,test,prod)
@@ -22,6 +30,104 @@ except ImportError:
ENV = os.environ.get("ENV", "dev")
+try:
+ with open(f"../package.json", "r") as f:
+ PACKAGE_DATA = json.load(f)
+except:
+ PACKAGE_DATA = {"version": "0.0.0"}
+
+VERSION = PACKAGE_DATA["version"]
+
+
+# Function to parse each section
+def parse_section(section):
+ items = []
+ for li in section.find_all("li"):
+ # Extract raw HTML string
+ raw_html = str(li)
+
+ # Extract text without HTML tags
+ text = li.get_text(separator=" ", strip=True)
+
+ # Split into title and content
+ parts = text.split(": ", 1)
+ title = parts[0].strip() if len(parts) > 1 else ""
+ content = parts[1].strip() if len(parts) > 1 else text
+
+ items.append({"title": title, "content": content, "raw": raw_html})
+ return items
+
+
+try:
+ with open("../CHANGELOG.md", "r") as file:
+ changelog_content = file.read()
+except:
+ changelog_content = ""
+
+# Convert markdown content to HTML
+html_content = markdown.markdown(changelog_content)
+
+# Parse the HTML content
+soup = BeautifulSoup(html_content, "html.parser")
+
+# Initialize JSON structure
+changelog_json = {}
+
+# Iterate over each version
+for version in soup.find_all("h2"):
+ version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets
+ date = version.get_text().strip().split(" - ")[1]
+
+ version_data = {"date": date}
+
+ # Find the next sibling that is a h3 tag (section title)
+ current = version.find_next_sibling()
+
+ print(current)
+
+ while current and current.name != "h2":
+ if current.name == "h3":
+ section_title = current.get_text().lower() # e.g., "added", "fixed"
+ section_items = parse_section(current.find_next_sibling("ul"))
+ version_data[section_title] = section_items
+
+ # Move to the next element
+ current = current.find_next_sibling()
+
+ changelog_json[version_number] = version_data
+
+
+CHANGELOG = changelog_json
+
+
+####################################
+# CUSTOM_NAME
+####################################
+
+CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
+if CUSTOM_NAME:
+ try:
+ r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
+ data = r.json()
+ if r.ok:
+ if "logo" in data:
+ url = (
+ f"https://api.openwebui.com{data['logo']}"
+ if data["logo"][0] == "/"
+ else data["logo"]
+ )
+
+ r = requests.get(url, stream=True)
+ if r.status_code == 200:
+ with open("./static/favicon.png", "wb") as f:
+ r.raw.decode_content = True
+ shutil.copyfileobj(r.raw, f)
+
+ WEBUI_NAME = data["name"]
+ except Exception as e:
+ print(e)
+ pass
+
####################################
# DATA/FRONTEND BUILD DIR
@@ -116,7 +222,7 @@ DEFAULT_PROMPT_SUGGESTIONS = (
)
-DEFAULT_USER_ROLE = "pending"
+DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
USER_PERMISSIONS = {"chat": {"deletion": True}}
diff --git a/backend/constants.py b/backend/constants.py
index 580db9c5..cb802c7f 100644
--- a/backend/constants.py
+++ b/backend/constants.py
@@ -44,3 +44,6 @@ class ERROR_MESSAGES(str, Enum):
MALICIOUS = "Unusual activities detected, please try again in a few minutes."
PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
+ INCORRECT_FORMAT = (
+ lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
+ )
diff --git a/backend/main.py b/backend/main.py
index 2ec741b0..b9370a18 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1,5 +1,9 @@
+from bs4 import BeautifulSoup
+import json
+import markdown
import time
+
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi import HTTPException
@@ -18,7 +22,7 @@ from apps.rag.main import app as rag_app
from apps.web.main import app as webui_app
-from config import ENV, FRONTEND_BUILD_DIR
+from config import WEBUI_NAME, ENV, VERSION, CHANGELOG, FRONTEND_BUILD_DIR
class SPAStaticFiles(StaticFiles):
@@ -69,14 +73,25 @@ app.mount("/rag/api/v1", rag_app)
@app.get("/api/config")
async def get_app_config():
+
return {
"status": True,
+ "name": WEBUI_NAME,
+ "version": VERSION,
"images": images_app.state.ENABLED,
"default_models": webui_app.state.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
}
+@app.get("/api/changelog")
+async def get_app_changelog():
+ return CHANGELOG
+
+
+app.mount("/static", StaticFiles(directory="static"), name="static")
+
+
app.mount(
"/",
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
diff --git a/backend/static/favicon.png b/backend/static/favicon.png
new file mode 100644
index 00000000..519af1db
Binary files /dev/null and b/backend/static/favicon.png differ
diff --git a/bun.lockb b/bun.lockb
index 7768741d..e0a038da 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/kubernetes/manifest/base/webui-pvc.yaml b/kubernetes/manifest/base/webui-pvc.yaml
index 285dfeef..5c75283a 100644
--- a/kubernetes/manifest/base/webui-pvc.yaml
+++ b/kubernetes/manifest/base/webui-pvc.yaml
@@ -4,7 +4,7 @@ metadata:
labels:
app: ollama-webui
name: ollama-webui-pvc
- namespace: ollama-namespace
+ namespace: open-webui
spec:
accessModes: ["ReadWriteOnce"]
resources:
diff --git a/kubernetes/manifest/kustomization.yaml b/kubernetes/manifest/kustomization.yaml
index f581839e..907bff3e 100644
--- a/kubernetes/manifest/kustomization.yaml
+++ b/kubernetes/manifest/kustomization.yaml
@@ -5,6 +5,7 @@ resources:
- base/webui-deployment.yaml
- base/webui-service.yaml
- base/webui-ingress.yaml
+- base/webui-pvc.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
diff --git a/package-lock.json b/package-lock.json
index ded65c9a..9fdfdb8a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "open-webui",
- "version": "0.0.1",
+ "version": "v1.0.0-alpha.101",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "open-webui",
- "version": "0.0.1",
+ "version": "v1.0.0-alpha.101",
"dependencies": {
"@sveltejs/adapter-node": "^1.3.1",
"async": "^3.2.5",
@@ -38,6 +38,7 @@
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5",
"svelte-check": "^3.4.3",
+ "svelte-confetti": "^1.3.2",
"tailwindcss": "^3.3.3",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
@@ -3174,6 +3175,15 @@
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0"
}
},
+ "node_modules/svelte-confetti": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-1.3.2.tgz",
+ "integrity": "sha512-R+JwFTC7hIgWVA/OuXrkj384B7CMoceb0t9VacyW6dORTQg0pWojVBB8Bo3tM30cLEQE48Fekzqgx+XSzHESMA==",
+ "dev": true,
+ "peerDependencies": {
+ "svelte": "^4.0.0"
+ }
+ },
"node_modules/svelte-eslint-parser": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz",
@@ -5852,6 +5862,13 @@
"typescript": "^5.0.3"
}
},
+ "svelte-confetti": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-1.3.2.tgz",
+ "integrity": "sha512-R+JwFTC7hIgWVA/OuXrkj384B7CMoceb0t9VacyW6dORTQg0pWojVBB8Bo3tM30cLEQE48Fekzqgx+XSzHESMA==",
+ "dev": true,
+ "requires": {}
+ },
"svelte-eslint-parser": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz",
diff --git a/package.json b/package.json
index edc4762f..30549fdd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "open-webui",
- "version": "v1.0.0-alpha.101",
+ "version": "0.1.102",
"private": true,
"scripts": {
"dev": "vite dev --host",
@@ -32,6 +32,7 @@
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5",
"svelte-check": "^3.4.3",
+ "svelte-confetti": "^1.3.2",
"tailwindcss": "^3.3.3",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
diff --git a/src/lib/apis/images/index.ts b/src/lib/apis/images/index.ts
index b25499d6..205ee90a 100644
--- a/src/lib/apis/images/index.ts
+++ b/src/lib/apis/images/index.ts
@@ -131,6 +131,73 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
return res.AUTOMATIC1111_BASE_URL;
};
+export const getImageSize = async (token: string = '') => {
+ let error = null;
+
+ const res = await fetch(`${IMAGES_API_BASE_URL}/size`, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ ...(token && { authorization: `Bearer ${token}` })
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .catch((err) => {
+ console.log(err);
+ if ('detail' in err) {
+ error = err.detail;
+ } else {
+ error = 'Server connection failed';
+ }
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res.IMAGE_SIZE;
+};
+
+export const updateImageSize = async (token: string = '', size: string) => {
+ let error = null;
+
+ const res = await fetch(`${IMAGES_API_BASE_URL}/size/update`, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ ...(token && { authorization: `Bearer ${token}` })
+ },
+ body: JSON.stringify({
+ size: size
+ })
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .catch((err) => {
+ console.log(err);
+ if ('detail' in err) {
+ error = err.detail;
+ } else {
+ error = 'Server connection failed';
+ }
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res.IMAGE_SIZE;
+};
+
export const getDiffusionModels = async (token: string = '') => {
let error = null;
diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts
index c20107ce..db29e548 100644
--- a/src/lib/apis/index.ts
+++ b/src/lib/apis/index.ts
@@ -21,3 +21,25 @@ export const getBackendConfig = async () => {
return res;
};
+
+export const getChangelog = async () => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_BASE_URL}/api/changelog`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .catch((err) => {
+ console.log(err);
+ error = err;
+ return null;
+ });
+
+ return res;
+};
diff --git a/src/lib/components/ChangelogModal.svelte b/src/lib/components/ChangelogModal.svelte
new file mode 100644
index 00000000..0cf1f006
--- /dev/null
+++ b/src/lib/components/ChangelogModal.svelte
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+ {#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
+
{message.content}-