mirror of
https://github.com/mudler/LocalAI.git
synced 2026-04-04 23:14:41 -04:00
feat(image-gen/UI): move controls to the left, make the page more compact (#7823)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
5ee6c1810b
commit
76cfe1f367
@@ -32,9 +32,13 @@ async function promptDallE() {
|
||||
const input = document.getElementById("input");
|
||||
const generateBtn = document.getElementById("generate-btn");
|
||||
const resultDiv = document.getElementById("result");
|
||||
const resultPlaceholder = document.getElementById("result-placeholder");
|
||||
|
||||
// Show loader and disable form
|
||||
loader.style.display = "block";
|
||||
loader.classList.remove("hidden");
|
||||
if (resultPlaceholder) {
|
||||
resultPlaceholder.style.display = "none";
|
||||
}
|
||||
input.disabled = true;
|
||||
generateBtn.disabled = true;
|
||||
|
||||
@@ -42,7 +46,10 @@ async function promptDallE() {
|
||||
const prompt = input.value.trim();
|
||||
if (!prompt) {
|
||||
alert("Please enter a prompt");
|
||||
loader.style.display = "none";
|
||||
loader.classList.add("hidden");
|
||||
if (resultPlaceholder) {
|
||||
resultPlaceholder.style.display = "flex";
|
||||
}
|
||||
input.disabled = false;
|
||||
generateBtn.disabled = false;
|
||||
return;
|
||||
@@ -103,8 +110,11 @@ async function promptDallE() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing image files:", error);
|
||||
resultDiv.innerHTML = '<p class="text-red-500">Error processing image files: ' + error.message + '</p>';
|
||||
loader.style.display = "none";
|
||||
resultDiv.innerHTML = '<p class="text-xs text-red-500 p-2">Error processing image files: ' + error.message + '</p>';
|
||||
loader.classList.add("hidden");
|
||||
if (resultPlaceholder) {
|
||||
resultPlaceholder.style.display = "none";
|
||||
}
|
||||
input.disabled = false;
|
||||
generateBtn.disabled = false;
|
||||
return;
|
||||
@@ -124,21 +134,27 @@ async function promptDallE() {
|
||||
|
||||
if (json.error) {
|
||||
// Display error
|
||||
resultDiv.innerHTML = '<p class="text-red-500 p-4">Error: ' + json.error.message + '</p>';
|
||||
loader.style.display = "none";
|
||||
resultDiv.innerHTML = '<p class="text-xs text-red-500 p-2">Error: ' + json.error.message + '</p>';
|
||||
loader.classList.add("hidden");
|
||||
if (resultPlaceholder) {
|
||||
resultPlaceholder.style.display = "none";
|
||||
}
|
||||
input.disabled = false;
|
||||
generateBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear result div
|
||||
// Clear result div and hide placeholder
|
||||
resultDiv.innerHTML = '';
|
||||
if (resultPlaceholder) {
|
||||
resultPlaceholder.style.display = "none";
|
||||
}
|
||||
|
||||
// Display all generated images
|
||||
if (json.data && json.data.length > 0) {
|
||||
json.data.forEach((item, index) => {
|
||||
const imageContainer = document.createElement("div");
|
||||
imageContainer.className = "mb-6 bg-[var(--color-bg-primary)]/50 border border-[#1E293B] rounded-xl p-4";
|
||||
imageContainer.className = "mb-4 bg-[var(--color-bg-primary)]/50 border border-[#1E293B] rounded-lg p-2";
|
||||
|
||||
// Create image element
|
||||
const img = document.createElement("img");
|
||||
@@ -150,30 +166,30 @@ async function promptDallE() {
|
||||
return; // Skip invalid items
|
||||
}
|
||||
img.alt = prompt;
|
||||
img.className = "w-full h-auto rounded-lg mb-3";
|
||||
img.className = "w-full h-auto rounded-lg mb-2";
|
||||
imageContainer.appendChild(img);
|
||||
|
||||
// Create caption container
|
||||
const captionDiv = document.createElement("div");
|
||||
captionDiv.className = "mt-3 p-3 bg-[var(--color-bg-secondary)] rounded-lg";
|
||||
captionDiv.className = "mt-2 p-2 bg-[var(--color-bg-secondary)] rounded-lg";
|
||||
|
||||
// Prompt caption
|
||||
const promptCaption = document.createElement("p");
|
||||
promptCaption.className = "text-sm text-[var(--color-text-primary)] mb-2";
|
||||
promptCaption.className = "text-xs text-[var(--color-text-primary)] mb-1.5";
|
||||
promptCaption.innerHTML = '<strong>Prompt:</strong> ' + escapeHtml(prompt);
|
||||
captionDiv.appendChild(promptCaption);
|
||||
|
||||
// Negative prompt if provided
|
||||
if (negativePrompt) {
|
||||
const negativeCaption = document.createElement("p");
|
||||
negativeCaption.className = "text-sm text-[var(--color-text-secondary)] mb-2";
|
||||
negativeCaption.className = "text-xs text-[var(--color-text-secondary)] mb-1.5";
|
||||
negativeCaption.innerHTML = '<strong>Negative Prompt:</strong> ' + escapeHtml(negativePrompt);
|
||||
captionDiv.appendChild(negativeCaption);
|
||||
}
|
||||
|
||||
// Generation details
|
||||
const detailsDiv = document.createElement("div");
|
||||
detailsDiv.className = "flex flex-wrap gap-4 text-xs text-[var(--color-text-secondary)] mt-2";
|
||||
detailsDiv.className = "flex flex-wrap gap-3 text-[10px] text-[var(--color-text-secondary)] mt-1.5";
|
||||
detailsDiv.innerHTML = `
|
||||
<span><strong>Size:</strong> ${size}</span>
|
||||
${step !== undefined ? `<span><strong>Steps:</strong> ${step}</span>` : ''}
|
||||
@@ -183,7 +199,7 @@ async function promptDallE() {
|
||||
|
||||
// Copy prompt button
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.className = "mt-2 px-3 py-1 text-xs bg-[var(--color-primary)] text-white rounded hover:opacity-80";
|
||||
copyBtn.className = "mt-1.5 px-2 py-0.5 text-[10px] bg-[var(--color-primary)] text-white rounded hover:opacity-80";
|
||||
copyBtn.innerHTML = '<i class="fas fa-copy mr-1"></i>Copy Prompt';
|
||||
copyBtn.onclick = () => {
|
||||
navigator.clipboard.writeText(prompt).then(() => {
|
||||
@@ -198,8 +214,15 @@ async function promptDallE() {
|
||||
imageContainer.appendChild(captionDiv);
|
||||
resultDiv.appendChild(imageContainer);
|
||||
});
|
||||
// Hide placeholder when images are displayed
|
||||
if (resultPlaceholder) {
|
||||
resultPlaceholder.style.display = "none";
|
||||
}
|
||||
} else {
|
||||
resultDiv.innerHTML = '<p class="text-[var(--color-text-secondary)] p-4">No images were generated.</p>';
|
||||
resultDiv.innerHTML = '<p class="text-xs text-[var(--color-text-secondary)] p-2">No images were generated.</p>';
|
||||
if (resultPlaceholder) {
|
||||
resultPlaceholder.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve prompt in input field (don't clear it)
|
||||
@@ -207,10 +230,13 @@ async function promptDallE() {
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error generating image:", error);
|
||||
resultDiv.innerHTML = '<p class="text-red-500 p-4">Error: ' + error.message + '</p>';
|
||||
resultDiv.innerHTML = '<p class="text-xs text-red-500 p-2">Error: ' + error.message + '</p>';
|
||||
if (resultPlaceholder) {
|
||||
resultPlaceholder.style.display = "none";
|
||||
}
|
||||
} finally {
|
||||
// Hide loader and re-enable form
|
||||
loader.style.display = "none";
|
||||
loader.classList.add("hidden");
|
||||
input.disabled = false;
|
||||
generateBtn.disabled = false;
|
||||
input.focus();
|
||||
@@ -250,6 +276,6 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
// Hide loader initially
|
||||
const loader = document.getElementById("loader");
|
||||
if (loader) {
|
||||
loader.style.display = "none";
|
||||
loader.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,249 +3,243 @@
|
||||
{{template "views/partials/head" .}}
|
||||
<script defer src="static/image.js"></script>
|
||||
|
||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||
<div class="flex flex-col min-h-screen">
|
||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex flex-col h-screen">
|
||||
<div class="flex flex-col flex-1 overflow-hidden">
|
||||
|
||||
{{template "views/partials/navbar" .}}
|
||||
<div class="container mx-auto px-4 py-8 flex-grow" x-data="{ component: 'menu' }">
|
||||
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-section">
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">
|
||||
Image Generation {{ if .Model }} with {{.Model}} {{ end }}
|
||||
</h1>
|
||||
<p class="hero-subtitle">Create stunning images from text descriptions</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Selection - Positioned between hero and generation form -->
|
||||
<div class="card p-5 mb-6">
|
||||
<div class="flex items-center">
|
||||
<div class="text-lg font-medium text-[var(--color-primary)] mr-4">
|
||||
<i class="fas fa-palette mr-2"></i>Select Model:
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<select x-data="{ link : '' }" x-model="link" x-init="$watch('link', value => window.location = link)"
|
||||
id="model-select"
|
||||
class="input w-full max-w-md p-2.5 pr-10"
|
||||
>
|
||||
<option value="" disabled class="text-[var(--color-text-secondary)]">Select a model</option>
|
||||
{{ $model:=.Model}}
|
||||
{{ range .ModelsConfig }}
|
||||
{{ $cfg := . }}
|
||||
{{ range .KnownUsecaseStrings }}
|
||||
{{ if eq . "FLAG_IMAGE" }}
|
||||
<option value="text2image/{{$cfg.Name}}" {{ if eq $cfg.Name $model }} selected {{end}} class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">{{$cfg.Name}}</option>
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- Two Column Layout: Settings on Left, Preview on Right -->
|
||||
<div class="flex flex-col lg:flex-row flex-1 gap-4 p-4">
|
||||
<!-- Left Column: Generation Settings -->
|
||||
<div class="flex-shrink-0 lg:w-1/4">
|
||||
<div class="card p-3 space-y-3">
|
||||
<!-- Model Selection - Compact -->
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<label class="text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide flex-shrink-0">Model</label>
|
||||
</div>
|
||||
<select x-data="{ link : '' }" x-model="link" x-init="$watch('link', value => window.location = link)"
|
||||
id="model-select"
|
||||
class="input w-full p-1.5 text-xs"
|
||||
>
|
||||
<option value="" disabled class="text-[var(--color-text-secondary)]">Select a model</option>
|
||||
{{ $model:=.Model}}
|
||||
{{ range .ModelsConfig }}
|
||||
{{ $cfg := . }}
|
||||
{{ range .KnownUsecaseStrings }}
|
||||
{{ if eq . "FLAG_IMAGE" }}
|
||||
<option value="text2image/{{$cfg.Name}}" {{ if eq $cfg.Name $model }} selected {{end}} class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">{{$cfg.Name}}</option>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ range .ModelsWithoutConfig }}
|
||||
<option value="text2image/{{.}}" {{ if eq . $model }} selected {{ end }} class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Generation Form -->
|
||||
<div class="card p-6">
|
||||
<h2 class="h3 mb-6">Generate an Image</h2>
|
||||
|
||||
<div class="relative">
|
||||
<input id="image-model" type="hidden" value="{{.Model}}">
|
||||
<form id="genimage" action="text2image/{{.Model}}" method="get" class="mb-8">
|
||||
<!-- Basic Settings -->
|
||||
<div class="space-y-4">
|
||||
<!-- Prompt -->
|
||||
<div class="relative">
|
||||
<label for="input" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-magic mr-2 text-[var(--color-primary)]"></i>Prompt:
|
||||
</label>
|
||||
<div class="relative">
|
||||
<textarea
|
||||
id="input"
|
||||
name="input"
|
||||
placeholder="Describe the image you want to generate..."
|
||||
autocomplete="off"
|
||||
rows="3"
|
||||
class="input w-full pr-12 py-4 text-lg resize-y"
|
||||
required
|
||||
></textarea>
|
||||
<span id="loader" class="loader absolute right-4 top-4 hidden" style="top: 1rem; right: 1rem;">
|
||||
<svg class="animate-spin h-6 w-6 text-[var(--color-primary)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Negative Prompt -->
|
||||
<div>
|
||||
<label for="negative-prompt" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-ban mr-2 text-[var(--color-primary)]"></i>Negative Prompt (optional):
|
||||
</label>
|
||||
<textarea
|
||||
id="negative-prompt"
|
||||
name="negative-prompt"
|
||||
placeholder="Things to avoid in the image..."
|
||||
rows="2"
|
||||
class="input w-full py-2 resize-y"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Size Selection with Presets -->
|
||||
<div>
|
||||
<label for="image-size" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-expand-arrows-alt mr-2 text-[var(--color-primary)]"></i>Image Size:
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
<button type="button" class="size-preset px-3 py-1 text-sm rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="256x256">256×256</button>
|
||||
<button type="button" class="size-preset px-3 py-1 text-sm rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="512x512">512×512</button>
|
||||
<button type="button" class="size-preset px-3 py-1 text-sm rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="768x768">768×768</button>
|
||||
<button type="button" class="size-preset px-3 py-1 text-sm rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="1024x1024">1024×1024</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="image-size"
|
||||
value="512x512"
|
||||
placeholder="e.g., 256x256, 512x512, 1024x1024"
|
||||
class="input p-2.5 w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Number of Images -->
|
||||
<div>
|
||||
<label for="image-count" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-images mr-2 text-[var(--color-primary)]"></i>Number of Images:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="image-count"
|
||||
name="n"
|
||||
min="1"
|
||||
max="4"
|
||||
value="1"
|
||||
class="input p-2.5 w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
{{ range .ModelsWithoutConfig }}
|
||||
<option value="text2image/{{.}}" {{ if eq . $model }} selected {{ end }} class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Settings (Collapsible) -->
|
||||
<div class="mt-6">
|
||||
<button type="button" id="advanced-toggle" class="flex items-center justify-between w-full text-left text-sm font-medium text-[var(--color-text-primary)] hover:text-[var(--color-primary)]">
|
||||
<span><i class="fas fa-cog mr-2"></i>Advanced Settings</span>
|
||||
<i class="fas fa-chevron-down" id="advanced-chevron"></i>
|
||||
</button>
|
||||
<div id="advanced-settings" class="hidden mt-4 space-y-4">
|
||||
<!-- Steps -->
|
||||
<div>
|
||||
<label for="image-steps" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-step-forward mr-2 text-[var(--color-primary)]"></i>Steps:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="image-steps"
|
||||
name="step"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="Leave empty for default"
|
||||
class="input p-2.5 w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Seed -->
|
||||
<div>
|
||||
<label for="image-seed" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-seedling mr-2 text-[var(--color-primary)]"></i>Seed (optional):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="image-seed"
|
||||
name="seed"
|
||||
min="0"
|
||||
placeholder="Leave empty for random"
|
||||
class="input p-2.5 w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Inputs (Collapsible) -->
|
||||
<div class="mt-6">
|
||||
<button type="button" id="image-inputs-toggle" class="flex items-center justify-between w-full text-left text-sm font-medium text-[var(--color-text-primary)] hover:text-[var(--color-primary)]">
|
||||
<span><i class="fas fa-image mr-2"></i>Image Inputs (for img2img/inpainting)</span>
|
||||
<i class="fas fa-chevron-down" id="image-inputs-chevron"></i>
|
||||
</button>
|
||||
<div id="image-inputs-settings" class="hidden mt-4 space-y-4">
|
||||
<!-- Source Image (img2img) -->
|
||||
<div>
|
||||
<label for="source-image" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-file-image mr-2 text-[var(--color-primary)]"></i>Source Image (img2img):
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="source-image"
|
||||
name="file"
|
||||
accept="image/*"
|
||||
class="input p-2.5 w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Reference Images (Dynamic) -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
<i class="fas fa-images mr-2 text-[var(--color-primary)]"></i>Multiple Input Images:
|
||||
|
||||
<div class="relative">
|
||||
<input id="image-model" type="hidden" value="{{.Model}}">
|
||||
<form id="genimage" action="text2image/{{.Model}}" method="get">
|
||||
<!-- Basic Settings -->
|
||||
<div class="space-y-2">
|
||||
<!-- Prompt -->
|
||||
<div class="space-y-1">
|
||||
<label for="input" class="block text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide">
|
||||
<i class="fas fa-magic mr-1.5 text-[var(--color-primary)]"></i>Prompt
|
||||
</label>
|
||||
<button type="button" id="add-reference-image" class="px-3 py-1 text-sm bg-[var(--color-primary)] text-white rounded hover:opacity-80">
|
||||
<i class="fas fa-plus mr-1"></i>Add Image
|
||||
</button>
|
||||
<textarea
|
||||
id="input"
|
||||
name="input"
|
||||
placeholder="Describe the image you want to generate..."
|
||||
autocomplete="off"
|
||||
rows="3"
|
||||
class="input w-full p-1.5 text-xs resize-y"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<div id="reference-images-container" class="space-y-2">
|
||||
<div class="reference-image-item flex items-center gap-2">
|
||||
|
||||
<!-- Negative Prompt -->
|
||||
<div class="space-y-1">
|
||||
<label for="negative-prompt" class="block text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide">
|
||||
<i class="fas fa-ban mr-1.5 text-[var(--color-primary)]"></i>Negative Prompt
|
||||
</label>
|
||||
<textarea
|
||||
id="negative-prompt"
|
||||
name="negative-prompt"
|
||||
placeholder="Things to avoid in the image..."
|
||||
rows="2"
|
||||
class="input w-full p-1.5 text-xs resize-y"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Size Selection with Presets -->
|
||||
<div class="space-y-1">
|
||||
<label for="image-size" class="block text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide">
|
||||
<i class="fas fa-expand-arrows-alt mr-1.5 text-[var(--color-primary)]"></i>Image Size
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-1.5 mb-1.5">
|
||||
<button type="button" class="size-preset px-2 py-0.5 text-[10px] rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="256x256">256×256</button>
|
||||
<button type="button" class="size-preset px-2 py-0.5 text-[10px] rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="512x512">512×512</button>
|
||||
<button type="button" class="size-preset px-2 py-0.5 text-[10px] rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="768x768">768×768</button>
|
||||
<button type="button" class="size-preset px-2 py-0.5 text-[10px] rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="1024x1024">1024×1024</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="image-size"
|
||||
value="512x512"
|
||||
placeholder="e.g., 256x256, 512x512, 1024x1024"
|
||||
class="input p-1.5 text-xs w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Number of Images -->
|
||||
<div class="space-y-1">
|
||||
<label for="image-count" class="block text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide">
|
||||
<i class="fas fa-images mr-1.5 text-[var(--color-primary)]"></i>Number of Images
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="image-count"
|
||||
name="n"
|
||||
min="1"
|
||||
max="4"
|
||||
value="1"
|
||||
class="input p-1.5 text-xs w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Settings (Collapsible) -->
|
||||
<div class="space-y-2">
|
||||
<button type="button" id="advanced-toggle" class="w-full flex items-center justify-between px-2 py-1.5 text-xs rounded text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors">
|
||||
<span><i class="fa-solid fa-sliders mr-1.5 text-[var(--color-primary)]"></i> Advanced Settings</span>
|
||||
<i class="fas fa-chevron-down text-[10px]" id="advanced-chevron"></i>
|
||||
</button>
|
||||
<div id="advanced-settings" class="hidden p-2 bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20 rounded pl-4 border-l-2 border-[var(--color-bg-secondary)] space-y-2">
|
||||
<!-- Steps -->
|
||||
<div class="space-y-1">
|
||||
<label for="image-steps" class="block text-xs text-[var(--color-text-secondary)]">
|
||||
<i class="fas fa-step-forward mr-1.5 text-[var(--color-primary)]"></i>Steps
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
class="reference-image-file input p-2.5 flex-1"
|
||||
accept="image/*"
|
||||
data-type="ref_images"
|
||||
type="number"
|
||||
id="image-steps"
|
||||
name="step"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="Leave empty for default"
|
||||
class="input p-1.5 text-xs w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Seed -->
|
||||
<div class="space-y-1">
|
||||
<label for="image-seed" class="block text-xs text-[var(--color-text-secondary)]">
|
||||
<i class="fas fa-seedling mr-1.5 text-[var(--color-primary)]"></i>Seed
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="image-seed"
|
||||
name="seed"
|
||||
min="0"
|
||||
placeholder="Leave empty for random"
|
||||
class="input p-1.5 text-xs w-full"
|
||||
/>
|
||||
<button type="button" class="remove-reference-image px-3 py-2 text-sm bg-red-500 text-white rounded hover:opacity-80 hidden">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Inputs (Collapsible) -->
|
||||
<div class="space-y-2">
|
||||
<button type="button" id="image-inputs-toggle" class="w-full flex items-center justify-between px-2 py-1.5 text-xs rounded text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors">
|
||||
<span><i class="fa-solid fa-image mr-1.5 text-[var(--color-primary)]"></i> Image Inputs</span>
|
||||
<i class="fas fa-chevron-down text-[10px]" id="image-inputs-chevron"></i>
|
||||
</button>
|
||||
<div id="image-inputs-settings" class="hidden p-2 bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20 rounded pl-4 border-l-2 border-[var(--color-bg-secondary)] space-y-2">
|
||||
<!-- Source Image (img2img) -->
|
||||
<div class="space-y-1">
|
||||
<label for="source-image" class="block text-xs text-[var(--color-text-secondary)]">
|
||||
<i class="fas fa-file-image mr-1.5 text-[var(--color-primary)]"></i>Source Image (img2img)
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="source-image"
|
||||
name="file"
|
||||
accept="image/*"
|
||||
class="input p-1.5 text-xs w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Reference Images (Dynamic) -->
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="block text-xs text-[var(--color-text-secondary)]">
|
||||
<i class="fas fa-images mr-1.5 text-[var(--color-primary)]"></i>Multiple Input Images
|
||||
</label>
|
||||
<button type="button" id="add-reference-image" class="px-2 py-0.5 text-[10px] bg-[var(--color-primary)] text-white rounded hover:opacity-80">
|
||||
<i class="fas fa-plus mr-1"></i>Add
|
||||
</button>
|
||||
</div>
|
||||
<div id="reference-images-container" class="space-y-1.5">
|
||||
<div class="reference-image-item flex items-center gap-1.5">
|
||||
<input
|
||||
type="file"
|
||||
class="reference-image-file input p-1.5 text-xs flex-1"
|
||||
accept="image/*"
|
||||
data-type="ref_images"
|
||||
/>
|
||||
<button type="button" class="remove-reference-image px-1.5 py-1.5 text-[10px] bg-red-500 text-white rounded hover:opacity-80 hidden">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
id="generate-btn"
|
||||
class="w-full px-2 py-1.5 text-xs rounded text-[var(--color-bg-primary)] bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 transition-colors font-medium"
|
||||
>
|
||||
<i class="fas fa-magic mr-1.5"></i>Generate Image
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
id="generate-btn"
|
||||
class="btn-primary w-full"
|
||||
>
|
||||
<i class="fas fa-magic mr-2"></i>Generate Image
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Image Results Container -->
|
||||
<div class="mt-6 border-t border-[#1E293B] pt-6">
|
||||
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4">Generated Images</h3>
|
||||
<div id="result" class="space-y-6">
|
||||
<div class="bg-[var(--color-bg-primary)]/50 border border-[#1E293B] rounded-xl p-4 min-h-[300px] flex items-center justify-center">
|
||||
<p class="text-[var(--color-text-secondary)] italic">Your generated images will appear here</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Image Preview -->
|
||||
<div class="flex-grow lg:w-3/4">
|
||||
<div class="card p-3">
|
||||
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Generated Images</h3>
|
||||
<div class="relative min-h-[400px]">
|
||||
<!-- Loading Animation -->
|
||||
<div id="loader" class="hidden absolute inset-0 flex items-center justify-center bg-[var(--color-bg-primary)]/80 rounded-xl z-10">
|
||||
<div class="text-center">
|
||||
<svg class="animate-spin h-10 w-10 text-[var(--color-primary)] mx-auto mb-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p class="text-xs text-[var(--color-text-secondary)]">Generating image...</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Placeholder when no images -->
|
||||
<div id="result-placeholder" class="bg-[var(--color-bg-primary)]/50 border border-[#1E293B] rounded-xl p-6 min-h-[400px] flex items-center justify-center">
|
||||
<p class="text-xs text-[var(--color-text-secondary)] italic text-center">Your generated images will appear here</p>
|
||||
</div>
|
||||
<!-- Results container -->
|
||||
<div id="result" class="space-y-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "views/partials/footer" .}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -290,11 +284,11 @@
|
||||
newItem.innerHTML = `
|
||||
<input
|
||||
type="file"
|
||||
class="reference-image-file input p-2.5 flex-1"
|
||||
class="reference-image-file input p-1.5 text-xs flex-1"
|
||||
accept="image/*"
|
||||
data-type="ref_images"
|
||||
/>
|
||||
<button type="button" class="remove-reference-image px-3 py-2 text-sm bg-red-500 text-white rounded hover:opacity-80">
|
||||
<button type="button" class="remove-reference-image px-1.5 py-1.5 text-[10px] bg-red-500 text-white rounded hover:opacity-80">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user