Un generatore di immagini multi-modello non è un singolo prompt cablato: è un pannello dove modello, dimensione e qualità diventano parametri espliciti, con preset per i casi comuni e una cronologia che tiene traccia di ogni risultato. Vediamo i pezzi fondamentali, con codice reale — incluso il punto dove il prototipo iniziale va corretto prima di andare oltre l'uso locale.
Un generatore di immagini multi-modello ha bisogno di due zone ben separate: da un lato i controlli (prompt, modello, dimensione, qualità), dall'altro la cronologia dei risultati. Tenerle in due colonne indipendenti, invece che in un unico form lineare, permette di generare più immagini in sequenza senza perdere le precedenti:
<div class="main-cols">
<!-- LEFT: pannello di controllo -->
<div class="col-left">
<div class="card">
<div class="field">
<label>Prompt</label>
<textarea id="prompt" rows="5" placeholder="Describe the image you want to generate…"></textarea>
</div>
<div class="field">
<label>Model</label>
<select id="modelSelect"> ... </select>
</div>
<!-- Size, Quality: stessa logica, altri <select> -->
</div>
</div>
<!-- RIGHT: cronologia risultati -->
<div class="col-right">
<div class="history-header">Generated Images</div>
<div id="chat-containerIMG">
<div class="empty-state" id="emptyState">
<div class="empty-icon">🖼️</div>
<div>Your generated images will appear here</div>
</div>
</div>
</div>
</div>
Il pannello a sinistra è a larghezza fissa (width: 320px) perché contiene solo controlli, mentre la colonna destra è flex: 1 e scrolla in verticale: la cronologia può crescere all'infinito senza rompere il layout.
La parte "multi-modello" del generatore non è altro che tre <select> che mappano direttamente sui parametri dell'API di generazione immagini di OpenAI: model, size e quality. Niente testo libero, solo valori enumerati validi:
<div class="field">
<label>Model</label>
<select id="modelSelect">
<option value="gpt-image-1-mini" selected>gpt-image-1-mini</option>
<option value="gpt-image-1">gpt-image-1</option>
<option value="gpt-image-1.5">gpt-image-1.5</option>
</select>
</div>
<div class="field">
<label>Size</label>
<select id="sizeSelect">
<option value="1024x1024" selected>1024×1024 — Square</option>
<option value="1536x1024">1536×1024 — Landscape</option>
<option value="1024x1536">1024×1536 — Portrait</option>
</select>
</div>
<div class="field">
<label>Quality</label>
<select id="qualitySelect">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
</div>
Tre famiglie di modelli, in ordine di capacità/costo crescente: gpt-image-1-mini (il più economico e veloce, buono per bozze), gpt-image-1 (equilibrio qualità/costo) e gpt-image-1.5 (la versione più capace, indicata quando il risultato finale conta più della velocità). Le tre dimensioni disponibili — quadrata, orizzontale, verticale — coprono la maggior parte degli usi reali senza esporre all'utente rapporti d'aspetto arbitrari che l'API potrebbe rifiutare.
quality non è un dettaglio estetico marginale — incide direttamente sul costo per immagine e sul tempo di generazione. Esporlo come scelta esplicita (non un default fisso nel codice) è ciò che rende il pannello davvero "multi-setting" e utile in produzione, dove non tutte le immagini richiedono la qualità massima.
Tre <select> indipendenti significano nove possibili combinazioni da impostare a mano ogni volta. Per i casi d'uso più comuni, tre bottoni preset scrivono direttamente i tre valori in un colpo:
<div class="preset-row">
<div class="btn-preset ultra" onclick="applyPreset('ultra')">🔥 Ultra</div>
<div class="btn-preset opt" onclick="applyPreset('optimized')">⚡ Optimized</div>
<div class="btn-preset cheap" onclick="applyPreset('cheap')">💚 Cheap</div>
</div>
function applyPreset(type) {
const m = document.getElementById("modelSelect");
const s = document.getElementById("sizeSelect");
const q = document.getElementById("qualitySelect");
if (type === "ultra") { m.value = "gpt-image-1.5"; s.value = "1024x1024"; q.value = "high"; }
if (type === "optimized") { m.value = "gpt-image-1"; s.value = "1024x1024"; q.value = "medium"; }
if (type === "cheap") { m.value = "gpt-image-1-mini"; s.value = "1024x1024"; q.value = "low"; }
}
Ultra punta al modello più capace con qualità alta (per l'immagine finale che conta), Optimized è il compromesso di default (modello intermedio, qualità media), Cheap minimizza costo e tempo (modello mini, qualità bassa) — utile per iterare velocemente su un prompt prima di generare la versione definitiva.
La chiamata vera e propria va all'endpoint images/generations, con tutti i valori letti dai controlli al momento del click — nessun parametro è hardcoded, così cambiare modello o dimensione nella UI si riflette immediatamente nella richiesta:
const response = await fetch("https://api.openai.com/v1/images/generations", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + apikey
},
body: JSON.stringify({
prompt: prompt + " MANDATORY: make sure the image content is not cut, and is all " +
"included inside the borders of the image with a little internal padding.",
n: 1,
size: size,
model: model,
moderation: "low",
quality: quality
})
});
const data = await response.json();
if (!response.ok) throw new Error(data?.error?.message || response.statusText);
Il parametro moderation: "low" regola la severità del filtro di contenuto lato OpenAI: un valore utile da esporre come impostazione avanzata in un pannello destinato a un pubblico interno, molto meno in un prodotto pubblico dove il default più prudente è preferibile.
L'API di generazione immagini può restituire il risultato in due formati diversi a seconda del modello e della configurazione: un url temporaneo ospitato da OpenAI, oppure i byte dell'immagine direttamente in b64_json. Il codice deve gestire entrambi i casi, perché non è garantito quale dei due arrivi:
let imageSrc;
if (data?.data?.[0]?.url) {
imageSrc = data.data[0].url; // link temporaneo ospitato da OpenAI
} else if (data?.data?.[0]?.b64_json) {
imageSrc = "data:image/png;base64," + data.data[0].b64_json; // immagine incorporata
} else {
throw new Error("No valid image data received.");
}
url, quel link ha una scadenza (tipicamente breve). Se l'immagine deve essere conservata — non solo mostrata subito nella sessione corrente — va scaricata e salvata lato server non appena arriva, non linkata direttamente a lungo termine.
Con chiamate che possono richiedere diversi secondi, l'interfaccia deve comunicare chiaramente cosa sta succedendo: una bolla con il prompt inviato, un indicatore di caricamento, e infine il risultato (o l'errore) al suo posto:
// 1. Bolla con il prompt dell'utente
const promptDiv = document.createElement("div");
promptDiv.className = "user-message";
promptDiv.textContent = `💬 ${prompt}`;
container.appendChild(promptDiv);
// 2. Indicatore di caricamento, rimosso non appena arriva la risposta
const loader = document.createElement("div");
loader.className = "loading-pill";
loader.innerHTML = `<span class="dot"></span> Generating image… this may take a moment`;
container.appendChild(loader);
container.scrollTop = container.scrollHeight;
// ... chiamata API ...
loader.remove(); // tolto sia in caso di successo che di errore
container.appendChild(card); // card con <img> e link di download
Il pattern è semplice ma efficace: ogni generazione aggiunge due elementi al contenitore (prompt + loader), e il loader viene sempre rimosso — che la chiamata vada a buon fine o fallisca — prima di inserire il risultato finale o un messaggio d'errore. Questo evita loader "fantasma" che restano visibili indefinitamente in caso di eccezione non gestita.
Il prototipo iniziale legge la chiave da localStorage e la spedisce direttamente nell'header Authorization da codice eseguito nel browser dell'utente:
// ⚠️ Codice originale del prototipo: la chiave viene letta dal browser
// e spedita così com'è nell'header Authorization.
const apikey = localStorage.getItem("openaikey");
const response = await fetch("https://api.openai.com/v1/images/generations", {
method: "POST",
headers: { "Authorization": "Bearer " + apikey },
// ...
});
La correzione è la stessa vista per la ricerca web nativa: nessuna chiamata diretta a OpenAI dal browser. Il frontend chiama un endpoint proprio, e solo quell'endpoint — in esecuzione sul server, dove le variabili d'ambiente non sono mai visibili al pubblico — conosce la chiave reale:
// server.js — endpoint proxy minimale (Node.js + Express)
// La chiave vive SOLO qui, come variabile d'ambiente sul server.
app.post("/api/generate-image", async (req, res) => {
const { prompt, model, size, quality } = req.body;
const openaiRes = await fetch("https://api.openai.com/v1/images/generations", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + process.env.OPENAI_API_KEY // non lascia mai il server
},
body: JSON.stringify({ prompt, model, size, quality, n: 1, moderation: "low" })
});
const data = await openaiRes.json();
res.status(openaiRes.status).json(data); // il browser riceve solo il risultato
});
// Nel browser: stessa firma di fetch, ma verso il proprio backend —
// nessuna chiave, nessun header Authorization lato client.
const response = await fetch("/api/generate-image", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt, model, size, quality })
});