Explorar o código

Merge branch 'main' into litellm

Timothy Jaeryang Baek hai 1 ano
pai
achega
ee22e641ff
Modificáronse 39 ficheiros con 888 adicións e 167 borrados
  1. 32 0
      .github/pull_request_template.md
  2. 49 0
      .github/workflows/build-release.yml
  3. 25 0
      CHANGELOG.md
  4. 2 0
      Dockerfile
  5. 6 1
      README.md
  6. 30 2
      backend/apps/images/main.py
  7. 5 5
      backend/apps/rag/main.py
  8. 109 3
      backend/config.py
  9. 3 0
      backend/constants.py
  10. 16 1
      backend/main.py
  11. BIN=BIN
      backend/static/favicon.png
  12. BIN=BIN
      bun.lockb
  13. 1 1
      kubernetes/manifest/base/webui-pvc.yaml
  14. 1 0
      kubernetes/manifest/kustomization.yaml
  15. 19 2
      package-lock.json
  16. 2 1
      package.json
  17. 67 0
      src/lib/apis/images/index.ts
  18. 22 0
      src/lib/apis/index.ts
  19. 115 0
      src/lib/components/ChangelogModal.svelte
  20. 119 87
      src/lib/components/chat/Messages.svelte
  21. 5 2
      src/lib/components/chat/Messages/Placeholder.svelte
  22. 6 3
      src/lib/components/chat/Messages/ResponseMessage.svelte
  23. 38 7
      src/lib/components/chat/Messages/UserMessage.svelte
  24. 20 5
      src/lib/components/chat/Settings/About.svelte
  25. 26 5
      src/lib/components/chat/Settings/Images.svelte
  26. 4 4
      src/lib/components/chat/Settings/Models.svelte
  27. 18 0
      src/lib/components/common/Image.svelte
  28. 62 0
      src/lib/components/common/ImagePreview.svelte
  29. 22 4
      src/lib/components/common/Modal.svelte
  30. 3 4
      src/lib/components/layout/Navbar.svelte
  31. 8 1
      src/lib/components/layout/Sidebar.svelte
  32. 2 3
      src/lib/constants.ts
  33. 3 0
      src/lib/stores/index.ts
  34. 15 8
      src/routes/(app)/+layout.svelte
  35. 15 3
      src/routes/(app)/+page.svelte
  36. 5 4
      src/routes/(app)/c/[id]/+page.svelte
  37. 6 3
      src/routes/+layout.svelte
  38. 5 5
      src/routes/auth/+page.svelte
  39. 2 3
      src/routes/error/+page.svelte

+ 32 - 0
.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]

+ 49 - 0
.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 }}

+ 25 - 0
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.

+ 2 - 0
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 .

+ 6 - 1
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! 😄

+ 30 - 2
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,

+ 5 - 5
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
 

+ 109 - 3
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}}
 
 

+ 3 - 0
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 ''}"
+    )

+ 16 - 1
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),

BIN=BIN
backend/static/favicon.png


BIN=BIN
bun.lockb


+ 1 - 1
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:

+ 1 - 0
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

+ 19 - 2
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",

+ 2 - 1
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",

+ 67 - 0
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;
 

+ 22 - 0
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;
+};

+ 115 - 0
src/lib/components/ChangelogModal.svelte

@@ -0,0 +1,115 @@
+<script lang="ts">
+	import { onMount } from 'svelte';
+	import { Confetti } from 'svelte-confetti';
+
+	import { WEBUI_NAME, config } from '$lib/stores';
+
+	import { WEBUI_VERSION } from '$lib/constants';
+	import { getChangelog } from '$lib/apis';
+
+	import Modal from './common/Modal.svelte';
+
+	export let show = false;
+
+	let changelog = null;
+
+	onMount(async () => {
+		const res = await getChangelog();
+		changelog = res;
+	});
+</script>
+
+<Modal bind:show>
+	<div class="px-5 py-4 dark:text-gray-300">
+		<div class="flex justify-between items-start">
+			<div class="text-xl font-bold">
+				What’s New in {$WEBUI_NAME}
+				<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
+			</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+					/>
+				</svg>
+			</button>
+		</div>
+		<div class="flex items-center mt-1">
+			<div class="text-sm dark:text-gray-200">Release Notes</div>
+			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
+			<div class="text-sm dark:text-gray-200">
+				v{WEBUI_VERSION}
+			</div>
+		</div>
+	</div>
+
+	<hr class=" dark:border-gray-800" />
+
+	<div class=" w-full p-4 px-5">
+		<div class=" overflow-y-scroll max-h-80">
+			<div class="mb-3">
+				{#if changelog}
+					{#each Object.keys(changelog) as version}
+						<div class=" mb-3 pr-2">
+							<div class="font-bold text-xl mb-1 dark:text-white">
+								v{version} - {changelog[version].date}
+							</div>
+
+							<hr class=" dark:border-gray-800 my-2" />
+
+							{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
+								<div class="">
+									<div
+										class="font-bold uppercase text-xs {section === 'added'
+											? 'text-white bg-blue-600'
+											: section === 'fixed'
+											? 'text-white bg-green-600'
+											: section === 'changed'
+											? 'text-white bg-yellow-600'
+											: section === 'removed'
+											? 'text-white bg-red-600'
+											: ''}  w-fit px-3 rounded-full my-2.5"
+									>
+										{section}
+									</div>
+
+									<div class="my-2.5 px-1.5">
+										{#each Object.keys(changelog[version][section]) as item}
+											<div class="text-sm mb-2">
+												<div class="font-semibold uppercase">
+													{changelog[version][section][item].title}
+												</div>
+												<div class="mb-2 mt-1">{changelog[version][section][item].content}</div>
+											</div>
+										{/each}
+									</div>
+								</div>
+							{/each}
+						</div>
+					{/each}
+				{/if}
+			</div>
+		</div>
+		<div class="flex justify-end pt-3 text-sm font-medium">
+			<button
+				on:click={() => {
+					localStorage.version = $config.version;
+					show = false;
+				}}
+				class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
+			>
+				<span class="relative">Okay, Let's Go!</span>
+			</button>
+		</div>
+	</div>
+</Modal>

+ 119 - 87
src/lib/components/chat/Messages.svelte

@@ -222,6 +222,34 @@
 			scrollToBottom();
 		}, 100);
 	};
+
+	// TODO: change delete behaviour
+	// const deleteMessageAndDescendants = async (messageId: string) => {
+	// 	if (history.messages[messageId]) {
+	// 		history.messages[messageId].deleted = true;
+
+	// 		for (const childId of history.messages[messageId].childrenIds) {
+	// 			await deleteMessageAndDescendants(childId);
+	// 		}
+	// 	}
+	// };
+
+	// const triggerDeleteMessageRecursive = async (messageId: string) => {
+	// 	await deleteMessageAndDescendants(messageId);
+	// 	await updateChatById(localStorage.token, chatId, { history });
+	// 	await chats.set(await getChatList(localStorage.token));
+	// };
+
+	const messageDeleteHandler = async (messageId) => {
+		if (history.messages[messageId]) {
+			history.messages[messageId].deleted = true;
+
+			for (const childId of history.messages[messageId].childrenIds) {
+				history.messages[childId].deleted = true;
+			}
+		}
+		await updateChatById(localStorage.token, chatId, { history });
+	};
 </script>
 
 {#if messages.length == 0}
@@ -230,99 +258,103 @@
 	<div class=" pb-10">
 		{#key chatId}
 			{#each messages as message, messageIdx}
-				<div class=" w-full">
-					<div
-						class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
-							? 'max-w-full'
-							: 'max-w-3xl'} mx-auto rounded-lg group"
-					>
-						{#if message.role === 'user'}
-							<UserMessage
-								user={$user}
-								{message}
-								siblings={message.parentId !== null
-									? history.messages[message.parentId]?.childrenIds ?? []
-									: Object.values(history.messages)
-											.filter((message) => message.parentId === null)
-											.map((message) => message.id) ?? []}
-								{confirmEditMessage}
-								{showPreviousMessage}
-								{showNextMessage}
-								{copyToClipboard}
-							/>
-
-							{#if messages.length - 1 === messageIdx && processing !== ''}
-								<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
-									<div class=" dark:text-blue-100">
-										<svg
-											class=" w-4 h-4 translate-y-[0.5px]"
-											fill="currentColor"
-											viewBox="0 0 24 24"
-											xmlns="http://www.w3.org/2000/svg"
-											><style>
-												.spinner_qM83 {
-													animation: spinner_8HQG 1.05s infinite;
-												}
-												.spinner_oXPr {
-													animation-delay: 0.1s;
-												}
-												.spinner_ZTLf {
-													animation-delay: 0.2s;
-												}
-												@keyframes spinner_8HQG {
-													0%,
-													57.14% {
-														animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
-														transform: translate(0);
+				{#if !message.deleted}
+					<div class=" w-full">
+						<div
+							class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
+								? 'max-w-full'
+								: 'max-w-3xl'} mx-auto rounded-lg group"
+						>
+							{#if message.role === 'user'}
+								<UserMessage
+									on:delete={() => messageDeleteHandler(message.id)}
+									user={$user}
+									{message}
+									isFirstMessage={messageIdx === 0}
+									siblings={message.parentId !== null
+										? history.messages[message.parentId]?.childrenIds ?? []
+										: Object.values(history.messages)
+												.filter((message) => message.parentId === null)
+												.map((message) => message.id) ?? []}
+									{confirmEditMessage}
+									{showPreviousMessage}
+									{showNextMessage}
+									{copyToClipboard}
+								/>
+
+								{#if messages.length - 1 === messageIdx && processing !== ''}
+									<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
+										<div class=" dark:text-blue-100">
+											<svg
+												class=" w-4 h-4 translate-y-[0.5px]"
+												fill="currentColor"
+												viewBox="0 0 24 24"
+												xmlns="http://www.w3.org/2000/svg"
+												><style>
+													.spinner_qM83 {
+														animation: spinner_8HQG 1.05s infinite;
 													}
-													28.57% {
-														animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
-														transform: translateY(-6px);
+													.spinner_oXPr {
+														animation-delay: 0.1s;
 													}
-													100% {
-														transform: translate(0);
+													.spinner_ZTLf {
+														animation-delay: 0.2s;
 													}
-												}
-											</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
-												class="spinner_qM83 spinner_oXPr"
-												cx="12"
-												cy="12"
-												r="2.5"
-											/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
-										>
-									</div>
-									<div class=" text-sm font-medium">
-										{processing}
+													@keyframes spinner_8HQG {
+														0%,
+														57.14% {
+															animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
+															transform: translate(0);
+														}
+														28.57% {
+															animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
+															transform: translateY(-6px);
+														}
+														100% {
+															transform: translate(0);
+														}
+													}
+												</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
+													class="spinner_qM83 spinner_oXPr"
+													cx="12"
+													cy="12"
+													r="2.5"
+												/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
+											>
+										</div>
+										<div class=" text-sm font-medium">
+											{processing}
+										</div>
 									</div>
-								</div>
+								{/if}
+							{:else}
+								<ResponseMessage
+									{message}
+									modelfiles={selectedModelfiles}
+									siblings={history.messages[message.parentId]?.childrenIds ?? []}
+									isLastMessage={messageIdx + 1 === messages.length}
+									{confirmEditResponseMessage}
+									{showPreviousMessage}
+									{showNextMessage}
+									{rateMessage}
+									{copyToClipboard}
+									{continueGeneration}
+									{regenerateResponse}
+									on:save={async (e) => {
+										console.log('save', e);
+
+										const message = e.detail;
+										history.messages[message.id] = message;
+										await updateChatById(localStorage.token, chatId, {
+											messages: messages,
+											history: history
+										});
+									}}
+								/>
 							{/if}
-						{:else}
-							<ResponseMessage
-								{message}
-								modelfiles={selectedModelfiles}
-								siblings={history.messages[message.parentId]?.childrenIds ?? []}
-								isLastMessage={messageIdx + 1 === messages.length}
-								{confirmEditResponseMessage}
-								{showPreviousMessage}
-								{showNextMessage}
-								{rateMessage}
-								{copyToClipboard}
-								{continueGeneration}
-								{regenerateResponse}
-								on:save={async (e) => {
-									console.log('save', e);
-
-									const message = e.detail;
-									history.messages[message.id] = message;
-									await updateChatById(localStorage.token, chatId, {
-										messages: messages,
-										history: history
-									});
-								}}
-							/>
-						{/if}
+						</div>
 					</div>
-				</div>
+				{/if}
 			{/each}
 
 			{#if bottomPadding}

+ 5 - 2
src/lib/components/chat/Messages/Placeholder.svelte

@@ -1,4 +1,5 @@
 <script lang="ts">
+	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { onMount } from 'svelte';
 
 	export let models = [];
@@ -27,14 +28,16 @@
 					>
 						{#if model in modelfiles}
 							<img
-								src={modelfiles[model]?.imageUrl ?? './favicon.png'}
+								src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
 								alt="modelfile"
 								class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
 								draggable="false"
 							/>
 						{:else}
 							<img
-								src={models.length === 1 ? '/favicon.png' : '/favicon.png'}
+								src={models.length === 1
+									? `${WEBUI_BASE_URL}/static/favicon.png`
+									: `${WEBUI_BASE_URL}/static/favicon.png`}
 								class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
 								alt="logo"
 								draggable="false"

+ 6 - 3
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -20,6 +20,8 @@
 	import ProfileImage from './ProfileImage.svelte';
 	import Skeleton from './Skeleton.svelte';
 	import CodeBlock from './CodeBlock.svelte';
+	import Image from '$lib/components/common/Image.svelte';
+	import { WEBUI_BASE_URL } from '$lib/constants';
 
 	export let modelfiles = [];
 	export let message;
@@ -46,7 +48,6 @@
 	let speakingIdx = null;
 
 	let loadingSpeech = false;
-
 	let generatingImage = false;
 
 	$: tokens = marked.lexer(message.content);
@@ -298,7 +299,9 @@
 
 {#key message.id}
 	<div class=" flex w-full message-{message.id}">
-		<ProfileImage src={modelfiles[message.model]?.imageUrl ?? '/favicon.png'} />
+		<ProfileImage
+			src={modelfiles[message.model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
+		/>
 
 		<div class="w-full overflow-hidden">
 			<Name>
@@ -323,7 +326,7 @@
 						{#each message.files as file}
 							<div>
 								{#if file.type === 'image'}
-									<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
+									<Image src={file.url} />
 								{/if}
 							</div>
 						{/each}

+ 38 - 7
src/lib/components/chat/Messages/UserMessage.svelte

@@ -1,14 +1,17 @@
 <script lang="ts">
 	import dayjs from 'dayjs';
 
-	import { tick } from 'svelte';
+	import { tick, createEventDispatcher } from 'svelte';
 	import Name from './Name.svelte';
 	import ProfileImage from './ProfileImage.svelte';
 	import { modelfiles, settings } from '$lib/stores';
 
+	const dispatch = createEventDispatcher();
+
 	export let user;
 	export let message;
 	export let siblings;
+	export let isFirstMessage: boolean;
 
 	export let confirmEditMessage: Function;
 	export let showPreviousMessage: Function;
@@ -42,6 +45,10 @@
 		edit = false;
 		editedContent = '';
 	};
+
+	const deleteMessageHandler = async () => {
+		dispatch('delete', message.id);
+	};
 </script>
 
 <div class=" flex w-full">
@@ -189,11 +196,11 @@
 				<div class="w-full">
 					<pre id="user-message">{message.content}</pre>
 
-					<div class=" flex justify-start space-x-1">
+					<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
 						{#if siblings.length > 1}
 							<div class="flex self-center">
 								<button
-									class="self-center"
+									class="self-center dark:hover:text-white hover:text-black transition"
 									on:click={() => {
 										showPreviousMessage(message);
 									}}
@@ -212,12 +219,12 @@
 									</svg>
 								</button>
 
-								<div class="text-xs font-bold self-center">
+								<div class="text-xs font-bold self-center dark:text-gray-100">
 									{siblings.indexOf(message.id) + 1} / {siblings.length}
 								</div>
 
 								<button
-									class="self-center"
+									class="self-center dark:hover:text-white hover:text-black transition"
 									on:click={() => {
 										showNextMessage(message);
 									}}
@@ -239,7 +246,7 @@
 						{/if}
 
 						<button
-							class="invisible group-hover:visible p-1 rounded dark:hover:text-white transition edit-user-message-button"
+							class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
 							on:click={() => {
 								editMessageHandler();
 							}}
@@ -261,7 +268,7 @@
 						</button>
 
 						<button
-							class="invisible group-hover:visible p-1 rounded dark:hover:text-white transition"
+							class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
 							on:click={() => {
 								copyToClipboard(message.content);
 							}}
@@ -281,6 +288,30 @@
 								/>
 							</svg>
 						</button>
+
+						{#if !isFirstMessage}
+							<button
+								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
+								on:click={() => {
+									deleteMessageHandler();
+								}}
+							>
+								<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>
+						{/if}
 					</div>
 				</div>
 			{/if}

+ 20 - 5
src/lib/components/chat/Settings/About.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { getOllamaVersion } from '$lib/apis/ollama';
-	import { WEBUI_NAME, WEB_UI_VERSION } from '$lib/constants';
-	import { config } from '$lib/stores';
+	import { WEBUI_VERSION } from '$lib/constants';
+	import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
 	import { onMount } from 'svelte';
 
 	let ollamaVersion = '';
@@ -15,10 +15,25 @@
 <div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
 	<div class=" space-y-3">
 		<div>
-			<div class=" mb-2.5 text-sm font-medium">{WEBUI_NAME} Version</div>
+			<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
+				<div>
+					{$WEBUI_NAME} Version
+				</div>
+			</div>
 			<div class="flex w-full">
-				<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
-					{WEB_UI_VERSION}
+				<div class="flex-1 text-xs text-gray-700 dark:text-gray-200 flex space-x-1.5 items-center">
+					<div>
+						v{WEBUI_VERSION}
+					</div>
+
+					<button
+						class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
+						on:click={() => {
+							showChangelog.set(true);
+						}}
+					>
+						<div>See what's new</div>
+					</button>
 				</div>
 			</div>
 		</div>

+ 26 - 5
src/lib/components/chat/Settings/Images.svelte

@@ -8,9 +8,11 @@
 		getDefaultDiffusionModel,
 		getDiffusionModels,
 		getImageGenerationEnabledStatus,
+		getImageSize,
 		toggleImageGenerationEnabledStatus,
 		updateAUTOMATIC1111Url,
-		updateDefaultDiffusionModel
+		updateDefaultDiffusionModel,
+		updateImageSize
 	} from '$lib/apis/images';
 	import { getBackendConfig } from '$lib/apis';
 	const dispatch = createEventDispatcher();
@@ -25,6 +27,8 @@
 	let selectedModel = '';
 	let models = [];
 
+	let imageSize = '';
+
 	const getModels = async () => {
 		models = await getDiffusionModels(localStorage.token).catch((error) => {
 			toast.error(error);
@@ -53,7 +57,6 @@
 			AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
 		}
 	};
-
 	const toggleImageGeneration = async () => {
 		if (AUTOMATIC1111_BASE_URL) {
 			enableImageGeneration = await toggleImageGenerationEnabledStatus(localStorage.token).catch(
@@ -79,6 +82,7 @@
 			AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
 
 			if (enableImageGeneration && AUTOMATIC1111_BASE_URL) {
+				imageSize = await getImageSize(localStorage.token);
 				getModels();
 			}
 		}
@@ -89,13 +93,17 @@
 	class="flex flex-col h-full justify-between space-y-3 text-sm"
 	on:submit|preventDefault={async () => {
 		loading = true;
-		const res = await updateDefaultDiffusionModel(localStorage.token, selectedModel);
+		await updateDefaultDiffusionModel(localStorage.token, selectedModel);
+		await updateImageSize(localStorage.token, imageSize).catch((error) => {
+			toast.error(error);
+			return null;
+		});
 
 		dispatch('save');
 		loading = false;
 	}}
 >
-	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[21rem]">
 		<div>
 			<div class=" mb-1 text-sm font-medium">Image Settings</div>
 
@@ -169,7 +177,7 @@
 			<hr class=" dark:border-gray-700" />
 
 			<div>
-				<div class=" mb-2.5 text-sm font-medium">Set default model</div>
+				<div class=" mb-2.5 text-sm font-medium">Set Default Model</div>
 				<div class="flex w-full">
 					<div class="flex-1 mr-2">
 						<select
@@ -189,6 +197,19 @@
 					</div>
 				</div>
 			</div>
+
+			<div>
+				<div class=" mb-2.5 text-sm font-medium">Set Image Size</div>
+				<div class="flex w-full">
+					<div class="flex-1 mr-2">
+						<input
+							class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+							placeholder="Enter Image Size (e.g. 512x512)"
+							bind:value={imageSize}
+						/>
+					</div>
+				</div>
+			</div>
 		{/if}
 	</div>
 

+ 4 - 4
src/lib/components/chat/Settings/Models.svelte

@@ -3,8 +3,8 @@
 	import toast from 'svelte-french-toast';
 
 	import { createModel, deleteModel, pullModel } from '$lib/apis/ollama';
-	import { WEBUI_API_BASE_URL, WEBUI_NAME } from '$lib/constants';
-	import { models, user } from '$lib/stores';
+	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
+	import { WEBUI_NAME, models, user } from '$lib/stores';
 	import { splitStream } from '$lib/utils';
 
 	export let getModels: Function;
@@ -59,9 +59,9 @@
 				} else {
 					toast.success(`Model '${modelName}' has been successfully downloaded.`);
 
-					const notification = new Notification(WEBUI_NAME, {
+					const notification = new Notification($WEBUI_NAME, {
 						body: `Model '${modelName}' has been successfully downloaded.`,
-						icon: '/favicon.png'
+						icon: `${WEBUI_BASE_URL}/static/favicon.png`
 					});
 
 					models.set(await getModels());

+ 18 - 0
src/lib/components/common/Image.svelte

@@ -0,0 +1,18 @@
+<script lang="ts">
+	import ImagePreview from './ImagePreview.svelte';
+
+	export let src = '';
+	export let alt = '';
+
+	let showImagePreview = false;
+</script>
+
+<ImagePreview bind:show={showImagePreview} {src} {alt} />
+<button
+	on:click={() => {
+		console.log('image preview');
+		showImagePreview = true;
+	}}
+>
+	<img {src} {alt} class=" max-h-96 rounded-lg" draggable="false" />
+</button>

+ 62 - 0
src/lib/components/common/ImagePreview.svelte

@@ -0,0 +1,62 @@
+<script lang="ts">
+	export let show = false;
+	export let src = '';
+	export let alt = '';
+</script>
+
+{#if show}
+	<!-- svelte-ignore a11y-click-events-have-key-events -->
+	<!-- svelte-ignore a11y-no-static-element-interactions -->
+	<div
+		class="fixed top-0 right-0 left-0 bottom-0 bg-black text-white w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
+	>
+		<div class=" absolute left-0 w-full flex justify-between">
+			<div>
+				<button
+					class=" p-5"
+					on:click={() => {
+						show = false;
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="2"
+						stroke="currentColor"
+						class="w-6 h-6"
+					>
+						<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
+					</svg>
+				</button>
+			</div>
+
+			<div>
+				<button
+					class=" p-5"
+					on:click={() => {
+						const a = document.createElement('a');
+						a.href = src;
+						a.download = 'Image.png';
+						a.click();
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 20 20"
+						fill="currentColor"
+						class="w-6 h-6"
+					>
+						<path
+							d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z"
+						/>
+						<path
+							d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
+						/>
+					</svg>
+				</button>
+			</div>
+		</div>
+		<img {src} {alt} class=" mx-auto h-full object-scale-down" />
+	</div>
+{/if}

+ 22 - 4
src/lib/components/common/Modal.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 	import { onMount } from 'svelte';
-	import { fade, blur } from 'svelte/transition';
+	import { fade } from 'svelte/transition';
 
 	export let show = true;
 	export let size = 'md';
@@ -34,16 +34,17 @@
 	<!-- svelte-ignore a11y-click-events-have-key-events -->
 	<!-- svelte-ignore a11y-no-static-element-interactions -->
 	<div
-		class="fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
+		class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
+		in:fade={{ duration: 10 }}
 		on:click={() => {
 			show = false;
 		}}
 	>
 		<div
-			class="m-auto rounded-xl max-w-full {sizeToWidth(
+			class=" modal-content m-auto rounded-xl max-w-full {sizeToWidth(
 				size
 			)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
-			transition:fade={{ delay: 100, duration: 200 }}
+			in:fade={{ duration: 10 }}
 			on:click={(e) => {
 				e.stopPropagation();
 			}}
@@ -52,3 +53,20 @@
 		</div>
 	</div>
 {/if}
+
+<style>
+	.modal-content {
+		animation: scaleUp 0.1s ease-out forwards;
+	}
+
+	@keyframes scaleUp {
+		from {
+			transform: scale(0.985);
+			opacity: 0;
+		}
+		to {
+			transform: scale(1);
+			opacity: 1;
+		}
+	}
+</style>

+ 3 - 4
src/lib/components/layout/Navbar.svelte

@@ -4,14 +4,13 @@
 	const { saveAs } = fileSaver;
 
 	import { getChatById } from '$lib/apis/chats';
-	import { chatId, modelfiles, settings } from '$lib/stores';
+	import { WEBUI_NAME, chatId, modelfiles, settings } from '$lib/stores';
 	import ShareChatModal from '../chat/ShareChatModal.svelte';
 	import TagInput from '../common/Tags/TagInput.svelte';
 	import Tags from '../common/Tags.svelte';
-	import { WEBUI_NAME } from '$lib/constants';
 
 	export let initNewChat: Function;
-	export let title: string = WEBUI_NAME;
+	export let title: string = $WEBUI_NAME;
 	export let shareEnabled: boolean = false;
 
 	export let tags = [];
@@ -102,7 +101,7 @@
 			</div>
 			<div class=" flex-1 self-center font-medium line-clamp-1">
 				<div>
-					{title != '' ? title : WEBUI_NAME}
+					{title != '' ? title : $WEBUI_NAME}
 				</div>
 			</div>
 

+ 8 - 1
src/lib/components/layout/Sidebar.svelte

@@ -16,6 +16,8 @@
 		updateChatById
 	} from '$lib/apis/chats';
 	import toast from 'svelte-french-toast';
+	import { slide } from 'svelte/transition';
+	import { WEBUI_BASE_URL } from '$lib/constants';
 
 	let show = false;
 	let navElement;
@@ -113,7 +115,11 @@
 			>
 				<div class="flex self-center">
 					<div class="self-center mr-1.5">
-						<img src="/favicon.png" class=" w-7 -translate-x-1.5 rounded-full" alt="logo" />
+						<img
+							src="{WEBUI_BASE_URL}/static/favicon.png"
+							class=" w-7 -translate-x-1.5 rounded-full"
+							alt="logo"
+						/>
 					</div>
 
 					<div class=" self-center font-medium text-sm">New Chat</div>
@@ -562,6 +568,7 @@
 						<div
 							id="dropdownDots"
 							class="absolute z-40 bottom-[70px] 4.5rem rounded-lg shadow w-[240px] bg-gray-900"
+							in:slide={{ duration: 150 }}
 						>
 							<div class="py-2 w-full">
 								{#if $user.role === 'admin'}

+ 2 - 3
src/lib/constants.ts

@@ -1,7 +1,7 @@
 import { dev } from '$app/environment';
 // import { version } from '../../package.json';
 
-export const WEBUI_NAME = 'Open WebUI';
+export const APP_NAME = 'Open WebUI';
 export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
 
 export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
@@ -13,8 +13,7 @@ export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`;
 export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`;
 export const RAG_API_BASE_URL = `${WEBUI_BASE_URL}/rag/api/v1`;
 
-export const WEB_UI_VERSION = APP_VERSION;
-
+export const WEBUI_VERSION = APP_VERSION;
 export const REQUIRED_OLLAMA_VERSION = '0.1.16';
 
 export const SUPPORTED_FILE_TYPE = [

+ 3 - 0
src/lib/stores/index.ts

@@ -1,6 +1,8 @@
+import { APP_NAME } from '$lib/constants';
 import { writable } from 'svelte/store';
 
 // Backend
+export const WEBUI_NAME = writable(APP_NAME);
 export const config = writable(undefined);
 export const user = writable(undefined);
 
@@ -32,3 +34,4 @@ export const documents = writable([
 
 export const settings = writable({});
 export const showSettings = writable(false);
+export const showChangelog = writable(false);

+ 15 - 8
src/routes/(app)/+layout.svelte

@@ -1,18 +1,19 @@
 <script lang="ts">
 	import toast from 'svelte-french-toast';
 	import { openDB, deleteDB } from 'idb';
-	import { onMount, tick } from 'svelte';
-	import { goto } from '$app/navigation';
-
 	import fileSaver from 'file-saver';
 	const { saveAs } = fileSaver;
 
+	import { onMount, tick } from 'svelte';
+	import { goto } from '$app/navigation';
+
 	import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
 	import { getModelfiles } from '$lib/apis/modelfiles';
 	import { getPrompts } from '$lib/apis/prompts';
-
 	import { getOpenAIModels } from '$lib/apis/openai';
 	import { getLiteLLMModels } from '$lib/apis/litellm';
+	import { getDocs } from '$lib/apis/documents';
+	import { getAllChatTags } from '$lib/apis/chats';
 
 	import {
 		user,
@@ -22,16 +23,17 @@
 		modelfiles,
 		prompts,
 		documents,
-		tags
+		tags,
+		showChangelog,
+		config
 	} from '$lib/stores';
 	import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
+	import { checkVersion } from '$lib/utils';
 
 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
 	import Sidebar from '$lib/components/layout/Sidebar.svelte';
-	import { checkVersion } from '$lib/utils';
 	import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
-	import { getDocs } from '$lib/apis/documents';
-	import { getAllChatTags } from '$lib/apis/chats';
+	import ChangelogModal from '$lib/components/ChangelogModal.svelte';
 
 	let ollamaVersion = '';
 	let loaded = false;
@@ -190,6 +192,10 @@
 				}
 			});
 
+			if ($user.role === 'admin') {
+				showChangelog.set(localStorage.version !== $config.version);
+			}
+
 			await tick();
 		}
 
@@ -363,6 +369,7 @@
 		>
 			<Sidebar />
 			<SettingsModal bind:show={$showSettings} />
+			<ChangelogModal bind:show={$showChangelog} />
 			<slot />
 		</div>
 	</div>

+ 15 - 3
src/routes/(app)/+page.svelte

@@ -37,6 +37,7 @@
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import { RAGTemplate } from '$lib/utils/rag';
 	import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
+	import { WEBUI_BASE_URL } from '$lib/constants';
 
 	let stopResponseFlag = false;
 	let autoScroll = true;
@@ -335,7 +336,7 @@
 						content: $settings.system
 				  }
 				: undefined,
-			...messages
+			...messages.filter((message) => !message.deleted)
 		]
 			.filter((message) => message)
 			.map((message, idx, arr) => ({
@@ -453,7 +454,7 @@
 												: `${model}`,
 											{
 												body: responseMessage.content,
-												icon: selectedModelfile?.imageUrl ?? '/favicon.png'
+												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
 											}
 										);
 									}
@@ -538,6 +539,17 @@
 				stream: true,
 				messages: [
 					$settings.system
+					? {
+							role: 'system',
+							content: $settings.system
+					  }
+					: undefined,
+				...messages.filter((message) => !message.deleted)
+			]
+				.filter((message) => message)
+				.map((message, idx, arr) => ({
+					role: message.role,
+					...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false
 						? {
 								role: 'system',
 								content: $settings.system
@@ -627,7 +639,7 @@
 				if ($settings.notificationEnabled && !document.hasFocus()) {
 					const notification = new Notification(`OpenAI ${model}`, {
 						body: responseMessage.content,
-						icon: '/favicon.png'
+						icon: `${WEBUI_BASE_URL}/static/favicon.png`
 					});
 				}
 

+ 5 - 4
src/routes/(app)/c/[id]/+page.svelte

@@ -37,6 +37,7 @@
 	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import { RAGTemplate } from '$lib/utils/rag';
+	import { WEBUI_BASE_URL } from '$lib/constants';
 
 	let loaded = false;
 
@@ -348,7 +349,7 @@
 						content: $settings.system
 				  }
 				: undefined,
-			...messages
+			...messages.filter((message) => !message.deleted)
 		]
 			.filter((message) => message)
 			.map((message, idx, arr) => ({
@@ -466,7 +467,7 @@
 												: `${model}`,
 											{
 												body: responseMessage.content,
-												icon: selectedModelfile?.imageUrl ?? '/favicon.png'
+												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
 											}
 										);
 									}
@@ -555,7 +556,7 @@
 							content: $settings.system
 					  }
 					: undefined,
-				...messages
+				...messages.filter((message) => !message.deleted)
 			]
 				.filter((message) => message)
 				.map((message, idx, arr) => ({
@@ -637,7 +638,7 @@
 				if ($settings.notificationEnabled && !document.hasFocus()) {
 					const notification = new Notification(`OpenAI ${model}`, {
 						body: responseMessage.content,
-						icon: '/favicon.png'
+						icon: `${WEBUI_BASE_URL}/static/favicon.png`
 					});
 				}
 

+ 6 - 3
src/routes/+layout.svelte

@@ -1,6 +1,6 @@
 <script>
 	import { onMount, tick } from 'svelte';
-	import { config, user, theme } from '$lib/stores';
+	import { config, user, theme, WEBUI_NAME } from '$lib/stores';
 	import { goto } from '$app/navigation';
 	import toast, { Toaster } from 'svelte-french-toast';
 
@@ -10,7 +10,7 @@
 	import '../app.css';
 	import '../tailwind.css';
 	import 'tippy.js/dist/tippy.css';
-	import { WEBUI_NAME } from '$lib/constants';
+	import { WEBUI_BASE_URL } from '$lib/constants';
 
 	let loaded = false;
 
@@ -22,6 +22,8 @@
 		if (backendConfig) {
 			// Save Backend Status to Store
 			await config.set(backendConfig);
+
+			await WEBUI_NAME.set(backendConfig.name);
 			console.log(backendConfig);
 
 			if ($config) {
@@ -55,7 +57,8 @@
 </script>
 
 <svelte:head>
-	<title>{WEBUI_NAME}</title>
+	<title>{$WEBUI_NAME}</title>
+	<link rel="icon" href="{WEBUI_BASE_URL}/static/favicon.png" />
 
 	<link rel="stylesheet" type="text/css" href="/themes/rosepine.css" />
 	<link rel="stylesheet" type="text/css" href="/themes/rosepine-dawn.css" />

+ 5 - 5
src/routes/auth/+page.svelte

@@ -1,8 +1,8 @@
 <script>
 	import { goto } from '$app/navigation';
 	import { userSignIn, userSignUp } from '$lib/apis/auths';
-	import { WEBUI_API_BASE_URL, WEBUI_NAME } from '$lib/constants';
-	import { config, user } from '$lib/stores';
+	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
+	import { WEBUI_NAME, config, user } from '$lib/stores';
 	import { onMount } from 'svelte';
 	import toast from 'svelte-french-toast';
 
@@ -61,7 +61,7 @@
 	<div class="fixed m-10 z-50">
 		<div class="flex space-x-2">
 			<div class=" self-center">
-				<img src="/favicon.png" class=" w-8 rounded-full" alt="logo" />
+				<img src="{WEBUI_BASE_URL}/static/favicon.png" class=" w-8 rounded-full" alt="logo" />
 			</div>
 		</div>
 	</div>
@@ -90,12 +90,12 @@
 					}}
 				>
 					<div class=" text-xl md:text-2xl font-bold">
-						{mode === 'signin' ? 'Sign in' : 'Sign up'} to {WEBUI_NAME}
+						{mode === 'signin' ? 'Sign in' : 'Sign up'} to {$WEBUI_NAME}
 					</div>
 
 					{#if mode === 'signup'}
 						<div class=" mt-1 text-xs font-medium text-gray-500">
-							ⓘ {WEBUI_NAME} does not make any external connections, and your data stays securely on
+							ⓘ {$WEBUI_NAME} does not make any external connections, and your data stays securely on
 							your locally hosted server.
 						</div>
 					{/if}

+ 2 - 3
src/routes/error/+page.svelte

@@ -1,7 +1,6 @@
 <script>
 	import { goto } from '$app/navigation';
-	import { WEBUI_NAME } from '$lib/constants';
-	import { config } from '$lib/stores';
+	import { WEBUI_NAME, config } from '$lib/stores';
 	import { onMount } from 'svelte';
 
 	let loaded = false;
@@ -20,7 +19,7 @@
 		<div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center">
 			<div class="m-auto pb-44 flex flex-col justify-center">
 				<div class="max-w-md">
-					<div class="text-center text-2xl font-medium z-50">{WEBUI_NAME} Backend Required</div>
+					<div class="text-center text-2xl font-medium z-50">{$WEBUI_NAME} Backend Required</div>
 
 					<div class=" mt-4 text-center text-sm w-full">
 						Oops! You're using an unsupported method (frontend only). Please serve the WebUI from