Timothy J. Baek 8 місяців тому
батько
коміт
8b5aed7a2b

+ 10 - 8
backend/apps/images/main.py

@@ -37,6 +37,7 @@ from config import (
     AUTOMATIC1111_API_AUTH,
     COMFYUI_BASE_URL,
     COMFYUI_WORKFLOW,
+    COMFYUI_WORKFLOW_NODES,
     IMAGES_OPENAI_API_BASE_URL,
     IMAGES_OPENAI_API_KEY,
     IMAGE_GENERATION_MODEL,
@@ -75,6 +76,7 @@ app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
 app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
 app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
 app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
+app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
 
 app.state.config.IMAGE_SIZE = IMAGE_SIZE
 app.state.config.IMAGE_STEPS = IMAGE_STEPS
@@ -211,9 +213,9 @@ def get_image_model():
 
 
 class ImageConfigForm(BaseModel):
-    model: str
-    size: str
-    steps: int
+    MODEL: str
+    IMAGE_SIZE: str
+    IMAGE_STEPS: int
 
 
 @app.get("/image/config")
@@ -227,19 +229,19 @@ async def get_image_config(user=Depends(get_admin_user)):
 
 @app.post("/image/config/update")
 async def update_image_config(form_data: ImageConfigForm, user=Depends(get_admin_user)):
-    app.state.config.MODEL = form_data.model
+    app.state.config.MODEL = form_data.MODEL
 
     pattern = r"^\d+x\d+$"
-    if re.match(pattern, form_data.size):
-        app.state.config.IMAGE_SIZE = form_data.size
+    if re.match(pattern, form_data.IMAGE_SIZE):
+        app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
     else:
         raise HTTPException(
             status_code=400,
             detail=ERROR_MESSAGES.INCORRECT_FORMAT("  (e.g., 512x512)."),
         )
 
-    if form_data.steps >= 0:
-        app.state.config.IMAGE_STEPS = form_data.steps
+    if form_data.IMAGE_STEPS >= 0:
+        app.state.config.IMAGE_STEPS = form_data.IMAGE_STEPS
     else:
         raise HTTPException(
             status_code=400,

+ 8 - 282
src/lib/apis/images/index.ts

@@ -1,6 +1,6 @@
 import { IMAGES_API_BASE_URL } from '$lib/constants';
 
-export const getImageGenerationConfig = async (token: string = '') => {
+export const getConfig = async (token: string = '') => {
 	let error = null;
 
 	const res = await fetch(`${IMAGES_API_BASE_URL}/config`, {
@@ -32,11 +32,7 @@ export const getImageGenerationConfig = async (token: string = '') => {
 	return res;
 };
 
-export const updateImageGenerationConfig = async (
-	token: string = '',
-	engine: string,
-	enabled: boolean
-) => {
+export const updateConfig = async (token: string = '', config: object) => {
 	let error = null;
 
 	const res = await fetch(`${IMAGES_API_BASE_URL}/config/update`, {
@@ -47,76 +43,7 @@ export const updateImageGenerationConfig = async (
 			...(token && { authorization: `Bearer ${token}` })
 		},
 		body: JSON.stringify({
-			engine,
-			enabled
-		})
-	})
-		.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 getOpenAIConfig = async (token: string = '') => {
-	let error = null;
-
-	const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config`, {
-		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 updateOpenAIConfig = async (token: string = '', url: string, key: string) => {
-	let error = null;
-
-	const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config/update`, {
-		method: 'POST',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			...(token && { authorization: `Bearer ${token}` })
-		},
-		body: JSON.stringify({
-			url: url,
-			key: key
+			...config
 		})
 	})
 		.then(async (res) => {
@@ -140,10 +67,10 @@ export const updateOpenAIConfig = async (token: string = '', url: string, key: s
 	return res;
 };
 
-export const getImageGenerationEngineUrls = async (token: string = '') => {
+export const getImageGenerationConfig = async (token: string = '') => {
 	let error = null;
 
-	const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
+	const res = await fetch(`${IMAGES_API_BASE_URL}/image/config`, {
 		method: 'GET',
 		headers: {
 			Accept: 'application/json',
@@ -172,19 +99,17 @@ export const getImageGenerationEngineUrls = async (token: string = '') => {
 	return res;
 };
 
-export const updateImageGenerationEngineUrls = async (token: string = '', urls: object = {}) => {
+export const updateImageGenerationConfig = async (token: string = '', config: object) => {
 	let error = null;
 
-	const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
+	const res = await fetch(`${IMAGES_API_BASE_URL}/image/config/update`, {
 		method: 'POST',
 		headers: {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
 			...(token && { authorization: `Bearer ${token}` })
 		},
-		body: JSON.stringify({
-			...urls
-		})
+		body: JSON.stringify({ ...config })
 	})
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();
@@ -207,138 +132,6 @@ export const updateImageGenerationEngineUrls = async (token: string = '', urls:
 	return res;
 };
 
-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 getImageSteps = async (token: string = '') => {
-	let error = null;
-
-	const res = await fetch(`${IMAGES_API_BASE_URL}/steps`, {
-		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_STEPS;
-};
-
-export const updateImageSteps = async (token: string = '', steps: number) => {
-	let error = null;
-
-	const res = await fetch(`${IMAGES_API_BASE_URL}/steps/update`, {
-		method: 'POST',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			...(token && { authorization: `Bearer ${token}` })
-		},
-		body: JSON.stringify({ steps })
-	})
-		.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_STEPS;
-};
-
 export const getImageGenerationModels = async (token: string = '') => {
 	let error = null;
 
@@ -371,73 +164,6 @@ export const getImageGenerationModels = async (token: string = '') => {
 	return res;
 };
 
-export const getDefaultImageGenerationModel = 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 updateDefaultImageGenerationModel = 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;
 

+ 303 - 313
src/lib/components/admin/Settings/Images.svelte

@@ -2,23 +2,16 @@
 	import { toast } from 'svelte-sonner';
 
 	import { createEventDispatcher, onMount, getContext } from 'svelte';
-	import { config, user } from '$lib/stores';
+	import { config as backendConfig, user } from '$lib/stores';
+
+	import { getBackendConfig } from '$lib/apis';
 	import {
 		getImageGenerationModels,
-		getDefaultImageGenerationModel,
-		updateDefaultImageGenerationModel,
-		getImageSize,
 		getImageGenerationConfig,
 		updateImageGenerationConfig,
-		getImageGenerationEngineUrls,
-		updateImageGenerationEngineUrls,
-		updateImageSize,
-		getImageSteps,
-		updateImageSteps,
-		getOpenAIConfig,
-		updateOpenAIConfig
+		getConfig,
+		updateConfig
 	} from '$lib/apis/images';
-	import { getBackendConfig } from '$lib/apis';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	const dispatch = createEventDispatcher();
@@ -27,127 +20,77 @@
 
 	let loading = false;
 
-	let imageGenerationEngine = 'openai';
-	let enableImageGeneration = false;
-
-	let AUTOMATIC1111_BASE_URL = '';
-	let AUTOMATIC1111_API_AUTH = '';
-	let COMFYUI_BASE_URL = '';
+	let config = null;
+	let imageGenerationConfig = null;
 
-	let OPENAI_API_BASE_URL = '';
-	let OPENAI_API_KEY = '';
-
-	let selectedModel = '';
 	let models = null;
 
-	let imageSize = '';
-	let steps = 50;
+	let inputFiles = null;
 
 	const getModels = async () => {
 		models = await getImageGenerationModels(localStorage.token).catch((error) => {
 			toast.error(error);
 			return null;
 		});
-		selectedModel = await getDefaultImageGenerationModel(localStorage.token).catch((error) => {
-			return '';
-		});
 	};
 
-	const updateUrlHandler = async () => {
-		if (imageGenerationEngine === 'comfyui') {
-			const res = await updateImageGenerationEngineUrls(localStorage.token, {
-				COMFYUI_BASE_URL: COMFYUI_BASE_URL
-			}).catch((error) => {
-				toast.error(error);
-
-				console.log(error);
-				return null;
-			});
-
-			if (res) {
-				COMFYUI_BASE_URL = res.COMFYUI_BASE_URL;
-
-				await getModels();
-
-				if (models) {
-					toast.success($i18n.t('Server connection verified'));
-				}
-			} else {
-				({ COMFYUI_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
-			}
-		} else {
-			const res = await updateImageGenerationEngineUrls(localStorage.token, {
-				AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL,
-				AUTOMATIC1111_API_AUTH: AUTOMATIC1111_API_AUTH
-			}).catch((error) => {
-				toast.error(error);
-				return null;
-			});
-
-			if (res) {
-				AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL;
-				AUTOMATIC1111_API_AUTH = res.AUTOMATIC1111_API_AUTH;
-
-				await getModels();
-
-				if (models) {
-					toast.success($i18n.t('Server connection verified'));
-				}
-			} else {
-				({ AUTOMATIC1111_BASE_URL, AUTOMATIC1111_API_AUTH } = await getImageGenerationEngineUrls(
-					localStorage.token
-				));
-			}
-		}
-	};
-	const updateImageGeneration = async () => {
-		const res = await updateImageGenerationConfig(
-			localStorage.token,
-			imageGenerationEngine,
-			enableImageGeneration
-		).catch((error) => {
+	const updateConfigHandler = async () => {
+		const res = await updateConfig(localStorage.token, config).catch((error) => {
 			toast.error(error);
 			return null;
 		});
 
 		if (res) {
-			imageGenerationEngine = res.engine;
-			enableImageGeneration = res.enabled;
+			config = res;
 		}
 
-		if (enableImageGeneration) {
-			config.set(await getBackendConfig(localStorage.token));
+		if (config.enabled) {
+			backendConfig.set(await getBackendConfig());
 			getModels();
 		}
 	};
 
+	const saveHandler = async () => {
+		loading = true;
+
+		await updateConfig(localStorage.token, config).catch((error) => {
+			toast.error(error);
+			loading = false;
+			return null;
+		});
+
+		await updateImageGenerationConfig(localStorage.token, imageGenerationConfig).catch((error) => {
+			toast.error(error);
+			loading = false;
+			return null;
+		});
+
+		dispatch('save');
+		loading = false;
+	};
+
 	onMount(async () => {
 		if ($user.role === 'admin') {
-			const res = await getImageGenerationConfig(localStorage.token).catch((error) => {
+			const res = await getConfig(localStorage.token).catch((error) => {
 				toast.error(error);
 				return null;
 			});
 
 			if (res) {
-				imageGenerationEngine = res.engine ?? 'automatic1111';
-				enableImageGeneration = res.enabled;
+				config = res;
 			}
-			const URLS = await getImageGenerationEngineUrls(localStorage.token);
-
-			AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
-			AUTOMATIC1111_API_AUTH = URLS.AUTOMATIC1111_API_AUTH;
-			COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
 
-			const config = await getOpenAIConfig(localStorage.token);
-
-			OPENAI_API_KEY = config.OPENAI_API_KEY;
-			OPENAI_API_BASE_URL = config.OPENAI_API_BASE_URL;
+			if (config.enabled) {
+				getModels();
+			}
 
-			imageSize = await getImageSize(localStorage.token);
-			steps = await getImageSteps(localStorage.token);
+			const imageConfigRes = await getImageGenerationConfig(localStorage.token).catch((error) => {
+				toast.error(error);
+				return null;
+			});
 
-			if (enableImageGeneration) {
-				getModels();
+			if (imageConfigRes) {
+				imageGenerationConfig = imageConfigRes;
 			}
 		}
 	});
@@ -156,205 +99,267 @@
 <form
 	class="flex flex-col h-full justify-between space-y-3 text-sm"
 	on:submit|preventDefault={async () => {
-		loading = true;
-
-		if (imageGenerationEngine === 'openai') {
-			await updateOpenAIConfig(localStorage.token, OPENAI_API_BASE_URL, OPENAI_API_KEY);
-		}
-
-		await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
-
-		await updateImageSize(localStorage.token, imageSize).catch((error) => {
-			toast.error(error);
-			return null;
-		});
-		await updateImageSteps(localStorage.token, steps).catch((error) => {
-			toast.error(error);
-			return null;
-		});
-
-		dispatch('save');
-		loading = false;
+		saveHandler();
 	}}
 >
-	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden">
-		<div>
-			<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
-
+	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden pr-2">
+		{#if config && imageGenerationConfig}
 			<div>
-				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">
-						{$i18n.t('Image Generation (Experimental)')}
-					</div>
-
-					<div class="px-1">
-						<Switch
-							bind:state={enableImageGeneration}
-							on:change={(e) => {
-								const enabled = e.detail;
-
-								if (enabled) {
-									if (imageGenerationEngine === 'automatic1111' && AUTOMATIC1111_BASE_URL === '') {
-										toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
-										enableImageGeneration = false;
-									} else if (imageGenerationEngine === 'comfyui' && COMFYUI_BASE_URL === '') {
-										toast.error($i18n.t('ComfyUI Base URL is required.'));
-										enableImageGeneration = false;
-									} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
-										toast.error($i18n.t('OpenAI API Key is required.'));
-										enableImageGeneration = false;
+				<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
+
+				<div>
+					<div class=" py-0.5 flex w-full justify-between">
+						<div class=" self-center text-xs font-medium">
+							{$i18n.t('Image Generation (Experimental)')}
+						</div>
+
+						<div class="px-1">
+							<Switch
+								bind:state={config.enabled}
+								on:change={(e) => {
+									const enabled = e.detail;
+
+									if (enabled) {
+										if (
+											config.engine === 'automatic1111' &&
+											config.automatic1111.AUTOMATIC1111_BASE_URL === ''
+										) {
+											toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
+											config.enabled = false;
+										} else if (
+											config.engine === 'comfyui' &&
+											config.comfyui.COMFYUI_BASE_URL === ''
+										) {
+											toast.error($i18n.t('ComfyUI Base URL is required.'));
+											config.enabled = false;
+										} else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
+											toast.error($i18n.t('OpenAI API Key is required.'));
+											config.enabled = false;
+										}
 									}
-								}
 
-								updateImageGeneration();
-							}}
-						/>
+									updateConfigHandler();
+								}}
+							/>
+						</div>
 					</div>
 				</div>
-			</div>
 
-			<div class=" py-0.5 flex w-full justify-between">
-				<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
-				<div class="flex items-center relative">
-					<select
-						class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
-						bind:value={imageGenerationEngine}
-						placeholder={$i18n.t('Select a mode')}
-						on:change={async () => {
-							await updateImageGeneration();
-						}}
-					>
-						<option value="openai">{$i18n.t('Default (Open AI)')}</option>
-						<option value="comfyui">{$i18n.t('ComfyUI')}</option>
-						<option value="automatic1111">{$i18n.t('Automatic1111')}</option>
-					</select>
-				</div>
-			</div>
-		</div>
-		<hr class=" dark:border-gray-850" />
-
-		{#if imageGenerationEngine === 'automatic1111'}
-			<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
-			<div class="flex w-full">
-				<div class="flex-1 mr-2">
-					<input
-						class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-						placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
-						bind:value={AUTOMATIC1111_BASE_URL}
-					/>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
+					<div class="flex items-center relative">
+						<select
+							class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+							bind:value={config.engine}
+							placeholder={$i18n.t('Select Engine')}
+							on:change={async () => {
+								updateConfigHandler();
+							}}
+						>
+							<option value="openai">{$i18n.t('Default (Open AI)')}</option>
+							<option value="comfyui">{$i18n.t('ComfyUI')}</option>
+							<option value="automatic1111">{$i18n.t('Automatic1111')}</option>
+						</select>
+					</div>
 				</div>
-				<button
-					class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
-					type="button"
-					on:click={() => {
-						updateUrlHandler();
-					}}
-				>
-					<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>
+			<hr class=" dark:border-gray-850" />
 
-			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
-				{$i18n.t('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"
-				>
-					{$i18n.t('(e.g. `sh webui.sh --api`)')}
-				</a>
-			</div>
+			<div class="flex flex-col gap-2">
+				{#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
+					<div>
+						<div class=" mb-2 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
+						<div class="flex w-full">
+							<div class="flex-1 mr-2">
+								<input
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
+									bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
+								/>
+							</div>
+							<button
+								class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+								type="button"
+								on:click={() => {
+									updateConfigHandler();
+								}}
+							>
+								<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">
+							{$i18n.t('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"
+							>
+								{$i18n.t('(e.g. `sh webui.sh --api`)')}
+							</a>
+						</div>
+					</div>
 
-			<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Api Auth String')}</div>
-			<SensitiveInput
-				placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
-				bind:value={AUTOMATIC1111_API_AUTH}
-				required={false}
-			/>
-
-			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
-				{$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
-				<a
-					class=" text-gray-300 font-medium"
-					href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
-					target="_blank"
-				>
-					{$i18n.t('(e.g. `sh webui.sh --api --api-auth username_password`)').replace('_', ':')}
-				</a>
-			</div>
-		{:else if imageGenerationEngine === 'comfyui'}
-			<div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
-			<div class="flex w-full">
-				<div class="flex-1 mr-2">
-					<input
-						class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-						placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
-						bind:value={COMFYUI_BASE_URL}
-					/>
-				</div>
-				<button
-					class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
-					type="button"
-					on:click={() => {
-						updateUrlHandler();
-					}}
-				>
-					<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"
+					<div>
+						<div class=" mb-2 text-sm font-medium">
+							{$i18n.t('AUTOMATIC1111 Api Auth String')}
+						</div>
+						<SensitiveInput
+							placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
+							bind:value={config.automatic1111.AUTOMATIC1111_API_AUTH}
+							required={false}
 						/>
-					</svg>
-				</button>
-			</div>
-		{:else if imageGenerationEngine === 'openai'}
-			<div>
-				<div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
 
-				<div class="flex gap-2 mb-1">
-					<input
-						class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-						placeholder={$i18n.t('API Base URL')}
-						bind:value={OPENAI_API_BASE_URL}
-						required
-					/>
+						<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
+							{$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
+							<a
+								class=" text-gray-300 font-medium"
+								href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
+								target="_blank"
+							>
+								{$i18n
+									.t('(e.g. `sh webui.sh --api --api-auth username_password`)')
+									.replace('_', ':')}
+							</a>
+						</div>
+					</div>
+				{:else if config?.engine === 'comfyui'}
+					<div class="">
+						<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
+						<div class="flex w-full">
+							<div class="flex-1 mr-2">
+								<input
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
+									bind:value={config.comfyui.COMFYUI_BASE_URL}
+								/>
+							</div>
+							<button
+								class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+								type="button"
+								on:click={() => {
+									updateConfigHandler();
+								}}
+							>
+								<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>
 
-					<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OPENAI_API_KEY} />
-				</div>
+					<div class="">
+						<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
+
+						{#if config.comfyui.COMFYUI_WORKFLOW}
+							<textarea
+								class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none disabled:text-gray-600 resize-none"
+								rows="10"
+								value={JSON.stringify(JSON.parse(config.comfyui.COMFYUI_WORKFLOW), null, 2)}
+								disabled
+							/>
+						{/if}
+
+						<div class="flex w-full">
+							<div class="flex-1">
+								<input
+									id="upload-comfyui-workflow-input"
+									hidden
+									type="file"
+									accept=".json"
+									on:change={(e) => {
+										const file = e.target.files[0];
+										const reader = new FileReader();
+
+										reader.onload = (e) => {
+											config.comfyui.COMFYUI_WORKFLOW = e.target.result;
+											updateConfigHandler();
+
+											e.target.value = null;
+										};
+
+										reader.readAsText(file);
+									}}
+								/>
+
+								<button
+									class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
+									type="button"
+									on:click={() => {
+										document.getElementById('upload-comfyui-workflow-input')?.click();
+									}}
+								>
+									{$i18n.t('Click here to upload a workflow.json file.')}
+								</button>
+							</div>
+						</div>
+
+						<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
+							{$i18n.t('Make sure to export a workflow.json file as API format from ComfyUI.')}
+						</div>
+					</div>
+
+					{#if config.comfyui.COMFYUI_WORKFLOW}
+						<div class="">
+							<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow Nodes')}</div>
+
+							<div>hi</div>
+						</div>
+					{/if}
+				{:else if config?.engine === 'openai'}
+					<div>
+						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
+
+						<div class="flex gap-2 mb-1">
+							<input
+								class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								placeholder={$i18n.t('API Base URL')}
+								bind:value={config.openai.OPENAI_API_BASE_URL}
+								required
+							/>
+
+							<SensitiveInput
+								placeholder={$i18n.t('API Key')}
+								bind:value={config.openai.OPENAI_API_KEY}
+							/>
+						</div>
+					</div>
+				{/if}
 			</div>
-		{/if}
 
-		{#if enableImageGeneration}
-			<hr class=" dark:border-gray-850" />
+			{#if config?.enabled}
+				<hr class=" dark:border-gray-850" />
 
-			<div>
-				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
-				<div class="flex w-full">
-					<div class="flex-1 mr-2">
-						{#if imageGenerationEngine === 'openai' && !OPENAI_API_BASE_URL.includes('https://api.openai.com')}
+				<div>
+					<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
+					<div class="flex w-full">
+						<div class="flex-1 mr-2">
 							<div class="flex w-full">
 								<div class="flex-1">
 									<input
 										list="model-list"
 										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-										bind:value={selectedModel}
+										bind:value={imageGenerationConfig.MODEL}
 										placeholder="Select a model"
 									/>
 
@@ -365,51 +370,36 @@
 									</datalist>
 								</div>
 							</div>
-						{:else}
-							<select
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-								bind:value={selectedModel}
-								placeholder={$i18n.t('Select a model')}
-								required
-							>
-								{#if !selectedModel}
-									<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-								{/if}
-								{#each models ?? [] as model}
-									<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option
-									>
-								{/each}
-							</select>
-						{/if}
+						</div>
 					</div>
 				</div>
-			</div>
 
-			<div>
-				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
-				<div class="flex w-full">
-					<div class="flex-1 mr-2">
-						<input
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-							placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
-							bind:value={imageSize}
-						/>
+				<div>
+					<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
+					<div class="flex w-full">
+						<div class="flex-1 mr-2">
+							<input
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
+								bind:value={imageGenerationConfig.IMAGE_SIZE}
+							/>
+						</div>
 					</div>
 				</div>
-			</div>
 
-			<div>
-				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
-				<div class="flex w-full">
-					<div class="flex-1 mr-2">
-						<input
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-							placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
-							bind:value={steps}
-						/>
+				<div>
+					<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
+					<div class="flex w-full">
+						<div class="flex-1 mr-2">
+							<input
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
+								bind:value={imageGenerationConfig.IMAGE_STEPS}
+							/>
+						</div>
 					</div>
 				</div>
-			</div>
+			{/if}
 		{/if}
 	</div>