Skip to content

Instantly share code, notes, and snippets.

@OmerFarukOruc
Last active February 12, 2026 07:34
Show Gist options
  • Select an option

  • Save OmerFarukOruc/b5eb390098ef95f047667355b4304c17 to your computer and use it in GitHub Desktop.

Select an option

Save OmerFarukOruc/b5eb390098ef95f047667355b4304c17 to your computer and use it in GitHub Desktop.
Gemini Image Studio - Text-to-Image & Image-to-Image with AI prompt enhancement & queue
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Image Studio</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0a0a;
--surface: #141414;
--surface-hover: #1a1a1a;
--border: #2a2a2a;
--border-focus: #5b5bf7;
--text: #e4e4e7;
--text-dim: #71717a;
--accent: #5b5bf7;
--accent-hover: #4a4ae6;
--danger: #ef4444;
--success: #22c55e;
--warning: #f59e0b;
--radius: 12px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 24px;
}
.container { max-width: 960px; margin: 0 auto; }
h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; letter-spacing: -0.5px; }
.subtitle { color: var(--text-dim); font-size: 14px; margin-bottom: 32px; }
.mode-toggle {
display: flex; gap: 4px; background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 4px; margin-bottom: 24px; width: fit-content;
}
.mode-btn {
padding: 10px 24px; border: none; border-radius: 8px; background: transparent;
color: var(--text-dim); font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s;
}
.mode-btn.active { background: var(--accent); color: #fff; }
.mode-btn:hover:not(.active) { color: var(--text); background: var(--surface-hover); }
.panel {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 24px; margin-bottom: 24px;
}
.form-group { margin-bottom: 20px; }
label {
display: block; font-size: 13px; font-weight: 600; color: var(--text-dim);
text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;
}
textarea {
width: 100%; min-height: 80px; padding: 12px 16px; background: var(--bg);
border: 1px solid var(--border); border-radius: 8px; color: var(--text);
font-size: 15px; font-family: inherit; resize: vertical; transition: border-color 0.2s;
}
textarea:focus { outline: none; border-color: var(--border-focus); }
input[type="text"], input[type="password"], select {
width: 100%; padding: 10px 16px; background: var(--bg); border: 1px solid var(--border);
border-radius: 8px; color: var(--text); font-size: 14px; font-family: inherit; transition: border-color 0.2s;
}
input[type="text"]:focus, input[type="password"]:focus, select:focus { outline: none; border-color: var(--border-focus); }
select { cursor: pointer; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.dropzone {
border: 2px dashed var(--border); border-radius: var(--radius); padding: 32px;
text-align: center; cursor: pointer; transition: all 0.2s; position: relative;
min-height: 160px; display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.dropzone:hover, .dropzone.dragover { border-color: var(--accent); background: rgba(91, 91, 247, 0.05); }
.dropzone.has-image { padding: 8px; border-style: solid; border-color: var(--success); }
.dropzone input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.dropzone-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.5; }
.dropzone-text { color: var(--text-dim); font-size: 14px; }
.dropzone-text strong { color: var(--accent); }
.dropzone .preview { max-width: 100%; max-height: 200px; border-radius: 8px; object-fit: contain; }
.dropzone .remove-btn {
position: absolute; top: 8px; right: 8px; width: 28px; height: 28px;
border-radius: 50%; border: none; background: var(--danger); color: white;
font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 2;
}
.btn-row { display: flex; gap: 12px; }
.btn-row > * { flex: 1; }
.generate-btn, .queue-add-btn, .queue-run-btn {
padding: 14px; border: none; border-radius: var(--radius);
font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; letter-spacing: 0.3px;
}
.generate-btn { background: var(--accent); color: #fff; }
.generate-btn:hover:not(:disabled) { background: var(--accent-hover); transform: translateY(-1px); }
.generate-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.queue-add-btn { background: transparent; color: var(--text); border: 1px solid var(--border); }
.queue-add-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.queue-run-btn { background: var(--success); color: #fff; }
.queue-run-btn:hover:not(:disabled) { opacity: 0.9; }
.queue-run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.enhance-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.toggle { position: relative; width: 44px; height: 24px; flex-shrink: 0; }
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute; inset: 0; background: var(--border); border-radius: 12px; cursor: pointer; transition: 0.2s;
}
.toggle-slider::before {
content: ''; position: absolute; width: 18px; height: 18px;
left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: 0.2s;
}
.toggle input:checked + .toggle-slider { background: var(--accent); }
.toggle input:checked + .toggle-slider::before { transform: translateX(20px); }
.enhance-label {
font-size: 14px; font-weight: 500; color: var(--text); text-transform: none;
letter-spacing: 0; margin-bottom: 0; cursor: pointer;
}
.enhance-model-select { width: auto; padding: 6px 12px; font-size: 13px; flex-shrink: 0; }
.enhanced-preview {
background: var(--bg); border: 1px solid var(--warning); border-radius: 8px;
padding: 12px 16px; margin-bottom: 16px; display: none;
}
.enhanced-preview.visible { display: block; }
.enhanced-preview-header {
font-size: 11px; font-weight: 700; color: var(--warning);
text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px;
}
.enhanced-preview-text { font-size: 14px; color: var(--text); line-height: 1.5; white-space: pre-wrap; }
.status { text-align: center; padding: 16px; font-size: 14px; color: var(--text-dim); display: none; }
.status.visible { display: block; }
.status.error { color: var(--danger); }
.spinner {
display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border);
border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite;
margin-right: 8px; vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
.result-panel { display: none; }
.result-panel.visible { display: block; }
.result-image { width: 100%; border-radius: var(--radius); border: 1px solid var(--border); }
.result-actions { display: flex; gap: 12px; margin-top: 16px; }
.result-actions button {
flex: 1; padding: 10px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s;
}
.download-btn { background: var(--success); color: #fff; border: none; }
.download-btn:hover { opacity: 0.9; }
.copy-btn { background: transparent; color: var(--text); border: 1px solid var(--border); }
.copy-btn:hover { border-color: var(--text-dim); }
.settings-row { display: flex; gap: 12px; align-items: end; flex-wrap: wrap; }
.settings-row .form-group { flex: 1; margin-bottom: 0; min-width: 140px; }
.hidden { display: none !important; }
.result-meta { font-size: 12px; color: var(--text-dim); margin-top: 8px; text-align: right; }
.queue-panel { margin-bottom: 24px; }
.queue-panel h3 { font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.queue-badge {
background: var(--accent); color: #fff; font-size: 11px; font-weight: 700;
padding: 2px 8px; border-radius: 10px; min-width: 20px; text-align: center;
}
.queue-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
.queue-item {
display: flex; align-items: center; gap: 12px; background: var(--bg);
border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px;
font-size: 13px; transition: border-color 0.2s;
}
.queue-item.running { border-color: var(--accent); }
.queue-item.done { border-color: var(--success); opacity: 0.7; }
.queue-item.failed { border-color: var(--danger); opacity: 0.7; }
.queue-item-num { font-weight: 700; color: var(--text-dim); font-size: 12px; width: 24px; text-align: center; flex-shrink: 0; }
.queue-item-info { flex: 1; min-width: 0; }
.queue-item-prompt { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); margin-bottom: 2px; }
.queue-item-meta { font-size: 11px; color: var(--text-dim); }
.queue-item-status { flex-shrink: 0; font-size: 12px; font-weight: 600; }
.queue-item-status.pending { color: var(--text-dim); }
.queue-item-status.running { color: var(--accent); }
.queue-item-status.done { color: var(--success); }
.queue-item-status.failed { color: var(--danger); }
.queue-item-remove {
flex-shrink: 0; width: 24px; height: 24px; border-radius: 50%; border: none;
background: transparent; color: var(--text-dim); font-size: 16px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.queue-item-remove:hover { color: var(--danger); background: rgba(239,68,68,0.1); }
.queue-actions { display: flex; gap: 12px; }
.queue-clear-btn {
padding: 10px 20px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer;
background: transparent; color: var(--text-dim); border: 1px solid var(--border); transition: all 0.2s;
}
.queue-clear-btn:hover { border-color: var(--danger); color: var(--danger); }
.results-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; }
.gallery-item {
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
overflow: hidden; cursor: pointer; transition: transform 0.2s;
}
.gallery-item:hover { transform: translateY(-2px); }
.gallery-item img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }
.gallery-item-info { padding: 10px 12px; font-size: 12px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lightbox {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.9);
z-index: 100; align-items: center; justify-content: center; padding: 40px;
}
.lightbox.visible { display: flex; }
.lightbox img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 8px; }
.lightbox-close {
position: absolute; top: 20px; right: 20px; width: 40px; height: 40px; border-radius: 50%;
border: none; background: rgba(255,255,255,0.1); color: #fff; font-size: 24px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.lightbox-actions {
position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); display: flex; gap: 12px;
}
.lightbox-actions button {
padding: 10px 24px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; border: none;
}
.api-key-row { display: flex; gap: 8px; align-items: end; }
.api-key-row .form-group { flex: 1; margin-bottom: 0; }
.toggle-vis-btn {
padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px;
background: var(--bg); color: var(--text-dim); cursor: pointer; font-size: 14px;
transition: border-color 0.2s; flex-shrink: 0; height: 40px;
}
.toggle-vis-btn:hover { border-color: var(--accent); color: var(--text); }
.save-indicator {
font-size: 11px; color: var(--success); opacity: 0; transition: opacity 0.3s;
margin-left: 8px;
}
.save-indicator.visible { opacity: 1; }
.info-box {
background: rgba(91, 91, 247, 0.08); border: 1px solid rgba(91, 91, 247, 0.2);
border-radius: 8px; padding: 12px 16px; margin-bottom: 20px; font-size: 13px;
color: var(--text-dim); line-height: 1.5;
}
.info-box strong { color: var(--text); }
.i2i-settings-row { display: flex; gap: 12px; align-items: end; flex-wrap: wrap; margin-bottom: 20px; }
.i2i-settings-row .form-group { flex: 1; margin-bottom: 0; min-width: 120px; }
</style>
</head>
<body>
<div class="container">
<h1>Gemini Image Studio</h1>
<p class="subtitle">Text-to-Image & Image-to-Image with AI prompt enhancement & queue</p>
<div class="mode-toggle">
<button class="mode-btn active" data-mode="t2i">Text to Image</button>
<button class="mode-btn" data-mode="i2i">Image to Image</button>
</div>
<div class="panel">
<div class="info-box">
<strong>Setup:</strong> Enter your OpenAI-compatible API base URL and API key below.
Your credentials are stored only in your browser's local storage and never sent anywhere except the API endpoint you configure.
</div>
<div class="form-group">
<label>API Base URL</label>
<input type="text" id="apiBase" placeholder="http://localhost:8045/v1" />
</div>
<div class="form-group">
<label>API Key <span class="save-indicator" id="saveIndicator">Saved</span></label>
<div class="api-key-row">
<div class="form-group">
<input type="password" id="apiKey" placeholder="Enter your API key" />
</div>
<button class="toggle-vis-btn" id="toggleKeyVis" title="Show/hide API key">&#128065;</button>
</div>
</div>
<div class="settings-row form-group">
<div class="form-group">
<label>Model</label>
<input type="text" id="model" value="gemini-3-pro-image" placeholder="gemini-3-pro-image" />
</div>
<div class="form-group" id="aspectGroup">
<label>Aspect Ratio</label>
<select id="aspectRatio">
<option value="1:1">1:1 &mdash; Square</option>
<option value="16:9">16:9 &mdash; Widescreen</option>
<option value="9:16">9:16 &mdash; Mobile / Vertical</option>
<option value="4:3">4:3 &mdash; Traditional</option>
<option value="3:4">3:4 &mdash; Portrait</option>
<option value="3:2">3:2 &mdash; DSLR</option>
<option value="2:3">2:3 &mdash; Portrait Photo</option>
<option value="21:9">21:9 &mdash; Ultra-wide</option>
<option value="5:4">5:4 &mdash; Large Format</option>
<option value="4:5">4:5 &mdash; Social Media</option>
</select>
</div>
<div class="form-group" id="qualityGroup">
<label>Quality</label>
<select id="quality">
<option value="standard">Standard (1K)</option>
<option value="medium">Medium (2K)</option>
<option value="hd">HD (4K)</option>
</select>
</div>
</div>
<div id="imageInputs" class="hidden">
<div class="row form-group">
<div>
<label>Image 1</label>
<div class="dropzone" id="drop1">
<input type="file" accept="image/*" />
<div class="dropzone-icon">&#128444;</div>
<p class="dropzone-text">Drop image or <strong>browse</strong></p>
</div>
</div>
<div>
<label>Image 2 (optional)</label>
<div class="dropzone" id="drop2">
<input type="file" accept="image/*" />
<div class="dropzone-icon">&#128444;</div>
<p class="dropzone-text">Drop image or <strong>browse</strong></p>
</div>
</div>
</div>
<div class="i2i-settings-row" id="i2iSettings">
<div class="form-group">
<label>Aspect Ratio (override)</label>
<select id="i2iAspectRatio">
<option value="">Auto (from image)</option>
<option value="1:1">1:1 &mdash; Square</option>
<option value="16:9">16:9 &mdash; Widescreen</option>
<option value="9:16">9:16 &mdash; Vertical</option>
<option value="4:3">4:3 &mdash; Traditional</option>
<option value="3:4">3:4 &mdash; Portrait</option>
<option value="3:2">3:2 &mdash; DSLR</option>
<option value="2:3">2:3 &mdash; Portrait Photo</option>
<option value="21:9">21:9 &mdash; Ultra-wide</option>
<option value="5:4">5:4 &mdash; Large Format</option>
<option value="4:5">4:5 &mdash; Social Media</option>
</select>
</div>
<div class="form-group">
<label>Quality</label>
<select id="i2iQuality">
<option value="standard">Standard (1K)</option>
<option value="medium">Medium (2K)</option>
<option value="hd">HD (4K)</option>
</select>
</div>
<div class="form-group">
<label>Style (optional)</label>
<input type="text" id="i2iStyle" placeholder="e.g. watercolor, cyberpunk" />
</div>
</div>
</div>
<div class="form-group">
<label>Prompt</label>
<textarea id="prompt" placeholder="Describe the image you want to generate..."></textarea>
</div>
<div class="enhance-row">
<label class="toggle">
<input type="checkbox" id="enhanceToggle" checked />
<span class="toggle-slider"></span>
</label>
<span class="enhance-label" onclick="document.getElementById('enhanceToggle').click()">Enhance prompt with AI</span>
<select id="enhanceModel" class="enhance-model-select">
<option value="gemini-3-flash-preview">Gemini 3 Flash</option>
<option value="gemini-2.5-flash">Gemini 2.5 Flash</option>
<option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
</select>
</div>
<div class="enhanced-preview" id="enhancedPreview">
<div class="enhanced-preview-header">Enhanced Prompt</div>
<div class="enhanced-preview-text" id="enhancedText"></div>
</div>
<div class="btn-row">
<button class="generate-btn" id="generateBtn">Generate</button>
<button class="queue-add-btn" id="queueAddBtn">+ Add to Queue</button>
</div>
</div>
<div class="status" id="status"></div>
<div class="panel result-panel" id="resultPanel">
<img class="result-image" id="resultImage" />
<div class="result-meta" id="resultMeta"></div>
<div class="result-actions">
<button class="download-btn" id="downloadBtn">Download</button>
<button class="copy-btn" id="copyBtn">Copy Base64</button>
</div>
</div>
<div class="panel queue-panel hidden" id="queuePanel">
<h3>Queue <span class="queue-badge" id="queueBadge">0</span></h3>
<div class="queue-list" id="queueList"></div>
<div class="queue-actions">
<button class="queue-run-btn" id="queueRunBtn">Run Queue</button>
<button class="queue-clear-btn" id="queueClearBtn">Clear</button>
</div>
</div>
<div class="panel hidden" id="galleryPanel">
<h3 style="font-size:14px;font-weight:600;margin-bottom:16px;">Results</h3>
<div class="results-gallery" id="resultsGallery"></div>
</div>
</div>
<div class="lightbox" id="lightbox">
<button class="lightbox-close" onclick="closeLightbox()">&times;</button>
<img id="lightboxImg" />
<div class="lightbox-actions">
<button class="download-btn" id="lightboxDownload">Download</button>
<button class="copy-btn" id="lightboxCopy">Copy Base64</button>
</div>
</div>
<script>
const STORAGE_KEY = 'gemini-studio-settings';
let currentMode = 't2i';
let images = [null, null];
let imageFiles = [null, null];
let lastResultB64 = null;
let queue = [];
let queueRunning = false;
const $ = (s) => document.querySelector(s);
const $$ = (s) => document.querySelectorAll(s);
async function safeParseJSON(res) {
const text = await res.text();
try {
return JSON.parse(text);
} catch (_) {
throw new Error(text || ('HTTP ' + res.status));
}
}
// Persist settings in localStorage
function loadSettings() {
try {
var saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
if (saved.apiBase) $('#apiBase').value = saved.apiBase;
if (saved.apiKey) $('#apiKey').value = saved.apiKey;
if (saved.model) $('#model').value = saved.model;
if (saved.aspectRatio) $('#aspectRatio').value = saved.aspectRatio;
if (saved.quality) $('#quality').value = saved.quality;
if (saved.enhanceModel) $('#enhanceModel').value = saved.enhanceModel;
if (saved.enhanceEnabled !== undefined) $('#enhanceToggle').checked = saved.enhanceEnabled;
} catch (_) {}
}
function saveSettings() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
apiBase: $('#apiBase').value,
apiKey: $('#apiKey').value,
model: $('#model').value,
aspectRatio: $('#aspectRatio').value,
quality: $('#quality').value,
enhanceModel: $('#enhanceModel').value,
enhanceEnabled: $('#enhanceToggle').checked
}));
var indicator = $('#saveIndicator');
indicator.classList.add('visible');
setTimeout(function() { indicator.classList.remove('visible'); }, 1500);
} catch (_) {}
}
loadSettings();
// Auto-save on change
['apiBase', 'apiKey', 'model', 'aspectRatio', 'quality', 'enhanceModel'].forEach(function(id) {
$('#' + id).addEventListener('change', saveSettings);
$('#' + id).addEventListener('input', saveSettings);
});
$('#enhanceToggle').addEventListener('change', saveSettings);
// Toggle API key visibility
$('#toggleKeyVis').addEventListener('click', function() {
var input = $('#apiKey');
input.type = input.type === 'password' ? 'text' : 'password';
});
function getApiBase() {
var base = $('#apiBase').value.trim();
if (!base) throw new Error('API Base URL is required. Enter your OpenAI-compatible API endpoint.');
return base.replace(/\/+$/, '');
}
const ENHANCE_SYSTEM_PROMPTS = {
pro: `You are an expert image generation prompt engineer for Gemini Pro Image models.
Transform the user's basic prompt into a highly detailed, vivid prompt optimized for Gemini Pro image generation.
Rules:
- Output ONLY the enhanced prompt text. No explanations, no labels, no markdown.
- Keep the user's core intent and subject intact.
- Add rich visual details: lighting, composition, camera angle, color palette, atmosphere, textures, materials.
- Specify art style or photographic style if not provided (cinematic photography, digital art, oil painting, etc).
- Add depth: foreground, midground, background elements where appropriate.
- Include quality boosters: highly detailed, professional, 8K resolution, masterful composition.
- Pro models handle complexity well — add multiple visual layers and nuanced descriptions.
- Keep under 300 words.
- No negative prompts, only positive description.`,
flash: `You are an image generation prompt engineer for Gemini Flash Image models.
Transform the user's basic prompt into a clear, effective prompt optimized for Flash image generation.
Rules:
- Output ONLY the enhanced prompt text. No explanations, no labels, no markdown.
- Keep the user's core intent and subject intact.
- Be concise but descriptive — Flash models work best with clear, focused prompts.
- Add key visual details: main subject, style, lighting, and color mood.
- Avoid overly complex multi-layered scenes — keep composition focused.
- Include a clear art/photo style reference.
- Keep under 150 words.
- No negative prompts, only positive description.`,
nano: `You are an image generation prompt engineer for Gemini Nano Image models.
Transform the user's basic prompt into a simple, direct prompt optimized for Nano image generation.
Rules:
- Output ONLY the enhanced prompt text. No explanations, no labels, no markdown.
- Keep the user's core intent and subject intact.
- Be SHORT and DIRECT — Nano models work best with simple, unambiguous prompts.
- Focus on: main subject, one clear style, basic mood/lighting.
- Avoid complex compositions, multiple subjects, or layered descriptions.
- One sentence for subject, one for style.
- Keep under 80 words.
- No negative prompts.`
};
function getModelTier(modelName) {
if (modelName.includes('nano')) return 'nano';
if (modelName.includes('flash')) return 'flash';
return 'pro';
}
// Mode toggle
$$('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => {
$$('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentMode = btn.dataset.mode;
$('#imageInputs').classList.toggle('hidden', currentMode === 't2i');
$('#aspectGroup').classList.toggle('hidden', currentMode === 'i2i');
$('#qualityGroup').classList.toggle('hidden', currentMode === 'i2i');
});
});
// Dropzone
function setupDropzone(dropEl, index) {
const fileInput = dropEl.querySelector('input[type="file"]');
['dragenter', 'dragover'].forEach(e =>
dropEl.addEventListener(e, (ev) => { ev.preventDefault(); dropEl.classList.add('dragover'); })
);
['dragleave', 'drop'].forEach(e =>
dropEl.addEventListener(e, () => dropEl.classList.remove('dragover'))
);
dropEl.addEventListener('drop', (ev) => {
ev.preventDefault();
if (ev.dataTransfer.files.length) loadFile(ev.dataTransfer.files[0], dropEl, index);
});
fileInput.addEventListener('change', (ev) => {
if (ev.target.files.length) loadFile(ev.target.files[0], dropEl, index);
});
}
function loadFile(file, dropEl, index) {
imageFiles[index] = file;
const reader = new FileReader();
reader.onload = (e) => {
const b64 = e.target.result;
images[index] = b64;
dropEl.classList.add('has-image');
dropEl.innerHTML =
'<button class="remove-btn" onclick="removeImage(' + index + ', this.parentElement)">&times;</button>' +
'<img class="preview" src="' + b64 + '" />' +
'<input type="file" accept="image/*" />';
setupDropzone(dropEl, index);
};
reader.readAsDataURL(file);
}
window.removeImage = function(index, dropEl) {
images[index] = null;
imageFiles[index] = null;
dropEl.classList.remove('has-image');
dropEl.innerHTML =
'<input type="file" accept="image/*" />' +
'<div class="dropzone-icon">&#128444;</div>' +
'<p class="dropzone-text">Drop image or <strong>browse</strong></p>';
setupDropzone(dropEl, index);
};
setupDropzone($('#drop1'), 0);
setupDropzone($('#drop2'), 1);
// Status
function showStatus(msg, isError) {
const el = $('#status');
el.className = 'status visible' + (isError ? ' error' : '');
el.innerHTML = isError ? msg : '<span class="spinner"></span>' + msg;
}
function hideStatus() { $('#status').className = 'status'; }
// Prompt Enhancement
async function enhancePrompt(userPrompt, imageModel, imgs) {
const apiBase = getApiBase();
const tier = getModelTier(imageModel);
const systemPrompt = ENHANCE_SYSTEM_PROMPTS[tier];
const enhanceModel = $('#enhanceModel').value;
var userContent;
var hasImages = imgs && imgs.filter(Boolean).length > 0;
if (hasImages) {
userContent = [];
for (var i = 0; i < imgs.length; i++) {
if (imgs[i]) userContent.push({ type: 'image_url', image_url: { url: imgs[i] } });
}
userContent.push({ type: 'text', text: userPrompt });
} else {
userContent = userPrompt;
}
const res = await fetch(apiBase + '/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + $('#apiKey').value
},
body: JSON.stringify({
model: enhanceModel,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userContent }
],
temperature: 0.7,
max_tokens: 1024
})
});
const data = await safeParseJSON(res);
if (data.error) throw new Error('Enhancement failed: ' + (data.error.message || JSON.stringify(data.error)));
const enhanced = data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content;
if (!enhanced) throw new Error('Empty enhancement response');
return enhanced.trim();
}
// Text-to-Image: POST /v1/images/generations (JSON)
async function generateTextToImage(prompt, model, aspectRatio, quality) {
const apiBase = getApiBase();
const res = await fetch(apiBase + '/images/generations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + $('#apiKey').value
},
body: JSON.stringify({
model: model,
prompt: prompt,
n: 1,
response_format: 'b64_json',
size: aspectRatio,
quality: quality
})
});
const data = await safeParseJSON(res);
if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
if (!data.data || !data.data[0] || !data.data[0].b64_json) throw new Error('No image in response');
return data.data[0].b64_json;
}
// Image-to-Image: POST /v1/images/edits (multipart/form-data)
async function generateImageToImage(prompt, model, files, aspectRatio, quality, style) {
const apiBase = getApiBase();
const formData = new FormData();
formData.append('prompt', prompt);
formData.append('model', model);
formData.append('n', '1');
formData.append('response_format', 'b64_json');
// Append image files
for (var i = 0; i < files.length; i++) {
if (files[i]) {
formData.append('image' + (i + 1), files[i], files[i].name);
}
}
// Also send first image as "image" for OpenAI compat
if (files[0]) {
formData.append('image', files[0], files[0].name);
}
if (aspectRatio) formData.append('aspect_ratio', aspectRatio);
if (quality) formData.append('image_size', quality === 'hd' ? '4K' : quality === 'medium' ? '2K' : '1K');
if (style) formData.append('style', style);
const res = await fetch(apiBase + '/images/edits', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + $('#apiKey').value
},
body: formData
});
const data = await res.json();
if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
if (data.data && data.data[0] && data.data[0].b64_json) {
return data.data[0].b64_json;
}
// Fallback: try extracting from choices (chat completions compat)
var rc = data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content;
if (rc) {
var match = rc.match(/data:image\/[a-z]+;base64,([A-Za-z0-9+\/=]+)/);
if (match) return match[1];
}
throw new Error('No image found in response');
}
// Full pipeline
async function runGeneration(job) {
var finalPrompt = job.prompt;
if (job.enhance) {
showStatus('Enhancing prompt with ' + $('#enhanceModel').value + '...');
finalPrompt = await enhancePrompt(job.prompt, job.model, job.mode === 'i2i' ? job.imgs : null);
$('#enhancedText').textContent = finalPrompt;
$('#enhancedPreview').classList.add('visible');
}
showStatus(job.mode === 't2i' ? 'Generating image...' : 'Generating from images...');
if (job.mode === 't2i') {
return await generateTextToImage(finalPrompt, job.model, job.aspectRatio, job.quality);
} else {
return await generateImageToImage(finalPrompt, job.model, job.files, job.i2iAspectRatio, job.i2iQuality, job.i2iStyle);
}
}
function getCurrentJob() {
var prompt = $('#prompt').value.trim();
if (!prompt) throw new Error('Prompt is required');
if (!$('#apiKey').value.trim()) throw new Error('API Key is required');
getApiBase();
if (currentMode === 'i2i' && !imageFiles[0]) throw new Error('At least one image is required');
return {
prompt: prompt,
model: $('#model').value,
aspectRatio: $('#aspectRatio').value,
quality: $('#quality').value,
mode: currentMode,
imgs: [images[0], images[1]],
files: [imageFiles[0], imageFiles[1]],
i2iAspectRatio: $('#i2iAspectRatio').value,
i2iQuality: $('#i2iQuality').value,
i2iStyle: $('#i2iStyle').value.trim(),
enhance: $('#enhanceToggle').checked
};
}
// Single generate
function showResult(b64Data) {
lastResultB64 = b64Data;
$('#resultImage').src = 'data:image/png;base64,' + b64Data;
$('#resultPanel').classList.add('visible');
var sizeKB = Math.round((b64Data.length * 3) / 4 / 1024);
$('#resultMeta').textContent = '~' + sizeKB + ' KB';
$('#resultImage').onload = function() {
$('#resultMeta').textContent = this.naturalWidth + 'x' + this.naturalHeight + ' · ~' + sizeKB + ' KB';
};
}
$('#generateBtn').addEventListener('click', async () => {
var btn = $('#generateBtn');
btn.disabled = true;
btn.textContent = 'Generating...';
$('#resultPanel').classList.remove('visible');
$('#enhancedPreview').classList.remove('visible');
try {
var job = getCurrentJob();
var b64 = await runGeneration(job);
hideStatus();
showResult(b64);
} catch (err) {
showStatus(err.message, true);
} finally {
btn.disabled = false;
btn.textContent = 'Generate';
}
});
$('#downloadBtn').addEventListener('click', () => { if (lastResultB64) downloadB64(lastResultB64); });
$('#copyBtn').addEventListener('click', () => { if (lastResultB64) copyB64(lastResultB64, $('#copyBtn')); });
function downloadB64(b64) {
var a = document.createElement('a');
a.href = 'data:image/png;base64,' + b64;
a.download = 'gemini-' + Date.now() + '.png';
a.click();
}
function copyB64(b64, btn) {
navigator.clipboard.writeText(b64).then(() => {
var orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = orig; }, 2000);
});
}
// Queue
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
$('#queueAddBtn').addEventListener('click', () => {
try {
var job = getCurrentJob();
job.id = Date.now() + Math.random();
job.status = 'pending';
queue.push(job);
renderQueue();
} catch (err) {
showStatus(err.message, true);
}
});
function renderQueue() {
var panel = $('#queuePanel');
var list = $('#queueList');
if (queue.length === 0) { panel.classList.add('hidden'); return; }
panel.classList.remove('hidden');
$('#queueBadge').textContent = queue.length;
var html = '';
for (var i = 0; i < queue.length; i++) {
var job = queue[i];
var statusLabel = job.status === 'pending' ? 'Pending' : job.status === 'running' ? 'Running...' : job.status === 'done' ? 'Done' : 'Failed';
var modelShort = job.model.includes('pro') ? 'Pro' : 'Flash';
var removeBtn = job.status === 'pending' ? '<button class="queue-item-remove" onclick="removeQueueItem(' + i + ')">&times;</button>' : '';
var metaStr = job.mode.toUpperCase() + ' · ' + modelShort + ' · ' + (job.enhance ? 'Enhanced' : 'Raw');
if (job.mode === 't2i') {
metaStr += ' · ' + job.aspectRatio + ' · ' + job.quality;
}
html += '<div class="queue-item ' + job.status + '">' +
'<span class="queue-item-num">#' + (i + 1) + '</span>' +
'<div class="queue-item-info">' +
'<div class="queue-item-prompt">' + escHtml(job.prompt) + '</div>' +
'<div class="queue-item-meta">' + metaStr + '</div>' +
'</div>' +
'<span class="queue-item-status ' + job.status + '">' + statusLabel + '</span>' +
removeBtn +
'</div>';
}
list.innerHTML = html;
}
window.removeQueueItem = function(i) {
if (queue[i] && queue[i].status === 'pending') {
queue.splice(i, 1);
renderQueue();
}
};
$('#queueClearBtn').addEventListener('click', () => {
queue = queue.filter(j => j.status === 'running');
renderQueue();
if (queue.length === 0) $('#queuePanel').classList.add('hidden');
});
$('#queueRunBtn').addEventListener('click', runQueue);
async function runQueue() {
if (queueRunning) return;
queueRunning = true;
$('#queueRunBtn').disabled = true;
$('#queueAddBtn').disabled = true;
$('#generateBtn').disabled = true;
var gallery = $('#resultsGallery');
$('#galleryPanel').classList.remove('hidden');
for (var i = 0; i < queue.length; i++) {
var job = queue[i];
if (job.status !== 'pending') continue;
job.status = 'running';
renderQueue();
try {
showStatus('Queue ' + (i + 1) + '/' + queue.length + ': ' + (job.enhance ? 'Enhancing & generating...' : 'Generating...'));
var b64 = await runGeneration(job);
job.status = 'done';
job.result = b64;
var item = document.createElement('div');
item.className = 'gallery-item';
item.innerHTML = '<img src="data:image/png;base64,' + b64 + '" />' +
'<div class="gallery-item-info">' + escHtml(job.prompt.substring(0, 60)) + '</div>';
(function(capturedB64) {
item.addEventListener('click', function() { openLightbox(capturedB64); });
})(b64);
gallery.appendChild(item);
} catch (err) {
job.status = 'failed';
job.error = err.message;
}
renderQueue();
}
hideStatus();
queueRunning = false;
$('#queueRunBtn').disabled = false;
$('#queueAddBtn').disabled = false;
$('#generateBtn').disabled = false;
}
// Lightbox
var lightboxB64 = null;
function openLightbox(b64) {
lightboxB64 = b64;
$('#lightboxImg').src = 'data:image/png;base64,' + b64;
$('#lightbox').classList.add('visible');
}
window.closeLightbox = function() { $('#lightbox').classList.remove('visible'); };
$('#lightbox').addEventListener('click', function(e) {
if (e.target === $('#lightbox')) closeLightbox();
});
$('#lightboxDownload').addEventListener('click', () => { if (lightboxB64) downloadB64(lightboxB64); });
$('#lightboxCopy').addEventListener('click', () => { if (lightboxB64) copyB64(lightboxB64, $('#lightboxCopy')); });
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeLightbox(); });
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment