Files
LocalAI/core/http/views/text2image.html
Ettore Di Giacinto 797f27f09f feat(UI): image generation improvements (#7804)
* chore: drop mode from image generation(unused)

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(UI): improve image generation front-end

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(UI): only ref images. files is to be deprecated

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* do not override default steps

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-12-31 21:59:46 +01:00

338 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
{{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">
{{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>
{{ 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>
</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:
</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>
</div>
<div id="reference-images-container" class="space-y-2">
<div class="reference-image-item flex items-center gap-2">
<input
type="file"
class="reference-image-file input p-2.5 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 hidden">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
</div>
</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>
</div>
</div>
</div>
</div>
{{template "views/partials/footer" .}}
</div>
<script>
// Collapsible sections
document.getElementById('advanced-toggle').addEventListener('click', function() {
const settings = document.getElementById('advanced-settings');
const chevron = document.getElementById('advanced-chevron');
settings.classList.toggle('hidden');
chevron.classList.toggle('fa-chevron-down');
chevron.classList.toggle('fa-chevron-up');
});
document.getElementById('image-inputs-toggle').addEventListener('click', function() {
const settings = document.getElementById('image-inputs-settings');
const chevron = document.getElementById('image-inputs-chevron');
settings.classList.toggle('hidden');
chevron.classList.toggle('fa-chevron-down');
chevron.classList.toggle('fa-chevron-up');
});
// Size preset buttons
document.querySelectorAll('.size-preset').forEach(button => {
button.addEventListener('click', function() {
const size = this.getAttribute('data-size');
document.getElementById('image-size').value = size;
// Update active state
document.querySelectorAll('.size-preset').forEach(btn => {
btn.classList.remove('bg-[var(--color-primary)]', 'text-white');
});
this.classList.add('bg-[var(--color-primary)]', 'text-white');
});
});
// Set initial active size preset
document.querySelector('.size-preset[data-size="512x512"]').classList.add('bg-[var(--color-primary)]', 'text-white');
// Dynamic image inputs for Reference Images
function addReferenceImage() {
const container = document.getElementById('reference-images-container');
const newItem = document.createElement('div');
newItem.className = 'reference-image-item flex items-center gap-2';
newItem.innerHTML = `
<input
type="file"
class="reference-image-file input p-2.5 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">
<i class="fas fa-times"></i>
</button>
`;
container.appendChild(newItem);
updateRemoveButtons('reference-images-container', 'remove-reference-image');
}
function removeReferenceImage(button) {
const container = document.getElementById('reference-images-container');
if (container.children.length > 1) {
button.closest('.reference-image-item').remove();
updateRemoveButtons('reference-images-container', 'remove-reference-image');
}
}
// Update remove button visibility (hide if only one item, show if multiple)
function updateRemoveButtons(containerId, buttonClass) {
const container = document.getElementById(containerId);
const buttons = container.querySelectorAll('.' + buttonClass);
if (container.children.length > 1) {
buttons.forEach(btn => btn.classList.remove('hidden'));
} else {
buttons.forEach(btn => btn.classList.add('hidden'));
}
}
// Event listeners for dynamic inputs
document.getElementById('add-reference-image').addEventListener('click', addReferenceImage);
document.getElementById('reference-images-container').addEventListener('click', function(e) {
if (e.target.closest('.remove-reference-image')) {
removeReferenceImage(e.target.closest('.remove-reference-image'));
}
});
// Initialize remove button visibility
updateRemoveButtons('reference-images-container', 'remove-reference-image');
</script>
</body>
</html>