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>
This commit is contained in:
Ettore Di Giacinto
2025-12-31 21:59:46 +01:00
committed by GitHub
parent 3f1631aa87
commit 797f27f09f
8 changed files with 493 additions and 98 deletions

View File

@@ -301,7 +301,6 @@ message TranscriptSegment {
message GenerateImageRequest {
int32 height = 1;
int32 width = 2;
int32 mode = 3;
int32 step = 4;
int32 seed = 5;
string positive_prompt = 6;

View File

@@ -7,7 +7,7 @@ import (
model "github.com/mudler/LocalAI/pkg/model"
)
func ImageGeneration(height, width, mode, step, seed int, positive_prompt, negative_prompt, src, dst string, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig, refImages []string) (func() error, error) {
func ImageGeneration(height, width, step, seed int, positive_prompt, negative_prompt, src, dst string, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig, refImages []string) (func() error, error) {
opts := ModelOptions(modelConfig, appConfig)
inferenceModel, err := loader.Load(
@@ -23,7 +23,6 @@ func ImageGeneration(height, width, mode, step, seed int, positive_prompt, negat
&proto.GenerateImageRequest{
Height: int32(height),
Width: int32(width),
Mode: int32(mode),
Step: int32(step),
Seed: int32(seed),
CLIPSkip: int32(modelConfig.Diffusers.ClipSkip),

View File

@@ -157,16 +157,11 @@ func ImageEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
negative_prompt = prompts[1]
}
mode := 0
step := config.Step
if step == 0 {
step = 15
}
if input.Mode != 0 {
mode = input.Mode
}
if input.Step != 0 {
step = input.Step
}
@@ -197,7 +192,7 @@ func ImageEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
inputSrc = inputImages[0]
}
fn, err := backend.ImageGeneration(height, width, mode, step, *config.Seed, positive_prompt, negative_prompt, inputSrc, output, ml, *config, appConfig, refImages)
fn, err := backend.ImageGeneration(height, width, step, *config.Seed, positive_prompt, negative_prompt, inputSrc, output, ml, *config, appConfig, refImages)
if err != nil {
return err
}

View File

@@ -231,7 +231,7 @@ func InpaintingEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, app
// Note: ImageGenerationFunc will call into the loaded model's GenerateImage which expects src JSON
// Also pass ref images (orig + mask) so backends that support ref images can use them.
refImages := []string{origRef, maskRef}
fn, err := backend.ImageGenerationFunc(height, width, 0, steps, 0, prompt, "", jsonPath, dst, ml, *cfg, appConfig, refImages)
fn, err := backend.ImageGenerationFunc(height, width, steps, 0, prompt, "", jsonPath, dst, ml, *cfg, appConfig, refImages)
if err != nil {
return err
}

View File

@@ -10,9 +10,9 @@ import (
"testing"
"github.com/labstack/echo/v4"
"github.com/mudler/LocalAI/core/http/middleware"
"github.com/mudler/LocalAI/core/backend"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/http/middleware"
model "github.com/mudler/LocalAI/pkg/model"
"github.com/stretchr/testify/require"
)
@@ -58,7 +58,7 @@ func TestInpainting_HappyPath(t *testing.T) {
// stub the backend.ImageGenerationFunc
orig := backend.ImageGenerationFunc
backend.ImageGenerationFunc = func(height, width, mode, step, seed int, positive_prompt, negative_prompt, src, dst string, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig, refImages []string) (func() error, error) {
backend.ImageGenerationFunc = func(height, width, step, seed int, positive_prompt, negative_prompt, src, dst string, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig, refImages []string) (func() error, error) {
fn := func() error {
// write a fake png file to dst
return os.WriteFile(dst, []byte("PNGDATA"), 0644)

View File

@@ -1,61 +1,255 @@
// Helper function to convert file to base64
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// Remove data:image/...;base64, prefix if present
const base64 = reader.result.split(',')[1] || reader.result;
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// Helper function to read multiple files
async function filesToBase64Array(fileList) {
const base64Array = [];
for (let i = 0; i < fileList.length; i++) {
const base64 = await fileToBase64(fileList[i]);
base64Array.push(base64);
}
return base64Array;
}
function genImage(event) {
event.preventDefault();
const input = document.getElementById("input").value;
promptDallE(input);
promptDallE();
}
async function promptDallE(input) {
document.getElementById("loader").style.display = "block";
document.getElementById("input").value = "";
document.getElementById("input").disabled = true;
const model = document.getElementById("image-model").value;
const size = document.getElementById("image-size").value;
const response = await fetch("v1/images/generations", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: model,
steps: 10,
prompt: input,
n: 1,
size: size,
}),
});
const json = await response.json();
if (json.error) {
// Display error if there is one
var div = document.getElementById('result'); // Get the div by its ID
div.innerHTML = '<p style="color:red;">' + json.error.message + '</p>';
async function promptDallE() {
const loader = document.getElementById("loader");
const input = document.getElementById("input");
const generateBtn = document.getElementById("generate-btn");
const resultDiv = document.getElementById("result");
// Show loader and disable form
loader.style.display = "block";
input.disabled = true;
generateBtn.disabled = true;
// Store the prompt for later restoration
const prompt = input.value.trim();
if (!prompt) {
alert("Please enter a prompt");
loader.style.display = "none";
input.disabled = false;
generateBtn.disabled = false;
return;
}
const url = json.data[0].url;
var div = document.getElementById('result'); // Get the div by its ID
var img = document.createElement('img'); // Create a new img element
img.src = url; // Set the source of the image
img.alt = 'Generated image'; // Set the alt text of the image
// Collect all form values
const model = document.getElementById("image-model").value;
const size = document.getElementById("image-size").value;
const negativePrompt = document.getElementById("negative-prompt").value.trim();
const n = parseInt(document.getElementById("image-count").value) || 1;
const stepInput = document.getElementById("image-steps").value.trim();
const step = stepInput ? parseInt(stepInput) : undefined;
const seedInput = document.getElementById("image-seed").value.trim();
const seed = seedInput ? parseInt(seedInput) : undefined;
div.innerHTML = ''; // Clear the existing content of the div
div.appendChild(img); // Add the new img element to the div
// Prepare request body
// Combine prompt and negative prompt with "|" separator (backend expects this format)
let combinedPrompt = prompt;
if (negativePrompt) {
combinedPrompt = prompt + "|" + negativePrompt;
}
document.getElementById("loader").style.display = "none";
document.getElementById("input").disabled = false;
document.getElementById("input").focus();
const requestBody = {
model: model,
prompt: combinedPrompt,
n: n,
size: size,
};
if (step !== undefined) {
requestBody.step = step;
}
if (seed !== undefined) {
requestBody.seed = seed;
}
// Handle file inputs
try {
// Source image (single file for img2img)
const sourceImageInput = document.getElementById("source-image");
if (sourceImageInput.files.length > 0) {
const base64 = await fileToBase64(sourceImageInput.files[0]);
requestBody.file = base64;
}
// Reference images (collect from all dynamic inputs)
const refImageInputs = document.querySelectorAll('.reference-image-file');
const refImageFiles = [];
for (const input of refImageInputs) {
if (input.files.length > 0) {
refImageFiles.push(input.files[0]);
}
}
if (refImageFiles.length > 0) {
const base64Array = await filesToBase64Array(refImageFiles);
requestBody.ref_images = base64Array;
}
} 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";
input.disabled = false;
generateBtn.disabled = false;
return;
}
// Make API request
try {
const response = await fetch("v1/images/generations", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
const json = await response.json();
if (json.error) {
// Display error
resultDiv.innerHTML = '<p class="text-red-500 p-4">Error: ' + json.error.message + '</p>';
loader.style.display = "none";
input.disabled = false;
generateBtn.disabled = false;
return;
}
// Clear result div
resultDiv.innerHTML = '';
// 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";
// Create image element
const img = document.createElement("img");
if (item.url) {
img.src = item.url;
} else if (item.b64_json) {
img.src = "data:image/png;base64," + item.b64_json;
} else {
return; // Skip invalid items
}
img.alt = prompt;
img.className = "w-full h-auto rounded-lg mb-3";
imageContainer.appendChild(img);
// Create caption container
const captionDiv = document.createElement("div");
captionDiv.className = "mt-3 p-3 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.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.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.innerHTML = `
<span><strong>Size:</strong> ${size}</span>
${step !== undefined ? `<span><strong>Steps:</strong> ${step}</span>` : ''}
${seed !== undefined ? `<span><strong>Seed:</strong> ${seed}</span>` : ''}
`;
captionDiv.appendChild(detailsDiv);
// 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.innerHTML = '<i class="fas fa-copy mr-1"></i>Copy Prompt';
copyBtn.onclick = () => {
navigator.clipboard.writeText(prompt).then(() => {
copyBtn.innerHTML = '<i class="fas fa-check mr-1"></i>Copied!';
setTimeout(() => {
copyBtn.innerHTML = '<i class="fas fa-copy mr-1"></i>Copy Prompt';
}, 2000);
});
};
captionDiv.appendChild(copyBtn);
imageContainer.appendChild(captionDiv);
resultDiv.appendChild(imageContainer);
});
} else {
resultDiv.innerHTML = '<p class="text-[var(--color-text-secondary)] p-4">No images were generated.</p>';
}
// Preserve prompt in input field (don't clear it)
// The prompt is already in the input field, so we don't need to restore it
} catch (error) {
console.error("Error generating image:", error);
resultDiv.innerHTML = '<p class="text-red-500 p-4">Error: ' + error.message + '</p>';
} finally {
// Hide loader and re-enable form
loader.style.display = "none";
input.disabled = false;
generateBtn.disabled = false;
input.focus();
}
}
document.getElementById("input").focus();
document.getElementById("genimage").addEventListener("submit", genImage);
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Handle Enter key press in the prompt input
document.getElementById("input").addEventListener("keypress", function(event) {
if (event.key === "Enter") {
// Initialize
document.addEventListener("DOMContentLoaded", function() {
const input = document.getElementById("input");
const form = document.getElementById("genimage");
if (input) {
input.focus();
}
if (form) {
form.addEventListener("submit", genImage);
}
// Handle Enter key press in the prompt input (but allow Shift+Enter for new lines)
if (input) {
input.addEventListener("keydown", function(event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
genImage(event);
}
});
}
});
}
document.getElementById("loader").style.display = "none";
// Hide loader initially
const loader = document.getElementById("loader");
if (loader) {
loader.style.display = "none";
}
});

View File

@@ -55,46 +55,176 @@
<div class="relative">
<input id="image-model" type="hidden" value="{{.Model}}">
<form id="genimage" action="text2image/{{.Model}}" method="get" class="mb-8">
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<i class="fas fa-magic text-[var(--color-primary)]"></i>
<!-- 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>
<input
type="text"
id="input"
name="input"
placeholder="Describe the image you want to generate..."
autocomplete="off"
class="input w-full pr-12 py-4 text-lg"
style="padding-left: 3.5rem !important;"
required
/>
<span id="loader" class="my-2 loader absolute right-4 top-4 hidden">
<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>
<!-- Size Selection -->
<div class="mt-4">
<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>
<input
type="text"
id="image-size"
value="256x256"
placeholder="e.g., 256x256, 512x512, 1024x1024"
class="input p-2.5 w-full max-w-xs"
/>
<!-- 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
@@ -104,10 +234,10 @@
<!-- 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 Image</h3>
<div class="container mx-auto flex justify-center">
<div id="result" class="mx-auto bg-[var(--color-bg-primary)]/50 border border-[#1E293B] rounded-xl p-4 min-h-[300px] w-full flex items-center justify-center">
<p class="text-[var(--color-text-secondary)] italic">Your generated image will appear here</p>
<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>
@@ -119,10 +249,89 @@
</div>
<script>
// Show loader when form is submitted
document.getElementById('genimage').addEventListener('submit', function() {
document.getElementById('loader').classList.remove('hidden');
// 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>

View File

@@ -160,7 +160,6 @@ type OpenAIRequest struct {
Stream bool `json:"stream"`
// Image (not supported by OpenAI)
Mode int `json:"mode"`
Quality string `json:"quality"`
Step int `json:"step"`