Browse Source

Merge pull request #838 from open-webui/image-generation

feat: stable diffusion integration
Timothy Jaeryang Baek 1 year ago
parent
commit
210122f1e2

+ 2 - 0
example.env → .env.example

@@ -5,6 +5,8 @@ OLLAMA_API_BASE_URL='http://localhost:11434/api'
 OPENAI_API_BASE_URL=''
 OPENAI_API_KEY=''
 
+# AUTOMATIC1111_BASE_URL="http://localhost:7860"
+
 # DO NOT TRACK
 SCARF_NO_ANALYTICS=true
 DO_NOT_TRACK=true

+ 1 - 1
README.md

@@ -283,7 +283,7 @@ git clone https://github.com/open-webui/open-webui.git
 cd open-webui/
 
 # Copying required .env file
-cp -RPp example.env .env
+cp -RPp .env.example .env
 
 # Building Frontend Using Node
 npm i

+ 165 - 0
backend/apps/images/main.py

@@ -0,0 +1,165 @@
+import os
+import requests
+from fastapi import (
+    FastAPI,
+    Request,
+    Depends,
+    HTTPException,
+    status,
+    UploadFile,
+    File,
+    Form,
+)
+from fastapi.middleware.cors import CORSMiddleware
+from faster_whisper import WhisperModel
+
+from constants import ERROR_MESSAGES
+from utils.utils import (
+    get_current_user,
+    get_admin_user,
+)
+from utils.misc import calculate_sha256
+from typing import Optional
+from pydantic import BaseModel
+from config import AUTOMATIC1111_BASE_URL
+
+app = FastAPI()
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
+app.state.ENABLED = app.state.AUTOMATIC1111_BASE_URL != ""
+
+
+@app.get("/enabled", response_model=bool)
+async def get_enable_status(request: Request, user=Depends(get_admin_user)):
+    return app.state.ENABLED
+
+
+@app.get("/enabled/toggle", response_model=bool)
+async def toggle_enabled(request: Request, user=Depends(get_admin_user)):
+    try:
+        r = requests.head(app.state.AUTOMATIC1111_BASE_URL)
+        app.state.ENABLED = not app.state.ENABLED
+        return app.state.ENABLED
+    except Exception as e:
+        raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e))
+
+
+class UrlUpdateForm(BaseModel):
+    url: str
+
+
+@app.get("/url")
+async def get_openai_url(user=Depends(get_admin_user)):
+    return {"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL}
+
+
+@app.post("/url/update")
+async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
+
+    if form_data.url == "":
+        app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
+    else:
+        app.state.AUTOMATIC1111_BASE_URL = form_data.url.strip("/")
+
+    return {
+        "AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
+        "status": True,
+    }
+
+
+@app.get("/models")
+def get_models(user=Depends(get_current_user)):
+    try:
+        r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models")
+        models = r.json()
+        return models
+    except Exception as e:
+        raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e))
+
+
+@app.get("/models/default")
+async def get_default_model(user=Depends(get_admin_user)):
+    try:
+        r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
+        options = r.json()
+
+        return {"model": options["sd_model_checkpoint"]}
+    except Exception as e:
+        raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e))
+
+
+class UpdateModelForm(BaseModel):
+    model: str
+
+
+def set_model_handler(model: str):
+    r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
+    options = r.json()
+
+    if model != options["sd_model_checkpoint"]:
+        options["sd_model_checkpoint"] = model
+        r = requests.post(
+            url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options
+        )
+
+    return options
+
+
+@app.post("/models/default/update")
+def update_default_model(
+    form_data: UpdateModelForm,
+    user=Depends(get_current_user),
+):
+    return set_model_handler(form_data.model)
+
+
+class GenerateImageForm(BaseModel):
+    model: Optional[str] = None
+    prompt: str
+    n: int = 1
+    size: str = "512x512"
+    negative_prompt: Optional[str] = None
+
+
+@app.post("/generations")
+def generate_image(
+    form_data: GenerateImageForm,
+    user=Depends(get_current_user),
+):
+
+    print(form_data)
+
+    try:
+        if form_data.model:
+            set_model_handler(form_data.model)
+
+        width, height = tuple(map(int, form_data.size.split("x")))
+
+        data = {
+            "prompt": form_data.prompt,
+            "batch_size": form_data.n,
+            "width": width,
+            "height": height,
+        }
+
+        if form_data.negative_prompt != None:
+            data["negative_prompt"] = form_data.negative_prompt
+
+        print(data)
+
+        r = requests.post(
+            url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
+            json=data,
+        )
+
+        return r.json()
+    except Exception as e:
+        print(e)
+        raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e))

+ 0 - 1
backend/apps/web/main.py

@@ -57,7 +57,6 @@ app.include_router(utils.router, prefix="/utils", tags=["utils"])
 async def get_status():
     return {
         "status": True,
-        "version": WEBUI_VERSION,
         "auth": WEBUI_AUTH,
         "default_models": app.state.DEFAULT_MODELS,
         "default_prompt_suggestions": app.state.DEFAULT_PROMPT_SUGGESTIONS,

+ 7 - 0
backend/config.py

@@ -185,3 +185,10 @@ Query: [query]"""
 
 WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base")
 WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
+
+
+####################################
+# Images
+####################################
+
+AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")

+ 13 - 2
backend/main.py

@@ -11,10 +11,10 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
 from apps.ollama.main import app as ollama_app
 from apps.openai.main import app as openai_app
 from apps.audio.main import app as audio_app
-
+from apps.images.main import app as images_app
+from apps.rag.main import app as rag_app
 
 from apps.web.main import app as webui_app
-from apps.rag.main import app as rag_app
 
 from config import ENV, FRONTEND_BUILD_DIR
 
@@ -58,10 +58,21 @@ app.mount("/api/v1", webui_app)
 app.mount("/ollama/api", ollama_app)
 app.mount("/openai/api", openai_app)
 
+app.mount("/images/api/v1", images_app)
 app.mount("/audio/api/v1", audio_app)
 app.mount("/rag/api/v1", rag_app)
 
 
+@app.get("/api/config")
+async def get_app_config():
+    return {
+        "status": True,
+        "images": images_app.state.ENABLED,
+        "default_models": webui_app.state.DEFAULT_MODELS,
+        "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
+    }
+
+
 app.mount(
     "/",
     SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "open-webui",
-	"version": "0.0.1",
+	"version": "v1.0.0-alpha.101",
 	"private": true,
 	"scripts": {
 		"dev": "vite dev --host",

+ 266 - 0
src/lib/apis/images/index.ts

@@ -0,0 +1,266 @@
+import { IMAGES_API_BASE_URL } from '$lib/constants';
+
+export const getImageGenerationEnabledStatus = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/enabled`, {
+		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;
+};
+
+export const toggleImageGenerationEnabledStatus = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/enabled/toggle`, {
+		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;
+};
+
+export const getAUTOMATIC1111Url = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
+		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.AUTOMATIC1111_BASE_URL;
+};
+
+export const updateAUTOMATIC1111Url = async (token: string = '', url: string) => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			url: url
+		})
+	})
+		.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.AUTOMATIC1111_BASE_URL;
+};
+
+export const getDiffusionModels = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/models`, {
+		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;
+};
+
+export const getDefaultDiffusionModel = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, {
+		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.model;
+};
+
+export const updateDefaultDiffusionModel = async (token: string = '', model: string) => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			model: model
+		})
+	})
+		.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.model;
+};
+
+export const imageGenerations = async (token: string = '', prompt: string) => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/generations`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			prompt: prompt
+		})
+	})
+		.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;
+};

+ 2 - 2
src/lib/apis/index.ts

@@ -1,9 +1,9 @@
-import { WEBUI_API_BASE_URL } from '$lib/constants';
+import { WEBUI_BASE_URL } from '$lib/constants';
 
 export const getBackendConfig = async () => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/`, {
+	const res = await fetch(`${WEBUI_BASE_URL}/api/config`, {
 		method: 'GET',
 		headers: {
 			'Content-Type': 'application/json'

+ 11 - 0
src/lib/components/chat/Messages.svelte

@@ -11,6 +11,7 @@
 	import ResponseMessage from './Messages/ResponseMessage.svelte';
 	import Placeholder from './Messages/Placeholder.svelte';
 	import Spinner from '../common/Spinner.svelte';
+	import { imageGenerations } from '$lib/apis/images';
 
 	export let chatId = '';
 	export let sendPrompt: Function;
@@ -308,6 +309,16 @@
 								{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>

+ 104 - 4
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -2,21 +2,25 @@
 	import toast from 'svelte-french-toast';
 	import dayjs from 'dayjs';
 	import { marked } from 'marked';
-	import { settings } from '$lib/stores';
 	import tippy from 'tippy.js';
 	import auto_render from 'katex/dist/contrib/auto-render.mjs';
 	import 'katex/dist/katex.min.css';
 
+	import { createEventDispatcher } from 'svelte';
 	import { onMount, tick } from 'svelte';
 
+	const dispatch = createEventDispatcher();
+
+	import { config, settings } from '$lib/stores';
+	import { synthesizeOpenAISpeech } from '$lib/apis/openai';
+	import { imageGenerations } from '$lib/apis/images';
+	import { extractSentences } from '$lib/utils';
+
 	import Name from './Name.svelte';
 	import ProfileImage from './ProfileImage.svelte';
 	import Skeleton from './Skeleton.svelte';
 	import CodeBlock from './CodeBlock.svelte';
 
-	import { synthesizeOpenAISpeech } from '$lib/apis/openai';
-	import { extractSentences } from '$lib/utils';
-
 	export let modelfiles = [];
 	export let message;
 	export let siblings;
@@ -43,6 +47,8 @@
 
 	let loadingSpeech = false;
 
+	let generatingImage = false;
+
 	$: tokens = marked.lexer(message.content);
 
 	const renderer = new marked.Renderer();
@@ -267,6 +273,23 @@
 		renderStyling();
 	};
 
+	const generateImage = async (message) => {
+		generatingImage = true;
+		const res = await imageGenerations(localStorage.token, message.content);
+		console.log(res);
+
+		if (res) {
+			message.files = res.images.map((image) => ({
+				type: 'image',
+				url: `data:image/png;base64,${image}`
+			}));
+
+			dispatch('save', message);
+		}
+
+		generatingImage = false;
+	};
+
 	onMount(async () => {
 		await tick();
 		renderStyling();
@@ -295,6 +318,18 @@
 			{#if message.content === ''}
 				<Skeleton />
 			{:else}
+				{#if message.files}
+					<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
+						{#each message.files as file}
+							<div>
+								{#if file.type === 'image'}
+									<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
+								{/if}
+							</div>
+						{/each}
+					</div>
+				{/if}
+
 				<div
 					class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
 				>
@@ -595,6 +630,71 @@
 											{/if}
 										</button>
 
+										{#if $config.images}
+											<button
+												class="{isLastMessage
+													? 'visible'
+													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
+												on:click={() => {
+													if (!generatingImage) {
+														generateImage(message);
+													}
+												}}
+											>
+												{#if generatingImage}
+													<svg
+														class=" w-4 h-4"
+														fill="currentColor"
+														viewBox="0 0 24 24"
+														xmlns="http://www.w3.org/2000/svg"
+														><style>
+															.spinner_S1WN {
+																animation: spinner_MGfb 0.8s linear infinite;
+																animation-delay: -0.8s;
+															}
+															.spinner_Km9P {
+																animation-delay: -0.65s;
+															}
+															.spinner_JApP {
+																animation-delay: -0.5s;
+															}
+															@keyframes spinner_MGfb {
+																93.75%,
+																100% {
+																	opacity: 0.2;
+																}
+															}
+														</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
+															class="spinner_S1WN spinner_Km9P"
+															cx="12"
+															cy="12"
+															r="3"
+														/><circle
+															class="spinner_S1WN spinner_JApP"
+															cx="20"
+															cy="12"
+															r="3"
+														/></svg
+													>
+												{:else}
+													<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="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
+														/>
+													</svg>
+												{/if}
+											</button>
+										{/if}
+
 										{#if message.info}
 											<button
 												class=" {isLastMessage

+ 1 - 1
src/lib/components/chat/Settings/About.svelte

@@ -18,7 +18,7 @@
 			<div class=" mb-2.5 text-sm font-medium">{WEBUI_NAME} Version</div>
 			<div class="flex w-full">
 				<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
-					{$config && $config.version ? $config.version : WEB_UI_VERSION}
+					{WEB_UI_VERSION}
 				</div>
 			</div>
 		</div>

+ 64 - 7
src/lib/components/chat/Settings/External.svelte → src/lib/components/chat/Settings/Connections.svelte

@@ -1,12 +1,17 @@
 <script lang="ts">
-	import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
 	import { models, user } from '$lib/stores';
 	import { createEventDispatcher, onMount } from 'svelte';
 	const dispatch = createEventDispatcher();
 
+	import { getOllamaAPIUrl, updateOllamaAPIUrl } from '$lib/apis/ollama';
+	import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
+	import toast from 'svelte-french-toast';
+
 	export let getModels: Function;
 
 	// External
+	let API_BASE_URL = '';
+
 	let OPENAI_API_KEY = '';
 	let OPENAI_API_BASE_URL = '';
 
@@ -17,8 +22,19 @@
 		await models.set(await getModels());
 	};
 
+	const updateOllamaAPIUrlHandler = async () => {
+		API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
+		const _models = await getModels('ollama');
+
+		if (_models.length > 0) {
+			toast.success('Server connection verified');
+			await models.set(_models);
+		}
+	};
+
 	onMount(async () => {
 		if ($user.role === 'admin') {
+			API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
 			OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
 			OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
 		}
@@ -26,7 +42,7 @@
 </script>
 
 <form
-	class="flex flex-col h-full justify-between space-y-3 text-sm"
+	class="flex flex-col h-full space-y-3 text-sm"
 	on:submit|preventDefault={() => {
 		updateOpenAIHandler();
 		dispatch('save');
@@ -37,6 +53,52 @@
 		// });
 	}}
 >
+	<div>
+		<div class=" mb-2.5 text-sm font-medium">Ollama API URL</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 URL (e.g. http://localhost:11434/api)"
+					bind:value={API_BASE_URL}
+				/>
+			</div>
+			<button
+				class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
+				on:click={() => {
+					updateOllamaAPIUrlHandler();
+				}}
+				type="button"
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
+			Trouble accessing Ollama?
+			<a
+				class=" text-gray-300 font-medium"
+				href="https://github.com/open-webui/open-webui#troubleshooting"
+				target="_blank"
+			>
+				Click here for help.
+			</a>
+		</div>
+	</div>
+
+	<hr class=" dark:border-gray-700" />
+
 	<div class=" space-y-3">
 		<div>
 			<div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div>
@@ -50,13 +112,8 @@
 					/>
 				</div>
 			</div>
-			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
-				Adds optional support for online models.
-			</div>
 		</div>
 
-		<hr class=" dark:border-gray-700" />
-
 		<div>
 			<div class=" mb-2.5 text-sm font-medium">OpenAI API Base URL</div>
 			<div class="flex w-full">

+ 216 - 148
src/lib/components/chat/Settings/General.svelte

@@ -3,31 +3,20 @@
 	import { createEventDispatcher, onMount } from 'svelte';
 	const dispatch = createEventDispatcher();
 
-	import { getOllamaAPIUrl, updateOllamaAPIUrl } from '$lib/apis/ollama';
 	import { models, user } from '$lib/stores';
 
+	import AdvancedParams from './Advanced/AdvancedParams.svelte';
+
 	export let saveSettings: Function;
 	export let getModels: Function;
 
 	// General
-	let API_BASE_URL = '';
 	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
 	let theme = 'dark';
 	let notificationEnabled = false;
 	let system = '';
 
-	const toggleTheme = async () => {
-		if (theme === 'dark') {
-			theme = 'light';
-		} else {
-			theme = 'dark';
-		}
-
-		localStorage.theme = theme;
-
-		document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark');
-		document.documentElement.classList.add(theme);
-	};
+	let showAdvanced = false;
 
 	const toggleNotification = async () => {
 		const permission = await Notification.requestPermission();
@@ -42,170 +31,233 @@
 		}
 	};
 
-	const updateOllamaAPIUrlHandler = async () => {
-		API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
-		const _models = await getModels('ollama');
+	// Advanced
+	let requestFormat = '';
+	let keepAlive = null;
 
-		if (_models.length > 0) {
-			toast.success('Server connection verified');
-			await models.set(_models);
-		}
+	let options = {
+		// Advanced
+		seed: 0,
+		temperature: '',
+		repeat_penalty: '',
+		repeat_last_n: '',
+		mirostat: '',
+		mirostat_eta: '',
+		mirostat_tau: '',
+		top_k: '',
+		top_p: '',
+		stop: '',
+		tfs_z: '',
+		num_ctx: '',
+		num_predict: ''
 	};
 
-	onMount(async () => {
-		if ($user.role === 'admin') {
-			API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
+	const toggleRequestFormat = async () => {
+		if (requestFormat === '') {
+			requestFormat = 'json';
+		} else {
+			requestFormat = '';
 		}
 
+		saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
+	};
+
+	onMount(async () => {
 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
 
 		theme = localStorage.theme ?? 'dark';
 		notificationEnabled = settings.notificationEnabled ?? false;
 		system = settings.system ?? '';
+
+		requestFormat = settings.requestFormat ?? '';
+		keepAlive = settings.keepAlive ?? null;
+
+		options.seed = settings.seed ?? 0;
+		options.temperature = settings.temperature ?? '';
+		options.repeat_penalty = settings.repeat_penalty ?? '';
+		options.top_k = settings.top_k ?? '';
+		options.top_p = settings.top_p ?? '';
+		options.num_ctx = settings.num_ctx ?? '';
+		options = { ...options, ...settings.options };
+		options.stop = (settings?.options?.stop ?? []).join(',');
 	});
 </script>
 
-<div class="flex flex-col space-y-3">
-	<div>
-		<div class=" mb-1 text-sm font-medium">WebUI Settings</div>
-
-		<div class=" py-0.5 flex w-full justify-between">
-			<div class=" self-center text-xs font-medium">Theme</div>
-			<div class="flex items-center relative">
-				<div class=" absolute right-16">
-					{#if theme === 'dark'}
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 20 20"
-							fill="currentColor"
-							class="w-4 h-4"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
-								clip-rule="evenodd"
-							/>
-						</svg>
-					{:else if theme === 'light'}
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 20 20"
-							fill="currentColor"
-							class="w-4 h-4 self-center"
-						>
-							<path
-								d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
-							/>
-						</svg>
-					{/if}
-				</div>
+<div class="flex flex-col h-full justify-between text-sm">
+	<div class="  pr-1.5 overflow-y-scroll max-h-[21rem]">
+		<div class="">
+			<div class=" mb-1 text-sm font-medium">WebUI Settings</div>
+
+			<div class=" py-0.5 flex w-full justify-between">
+				<div class=" self-center text-xs font-medium">Theme</div>
+				<div class="flex items-center relative">
+					<div class=" absolute right-16">
+						{#if theme === 'dark'}
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 20 20"
+								fill="currentColor"
+								class="w-4 h-4"
+							>
+								<path
+									fill-rule="evenodd"
+									d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
+									clip-rule="evenodd"
+								/>
+							</svg>
+						{:else if theme === 'light'}
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 20 20"
+								fill="currentColor"
+								class="w-4 h-4 self-center"
+							>
+								<path
+									d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
+								/>
+							</svg>
+						{/if}
+					</div>
 
-				<select
-					class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
-					bind:value={theme}
-					placeholder="Select a theme"
-					on:change={(e) => {
-						localStorage.theme = theme;
-
-						themes
-							.filter((e) => e !== theme)
-							.forEach((e) => {
-								e.split(' ').forEach((e) => {
-									document.documentElement.classList.remove(e);
+					<select
+						class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
+						bind:value={theme}
+						placeholder="Select a theme"
+						on:change={(e) => {
+							localStorage.theme = theme;
+
+							themes
+								.filter((e) => e !== theme)
+								.forEach((e) => {
+									e.split(' ').forEach((e) => {
+										document.documentElement.classList.remove(e);
+									});
 								});
-							});
 
-						theme.split(' ').forEach((e) => {
-							document.documentElement.classList.add(e);
-						});
+							theme.split(' ').forEach((e) => {
+								document.documentElement.classList.add(e);
+							});
 
-						console.log(theme);
-					}}
-				>
-					<option value="dark">Dark</option>
-					<option value="light">Light</option>
-					<option value="rose-pine dark">Rosé Pine</option>
-					<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
-				</select>
+							console.log(theme);
+						}}
+					>
+						<option value="dark">Dark</option>
+						<option value="light">Light</option>
+						<option value="rose-pine dark">Rosé Pine</option>
+						<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
+					</select>
+				</div>
 			</div>
-		</div>
 
-		<div>
-			<div class=" py-0.5 flex w-full justify-between">
-				<div class=" self-center text-xs font-medium">Notification</div>
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs font-medium">Notification</div>
 
-				<button
-					class="p-1 px-3 text-xs flex rounded transition"
-					on:click={() => {
-						toggleNotification();
-					}}
-					type="button"
-				>
-					{#if notificationEnabled === true}
-						<span class="ml-2 self-center">On</span>
-					{:else}
-						<span class="ml-2 self-center">Off</span>
-					{/if}
-				</button>
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							toggleNotification();
+						}}
+						type="button"
+					>
+						{#if notificationEnabled === true}
+							<span class="ml-2 self-center">On</span>
+						{:else}
+							<span class="ml-2 self-center">Off</span>
+						{/if}
+					</button>
+				</div>
 			</div>
 		</div>
-	</div>
 
-	{#if $user.role === 'admin'}
-		<hr class=" dark:border-gray-700" />
+		<hr class=" dark:border-gray-700 my-3" />
+
 		<div>
-			<div class=" mb-2.5 text-sm font-medium">Ollama API URL</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 URL (e.g. http://localhost:11434/api)"
-						bind:value={API_BASE_URL}
-					/>
-				</div>
+			<div class=" my-2.5 text-sm font-medium">System Prompt</div>
+			<textarea
+				bind:value={system}
+				class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
+				rows="4"
+			/>
+		</div>
+
+		<div class="mt-2 space-y-3 pr-1.5">
+			<div class="flex justify-between items-center text-sm">
+				<div class="  font-medium">Advanced Parameters</div>
 				<button
-					class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
+					class=" text-xs font-medium text-gray-500"
+					type="button"
 					on:click={() => {
-						updateOllamaAPIUrlHandler();
-					}}
+						showAdvanced = !showAdvanced;
+					}}>{showAdvanced ? 'Hide' : 'Show'}</button
 				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 20 20"
-						fill="currentColor"
-						class="w-4 h-4"
-					>
-						<path
-							fill-rule="evenodd"
-							d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
-							clip-rule="evenodd"
-						/>
-					</svg>
-				</button>
 			</div>
 
-			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
-				Trouble accessing Ollama?
-				<a
-					class=" text-gray-300 font-medium"
-					href="https://github.com/open-webui/open-webui#troubleshooting"
-					target="_blank"
-				>
-					Click here for help.
-				</a>
-			</div>
-		</div>
-	{/if}
+			{#if showAdvanced}
+				<AdvancedParams bind:options />
+				<hr class=" dark:border-gray-700" />
+
+				<div class=" py-1 w-full justify-between">
+					<div class="flex w-full justify-between">
+						<div class=" self-center text-xs font-medium">Keep Alive</div>
+
+						<button
+							class="p-1 px-3 text-xs flex rounded transition"
+							type="button"
+							on:click={() => {
+								keepAlive = keepAlive === null ? '5m' : null;
+							}}
+						>
+							{#if keepAlive === null}
+								<span class="ml-2 self-center"> Default </span>
+							{:else}
+								<span class="ml-2 self-center"> Custom </span>
+							{/if}
+						</button>
+					</div>
 
-	<hr class=" dark:border-gray-700" />
+					{#if keepAlive !== null}
+						<div class="flex mt-1 space-x-2">
+							<input
+								class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
+								type="text"
+								placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`}
+								bind:value={keepAlive}
+							/>
+						</div>
+					{/if}
+				</div>
+
+				<div>
+					<div class=" py-1 flex w-full justify-between">
+						<div class=" self-center text-sm font-medium">Request Mode</div>
 
-	<div>
-		<div class=" mb-2.5 text-sm font-medium">System Prompt</div>
-		<textarea
-			bind:value={system}
-			class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
-			rows="4"
-		/>
+						<button
+							class="p-1 px-3 text-xs flex rounded transition"
+							on:click={() => {
+								toggleRequestFormat();
+							}}
+						>
+							{#if requestFormat === ''}
+								<span class="ml-2 self-center"> Default </span>
+							{:else if requestFormat === 'json'}
+								<!-- <svg
+                            xmlns="http://www.w3.org/2000/svg"
+                            viewBox="0 0 20 20"
+                            fill="currentColor"
+                            class="w-4 h-4 self-center"
+                        >
+                            <path
+                                d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
+                            />
+                        </svg> -->
+								<span class="ml-2 self-center"> JSON </span>
+							{/if}
+						</button>
+					</div>
+				</div>
+			{/if}
+		</div>
 	</div>
 
 	<div class="flex justify-end pt-3 text-sm font-medium">
@@ -213,7 +265,23 @@
 			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
 			on:click={() => {
 				saveSettings({
-					system: system !== '' ? system : undefined
+					system: system !== '' ? system : undefined,
+					options: {
+						seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
+						stop: options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined,
+						temperature: options.temperature !== '' ? options.temperature : undefined,
+						repeat_penalty: options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
+						repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
+						mirostat: options.mirostat !== '' ? options.mirostat : undefined,
+						mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
+						mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
+						top_k: options.top_k !== '' ? options.top_k : undefined,
+						top_p: options.top_p !== '' ? options.top_p : undefined,
+						tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
+						num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined,
+						num_predict: options.num_predict !== '' ? options.num_predict : undefined
+					},
+					keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
 				});
 				dispatch('save');
 			}}

+ 234 - 0
src/lib/components/chat/Settings/Images.svelte

@@ -0,0 +1,234 @@
+<script lang="ts">
+	import toast from 'svelte-french-toast';
+
+	import { createEventDispatcher, onMount } from 'svelte';
+	import { config, user } from '$lib/stores';
+	import {
+		getAUTOMATIC1111Url,
+		getDefaultDiffusionModel,
+		getDiffusionModels,
+		getImageGenerationEnabledStatus,
+		toggleImageGenerationEnabledStatus,
+		updateAUTOMATIC1111Url,
+		updateDefaultDiffusionModel
+	} from '$lib/apis/images';
+	import { getBackendConfig } from '$lib/apis';
+	const dispatch = createEventDispatcher();
+
+	export let saveSettings: Function;
+
+	let loading = false;
+
+	let enableImageGeneration = true;
+	let AUTOMATIC1111_BASE_URL = '';
+
+	let selectedModel = '';
+	let models = [];
+
+	const getModels = async () => {
+		models = await getDiffusionModels(localStorage.token).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+		selectedModel = await getDefaultDiffusionModel(localStorage.token);
+	};
+
+	const updateAUTOMATIC1111UrlHandler = async () => {
+		const res = await updateAUTOMATIC1111Url(localStorage.token, AUTOMATIC1111_BASE_URL).catch(
+			(error) => {
+				toast.error(error);
+				return null;
+			}
+		);
+
+		if (res) {
+			AUTOMATIC1111_BASE_URL = res;
+
+			await getModels();
+
+			if (models) {
+				toast.success('Server connection verified');
+			}
+		} else {
+			AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
+		}
+	};
+
+	const toggleImageGeneration = async () => {
+		if (AUTOMATIC1111_BASE_URL) {
+			enableImageGeneration = await toggleImageGenerationEnabledStatus(localStorage.token).catch(
+				(error) => {
+					toast.error(error);
+					return false;
+				}
+			);
+
+			if (enableImageGeneration) {
+				config.set(await getBackendConfig(localStorage.token));
+				getModels();
+			}
+		} else {
+			enableImageGeneration = false;
+			toast.error('AUTOMATIC1111_BASE_URL not provided');
+		}
+	};
+
+	onMount(async () => {
+		if ($user.role === 'admin') {
+			enableImageGeneration = await getImageGenerationEnabledStatus(localStorage.token);
+			AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
+
+			if (enableImageGeneration && AUTOMATIC1111_BASE_URL) {
+				getModels();
+			}
+		}
+	});
+</script>
+
+<form
+	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);
+
+		dispatch('save');
+		loading = false;
+	}}
+>
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
+		<div>
+			<div class=" mb-1 text-sm font-medium">Image Settings</div>
+
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs font-medium">Image Generation (Experimental)</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							toggleImageGeneration();
+						}}
+						type="button"
+					>
+						{#if enableImageGeneration === true}
+							<span class="ml-2 self-center">On</span>
+						{:else}
+							<span class="ml-2 self-center">Off</span>
+						{/if}
+					</button>
+				</div>
+			</div>
+		</div>
+		<hr class=" dark:border-gray-700" />
+
+		<div class=" mb-2.5 text-sm font-medium">AUTOMATIC1111 Base URL</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 URL (e.g. http://127.0.0.1:7860/)"
+					bind:value={AUTOMATIC1111_BASE_URL}
+				/>
+			</div>
+			<button
+				class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
+				type="button"
+				on:click={() => {
+					// updateOllamaAPIUrlHandler();
+
+					updateAUTOMATIC1111UrlHandler();
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
+			Include `--api` flag when running stable-diffusion-webui
+			<a
+				class=" text-gray-300 font-medium"
+				href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
+				target="_blank"
+			>
+				(e.g. `sh webui.sh --api`)
+			</a>
+		</div>
+
+		{#if enableImageGeneration}
+			<hr class=" dark:border-gray-700" />
+
+			<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
+							class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+							bind:value={selectedModel}
+							placeholder="Select a model"
+						>
+							{#if !selectedModel}
+								<option value="" disabled selected>Select a model</option>
+							{/if}
+							{#each models as model}
+								<option value={model.title} class="bg-gray-100 dark:bg-gray-700"
+									>{model.model_name}</option
+								>
+							{/each}
+						</select>
+					</div>
+				</div>
+			</div>
+		{/if}
+	</div>
+
+	<div class="flex justify-end pt-3 text-sm font-medium">
+		<button
+			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded flex flex-row space-x-1 items-center {loading
+				? ' cursor-not-allowed'
+				: ''}"
+			type="submit"
+			disabled={loading}
+		>
+			Save
+
+			{#if loading}
+				<div class="ml-2 self-center">
+					<svg
+						class=" w-4 h-4"
+						viewBox="0 0 24 24"
+						fill="currentColor"
+						xmlns="http://www.w3.org/2000/svg"
+						><style>
+							.spinner_ajPY {
+								transform-origin: center;
+								animation: spinner_AtaB 0.75s infinite linear;
+							}
+							@keyframes spinner_AtaB {
+								100% {
+									transform: rotate(360deg);
+								}
+							}
+						</style><path
+							d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+							opacity=".25"
+						/><path
+							d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+							class="spinner_ajPY"
+						/></svg
+					>
+				</div>
+			{/if}
+		</button>
+	</div>
+</form>

+ 52 - 48
src/lib/components/chat/SettingsModal.svelte

@@ -7,14 +7,14 @@
 
 	import Modal from '../common/Modal.svelte';
 	import Account from './Settings/Account.svelte';
-	import Advanced from './Settings/Advanced.svelte';
 	import About from './Settings/About.svelte';
 	import Models from './Settings/Models.svelte';
 	import General from './Settings/General.svelte';
-	import External from './Settings/External.svelte';
 	import Interface from './Settings/Interface.svelte';
 	import Audio from './Settings/Audio.svelte';
 	import Chats from './Settings/Chats.svelte';
+	import Connections from './Settings/Connections.svelte';
+	import Images from './Settings/Images.svelte';
 
 	export let show = false;
 
@@ -102,79 +102,55 @@
 					<div class=" self-center">General</div>
 				</button>
 
-				<button
-					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
-					'advanced'
-						? 'bg-gray-200 dark:bg-gray-700'
-						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
-					on:click={() => {
-						selectedTab = 'advanced';
-					}}
-				>
-					<div class=" self-center mr-2">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 20 20"
-							fill="currentColor"
-							class="w-4 h-4"
-						>
-							<path
-								d="M17 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM17 15.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM3.75 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5a.75.75 0 01.75-.75zM4.5 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM10 11a.75.75 0 01.75.75v5.5a.75.75 0 01-1.5 0v-5.5A.75.75 0 0110 11zM10.75 2.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM10 6a2 2 0 100 4 2 2 0 000-4zM3.75 10a2 2 0 100 4 2 2 0 000-4zM16.25 10a2 2 0 100 4 2 2 0 000-4z"
-							/>
-						</svg>
-					</div>
-					<div class=" self-center">Advanced</div>
-				</button>
-
 				{#if $user?.role === 'admin'}
 					<button
 						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
-						'models'
+						'connections'
 							? 'bg-gray-200 dark:bg-gray-700'
 							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
 						on:click={() => {
-							selectedTab = 'models';
+							selectedTab = 'connections';
 						}}
 					>
 						<div class=" self-center mr-2">
 							<svg
 								xmlns="http://www.w3.org/2000/svg"
-								viewBox="0 0 20 20"
+								viewBox="0 0 16 16"
 								fill="currentColor"
 								class="w-4 h-4"
 							>
 								<path
-									fill-rule="evenodd"
-									d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
-									clip-rule="evenodd"
+									d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
 								/>
 							</svg>
 						</div>
-						<div class=" self-center">Models</div>
+						<div class=" self-center">Connections</div>
 					</button>
 
 					<button
 						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
-						'external'
+						'models'
 							? 'bg-gray-200 dark:bg-gray-700'
 							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
 						on:click={() => {
-							selectedTab = 'external';
+							selectedTab = 'models';
 						}}
 					>
 						<div class=" self-center mr-2">
 							<svg
 								xmlns="http://www.w3.org/2000/svg"
-								viewBox="0 0 16 16"
+								viewBox="0 0 20 20"
 								fill="currentColor"
 								class="w-4 h-4"
 							>
 								<path
-									d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
+									fill-rule="evenodd"
+									d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
+									clip-rule="evenodd"
 								/>
 							</svg>
 						</div>
-						<div class=" self-center">External</div>
+						<div class=" self-center">Models</div>
 					</button>
 				{/if}
 
@@ -196,7 +172,7 @@
 						>
 							<path
 								fill-rule="evenodd"
-								d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+								d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z"
 								clip-rule="evenodd"
 							/>
 						</svg>
@@ -231,6 +207,34 @@
 					<div class=" self-center">Audio</div>
 				</button>
 
+				{#if $user.role === 'admin'}
+					<button
+						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
+						'images'
+							? 'bg-gray-200 dark:bg-gray-700'
+							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
+						on:click={() => {
+							selectedTab = 'images';
+						}}
+					>
+						<div class=" self-center mr-2">
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 16 16"
+								fill="currentColor"
+								class="w-4 h-4"
+							>
+								<path
+									fill-rule="evenodd"
+									d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+									clip-rule="evenodd"
+								/>
+							</svg>
+						</div>
+						<div class=" self-center">Images</div>
+					</button>
+				{/if}
+
 				<button
 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 					'chats'
@@ -318,17 +322,10 @@
 							show = false;
 						}}
 					/>
-				{:else if selectedTab === 'advanced'}
-					<Advanced
-						on:save={() => {
-							show = false;
-						}}
-						{saveSettings}
-					/>
 				{:else if selectedTab === 'models'}
 					<Models {getModels} />
-				{:else if selectedTab === 'external'}
-					<External
+				{:else if selectedTab === 'connections'}
+					<Connections
 						{getModels}
 						on:save={() => {
 							show = false;
@@ -348,6 +345,13 @@
 							show = false;
 						}}
 					/>
+				{:else if selectedTab === 'images'}
+					<Images
+						{saveSettings}
+						on:save={() => {
+							show = false;
+						}}
+					/>
 				{:else if selectedTab === 'chats'}
 					<Chats {saveSettings} />
 				{:else if selectedTab === 'account'}

+ 1 - 1
src/lib/components/common/Modal.svelte

@@ -13,7 +13,7 @@
 		} else if (size === 'sm') {
 			return 'w-[30rem]';
 		} else {
-			return 'w-[42rem]';
+			return 'w-[44rem]';
 		}
 	};
 

+ 4 - 2
src/lib/constants.ts

@@ -1,4 +1,5 @@
 import { dev } from '$app/environment';
+// import { version } from '../../package.json';
 
 export const WEBUI_NAME = 'Open WebUI';
 export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
@@ -6,10 +7,11 @@ export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
 export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
 export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`;
 export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
-export const RAG_API_BASE_URL = `${WEBUI_BASE_URL}/rag/api/v1`;
 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 = 'v1.0.0-alpha-static';
+export const WEB_UI_VERSION = APP_VERSION;
 
 export const REQUIRED_OLLAMA_VERSION = '0.1.16';
 

+ 4 - 1
vite.config.ts

@@ -2,5 +2,8 @@ import { sveltekit } from '@sveltejs/kit/vite';
 import { defineConfig } from 'vite';
 
 export default defineConfig({
-	plugins: [sveltekit()]
+	plugins: [sveltekit()],
+	define: {
+		APP_VERSION: JSON.stringify(process.env.npm_package_version)
+	}
 });