mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 02:12:12 +00:00
Fix: Inline all JS, DOM-safe init, defensive loading
This commit is contained in:
403
index.html
403
index.html
@@ -4,13 +4,24 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>vibegif.lol — AI Generated Gifs</title>
|
<title>vibegif.lol — AI Generated Gifs</title>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: "Stain";
|
||||||
|
src: url("https://cdn.jsdelivr.net/gh/multipleof4/stain.otf@master/dist/Stain.otf") format("opentype");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
body, input, select, button, a {
|
||||||
|
font-family: "Stain", sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js"></script>
|
|
||||||
<link rel="stylesheet" href="/styles.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white text-neutral-800 min-h-screen flex flex-col items-center">
|
<body class="bg-white text-neutral-800 min-h-screen flex flex-col items-center">
|
||||||
|
|
||||||
<div id="app" class="w-full max-w-xl mx-auto px-4 py-8 flex flex-col items-center gap-6">
|
<div id="app" class="w-full max-w-xl mx-auto px-4 py-8 flex flex-col items-center gap-6">
|
||||||
|
|
||||||
|
<!-- Header (always visible) -->
|
||||||
<header class="w-full flex items-center justify-between">
|
<header class="w-full flex items-center justify-between">
|
||||||
<h1 class="text-4xl tracking-tight">vibegif<span class="text-neutral-400">.lol</span></h1>
|
<h1 class="text-4xl tracking-tight">vibegif<span class="text-neutral-400">.lol</span></h1>
|
||||||
<button id="btn-settings" class="p-2 rounded-lg hover:bg-neutral-100 transition" title="Settings">
|
<button id="btn-settings" class="p-2 rounded-lg hover:bg-neutral-100 transition" title="Settings">
|
||||||
@@ -18,12 +29,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="setup-screen" class="w-full flex flex-col items-center gap-4 mt-12 hidden">
|
<!-- Setup Screen (visible by default so user sees SOMETHING) -->
|
||||||
|
<div id="setup-screen" class="w-full flex flex-col items-center gap-4 mt-12">
|
||||||
<p class="text-neutral-500 text-center text-lg">enter your <a href="https://openrouter.ai/keys" target="_blank" class="underline hover:text-neutral-800">OpenRouter</a> API key to start</p>
|
<p class="text-neutral-500 text-center text-lg">enter your <a href="https://openrouter.ai/keys" target="_blank" class="underline hover:text-neutral-800">OpenRouter</a> API key to start</p>
|
||||||
<input id="setup-key" type="password" placeholder="sk-or-..." class="w-full border border-neutral-300 rounded-lg px-4 py-3 text-center focus:outline-none focus:ring-2 focus:ring-neutral-400">
|
<input id="setup-key" type="password" placeholder="sk-or-..." class="w-full border border-neutral-300 rounded-lg px-4 py-3 text-center focus:outline-none focus:ring-2 focus:ring-neutral-400">
|
||||||
<button id="setup-save" class="bg-neutral-800 text-white rounded-lg px-6 py-3 hover:bg-neutral-700 transition">save & start vibing</button>
|
<button id="setup-save" class="bg-neutral-800 text-white rounded-lg px-6 py-3 hover:bg-neutral-700 transition">save & start vibing</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Screen -->
|
||||||
<div id="main-screen" class="w-full flex flex-col gap-5 hidden">
|
<div id="main-screen" class="w-full flex flex-col gap-5 hidden">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-xs text-neutral-400 uppercase tracking-wider">model</label>
|
<label class="text-xs text-neutral-400 uppercase tracking-wider">model</label>
|
||||||
@@ -54,8 +67,8 @@
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-xs text-neutral-400 uppercase tracking-wider">size</label>
|
<label class="text-xs text-neutral-400 uppercase tracking-wider">size</label>
|
||||||
<select id="sel-size" class="border border-neutral-300 rounded-lg px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-neutral-400">
|
<select id="sel-size" class="border border-neutral-300 rounded-lg px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-neutral-400">
|
||||||
<option value="1K">1K</option>
|
<option value="1024x1024">1K</option>
|
||||||
<option value="0.5K">0.5K (Gemini only)</option>
|
<option value="512x512">0.5K (Gemini only)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@@ -85,7 +98,7 @@
|
|||||||
|
|
||||||
<div id="result-area" class="w-full hidden flex-col items-center gap-4">
|
<div id="result-area" class="w-full hidden flex-col items-center gap-4">
|
||||||
<img id="result-gif" class="rounded-xl border border-neutral-200 max-w-full">
|
<img id="result-gif" class="rounded-xl border border-neutral-200 max-w-full">
|
||||||
<a id="btn-download" download="vibegif.gif" class="bg-neutral-800 text-white rounded-lg px-6 py-3 hover:bg-neutral-700 transition flex items-center gap-2 cursor-pointer">
|
<a id="btn-download" download="vibegif.gif" class="bg-neutral-800 text-white rounded-lg px-6 py-3 hover:bg-neutral-700 transition flex items-center gap-2 cursor-pointer no-underline">
|
||||||
<i data-lucide="download" class="w-5 h-5"></i>
|
<i data-lucide="download" class="w-5 h-5"></i>
|
||||||
download gif
|
download gif
|
||||||
</a>
|
</a>
|
||||||
@@ -110,6 +123,380 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/app.js"></script>
|
<!-- Lucide loaded BEFORE our script, with fallback -->
|
||||||
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ============ STORAGE ============ */
|
||||||
|
var STORAGE_KEY = 'vibegif_api_key';
|
||||||
|
function getApiKey() { return localStorage.getItem(STORAGE_KEY) || ''; }
|
||||||
|
function setApiKey(k) { localStorage.setItem(STORAGE_KEY, (k || '').trim()); }
|
||||||
|
function hasApiKey() { return !!getApiKey(); }
|
||||||
|
|
||||||
|
/* ============ DOM REFS (grabbed once on DOMContentLoaded) ============ */
|
||||||
|
var el = {};
|
||||||
|
|
||||||
|
function grabElements() {
|
||||||
|
var ids = [
|
||||||
|
'setup-screen','main-screen','setup-key','setup-save',
|
||||||
|
'btn-settings','modal-settings','modal-key','modal-save','btn-close-modal',
|
||||||
|
'sel-model','inp-prompt','inp-frames','inp-fps','sel-size','sel-ratio',
|
||||||
|
'btn-generate','progress-area','progress-bar','progress-text','frames-preview',
|
||||||
|
'result-area','result-gif','btn-download'
|
||||||
|
];
|
||||||
|
for (var i = 0; i < ids.length; i++) {
|
||||||
|
var camel = ids[i].replace(/-([a-z])/g, function(_, c) { return c.toUpperCase(); });
|
||||||
|
el[camel] = document.getElementById(ids[i]);
|
||||||
|
if (!el[camel]) console.warn('vibegif: missing element #' + ids[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ UI HELPERS ============ */
|
||||||
|
function show(node) { if (node) { node.classList.remove('hidden'); node.style.display = ''; } }
|
||||||
|
function hide(node) { if (node) node.classList.add('hidden'); }
|
||||||
|
|
||||||
|
function showSetup() { show(el.setupScreen); hide(el.mainScreen); }
|
||||||
|
function showMain() { hide(el.setupScreen); show(el.mainScreen); }
|
||||||
|
|
||||||
|
function showModal() {
|
||||||
|
if (el.modalKey) el.modalKey.value = getApiKey();
|
||||||
|
show(el.modalSettings);
|
||||||
|
}
|
||||||
|
function hideModal() { hide(el.modalSettings); }
|
||||||
|
|
||||||
|
function setProgress(pct, text) {
|
||||||
|
show(el.progressArea);
|
||||||
|
if (el.progressBar) el.progressBar.style.width = pct + '%';
|
||||||
|
if (el.progressText) el.progressText.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFramePreview(base64, index) {
|
||||||
|
if (!el.framesPreview) return;
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = base64;
|
||||||
|
img.className = 'w-16 h-16 rounded-lg border border-neutral-200 object-cover';
|
||||||
|
img.title = 'frame ' + (index + 1);
|
||||||
|
el.framesPreview.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetProgress() {
|
||||||
|
hide(el.progressArea);
|
||||||
|
if (el.progressBar) el.progressBar.style.width = '0%';
|
||||||
|
if (el.progressText) el.progressText.textContent = '';
|
||||||
|
if (el.framesPreview) el.framesPreview.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResult(blobUrl) {
|
||||||
|
if (el.resultArea) { el.resultArea.classList.remove('hidden'); el.resultArea.style.display = 'flex'; }
|
||||||
|
if (el.resultGif) el.resultGif.src = blobUrl;
|
||||||
|
if (el.btnDownload) el.btnDownload.href = blobUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideResult() {
|
||||||
|
if (el.resultArea) { el.resultArea.classList.add('hidden'); el.resultArea.style.display = ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGenerating(active) {
|
||||||
|
if (!el.btnGenerate) return;
|
||||||
|
el.btnGenerate.disabled = active;
|
||||||
|
el.btnGenerate.style.opacity = active ? '0.5' : '1';
|
||||||
|
el.btnGenerate.style.cursor = active ? 'not-allowed' : 'pointer';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ API ============ */
|
||||||
|
var API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
var MASTER_PROMPT = 'minimal black and white line doodle, single stroke, white background, kawaii style';
|
||||||
|
|
||||||
|
function buildFirstMessage(userPrompt) {
|
||||||
|
return { role: 'user', content: MASTER_PROMPT + ', ' + userPrompt };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNextMessage(frameIndex, frameCount) {
|
||||||
|
return {
|
||||||
|
role: 'user',
|
||||||
|
content: 'imagine we are trying to create a ' + frameCount + ' frame gif. generate the next meaningful frame (frame ' + frameIndex + ' of ' + frameCount + ')'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateFrame(opts) {
|
||||||
|
var model = opts.model;
|
||||||
|
var messages = opts.messages;
|
||||||
|
var imageSize = opts.imageSize;
|
||||||
|
var aspectRatio = opts.aspectRatio;
|
||||||
|
var isGemini = model.indexOf('google/') === 0;
|
||||||
|
|
||||||
|
var body = {
|
||||||
|
model: model,
|
||||||
|
messages: messages,
|
||||||
|
modalities: ['image'],
|
||||||
|
image_config: {
|
||||||
|
aspect_ratio: aspectRatio,
|
||||||
|
image_size: imageSize,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isGemini && imageSize === '512x512') {
|
||||||
|
body.image_config.image_size = '1024x1024';
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(API_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + getApiKey(),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://vibegif.lol',
|
||||||
|
'X-Title': 'vibegif.lol'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(function(res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
return res.json().catch(function() { return {}; }).then(function(e) {
|
||||||
|
throw new Error((e && e.error && e.error.message) || 'API error ' + res.status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
console.log('API response:', JSON.stringify(data).substring(0, 500));
|
||||||
|
|
||||||
|
var choice = data.choices && data.choices[0];
|
||||||
|
if (!choice) throw new Error('No response from model');
|
||||||
|
|
||||||
|
/* Try multiple known response shapes */
|
||||||
|
var b64 = null;
|
||||||
|
|
||||||
|
// Shape 1: choice.message.images[]
|
||||||
|
var images = choice.message && choice.message.images;
|
||||||
|
if (images && images.length) {
|
||||||
|
var imgObj = images[0];
|
||||||
|
b64 = (imgObj.image_url && imgObj.image_url.url) || imgObj.url || imgObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape 2: multipart content with image_url
|
||||||
|
if (!b64 && choice.message && Array.isArray(choice.message.content)) {
|
||||||
|
for (var i = 0; i < choice.message.content.length; i++) {
|
||||||
|
var part = choice.message.content[i];
|
||||||
|
if (part.type === 'image_url' && part.image_url && part.image_url.url) {
|
||||||
|
b64 = part.image_url.url;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!b64) throw new Error('No image in response. Model may have refused or returned text only. Check console.');
|
||||||
|
|
||||||
|
if (typeof b64 === 'string' && b64.indexOf('data:') !== 0) {
|
||||||
|
b64 = 'data:image/png;base64,' + b64;
|
||||||
|
}
|
||||||
|
|
||||||
|
var assistantMsg = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: [{ type: 'image_url', image_url: { url: b64 } }]
|
||||||
|
};
|
||||||
|
|
||||||
|
return { base64: b64, assistantMsg: assistantMsg };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ GIF MAKER ============ */
|
||||||
|
var WORKER_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js';
|
||||||
|
var workerBlobUrl = null;
|
||||||
|
|
||||||
|
function getWorkerUrl() {
|
||||||
|
if (workerBlobUrl) return Promise.resolve(workerBlobUrl);
|
||||||
|
return fetch(WORKER_CDN)
|
||||||
|
.then(function(r) { return r.blob(); })
|
||||||
|
.then(function(blob) {
|
||||||
|
workerBlobUrl = URL.createObjectURL(blob);
|
||||||
|
return workerBlobUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImage(src) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var img = new Image();
|
||||||
|
img.onload = function() { resolve(img); };
|
||||||
|
img.onerror = function() { reject(new Error('Failed to load frame image')); };
|
||||||
|
img.src = src;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function assembleGif(base64Frames, fps) {
|
||||||
|
return getWorkerUrl().then(function(workerScript) {
|
||||||
|
return Promise.all(base64Frames.map(loadImage)).then(function(images) {
|
||||||
|
var w = images[0].naturalWidth;
|
||||||
|
var h = images[0].naturalHeight;
|
||||||
|
var delay = Math.round(1000 / fps);
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = w;
|
||||||
|
canvas.height = h;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var gif = new GIF({
|
||||||
|
workers: 2,
|
||||||
|
quality: 10,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
workerScript: workerScript,
|
||||||
|
repeat: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var i = 0; i < images.length; i++) {
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
ctx.drawImage(images[i], 0, 0, w, h);
|
||||||
|
gif.addFrame(ctx, { copy: true, delay: delay });
|
||||||
|
}
|
||||||
|
|
||||||
|
gif.on('finished', function(blob) { resolve(blob); });
|
||||||
|
gif.on('error', function(err) { reject(err); });
|
||||||
|
gif.render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ GENERATION ORCHESTRATOR ============ */
|
||||||
|
function handleGenerate() {
|
||||||
|
var prompt = (el.inpPrompt.value || '').trim();
|
||||||
|
if (!prompt) { el.inpPrompt.focus(); return; }
|
||||||
|
|
||||||
|
var model = el.selModel.value;
|
||||||
|
var frameCount = Math.max(2, Math.min(24, parseInt(el.inpFrames.value) || 4));
|
||||||
|
var fps = Math.max(1, Math.min(30, parseInt(el.inpFps.value) || 4));
|
||||||
|
var imageSize = el.selSize.value;
|
||||||
|
var aspectRatio = el.selRatio.value;
|
||||||
|
var WINDOW = 2;
|
||||||
|
|
||||||
|
setGenerating(true);
|
||||||
|
resetProgress();
|
||||||
|
hideResult();
|
||||||
|
|
||||||
|
var allBase64 = [];
|
||||||
|
var fullHistory = [];
|
||||||
|
var firstMsg = buildFirstMessage(prompt);
|
||||||
|
|
||||||
|
setProgress(0, 'generating frame 1 of ' + frameCount + '...');
|
||||||
|
|
||||||
|
generateFrame({ model: model, messages: [firstMsg], imageSize: imageSize, aspectRatio: aspectRatio })
|
||||||
|
.then(function(r1) {
|
||||||
|
allBase64.push(r1.base64);
|
||||||
|
fullHistory.push(firstMsg, r1.assistantMsg);
|
||||||
|
addFramePreview(r1.base64, 0);
|
||||||
|
setProgress(Math.round(100 / frameCount), 'frame 1 of ' + frameCount + ' done');
|
||||||
|
|
||||||
|
var chain = Promise.resolve();
|
||||||
|
for (var i = 2; i <= frameCount; i++) {
|
||||||
|
(function(idx) {
|
||||||
|
chain = chain.then(function() {
|
||||||
|
setProgress(Math.round(((idx - 1) / frameCount) * 100), 'generating frame ' + idx + ' of ' + frameCount + '...');
|
||||||
|
|
||||||
|
var nextUserMsg = buildNextMessage(idx, frameCount);
|
||||||
|
|
||||||
|
// Rolling window: first msg + last WINDOW*2 history entries + new user msg
|
||||||
|
var windowMessages = [firstMsg];
|
||||||
|
var startIdx = Math.max(1, fullHistory.length - WINDOW * 2);
|
||||||
|
for (var j = startIdx; j < fullHistory.length; j++) {
|
||||||
|
windowMessages.push(fullHistory[j]);
|
||||||
|
}
|
||||||
|
windowMessages.push(nextUserMsg);
|
||||||
|
|
||||||
|
return generateFrame({ model: model, messages: windowMessages, imageSize: imageSize, aspectRatio: aspectRatio })
|
||||||
|
.then(function(ri) {
|
||||||
|
allBase64.push(ri.base64);
|
||||||
|
fullHistory.push(nextUserMsg, ri.assistantMsg);
|
||||||
|
addFramePreview(ri.base64, idx - 1);
|
||||||
|
setProgress(Math.round((idx / frameCount) * 100), 'frame ' + idx + ' of ' + frameCount + ' done');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(i);
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
setProgress(100, 'assembling gif...');
|
||||||
|
return assembleGif(allBase64, fps);
|
||||||
|
})
|
||||||
|
.then(function(blob) {
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
showResult(url);
|
||||||
|
setProgress(100, 'done! 🎉');
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
setProgress(0, 'error: ' + err.message);
|
||||||
|
console.error('vibegif error:', err);
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
setGenerating(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ INIT ON DOM READY ============ */
|
||||||
|
function boot() {
|
||||||
|
grabElements();
|
||||||
|
|
||||||
|
// Init lucide icons safely
|
||||||
|
if (window.lucide && window.lucide.createIcons) {
|
||||||
|
try { window.lucide.createIcons(); } catch(e) { console.warn('lucide init error:', e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route to setup or main
|
||||||
|
if (hasApiKey()) {
|
||||||
|
showMain();
|
||||||
|
} else {
|
||||||
|
showSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup save
|
||||||
|
if (el.setupSave) el.setupSave.addEventListener('click', function() {
|
||||||
|
var k = (el.setupKey.value || '').trim();
|
||||||
|
if (!k) return;
|
||||||
|
setApiKey(k);
|
||||||
|
showMain();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings modal
|
||||||
|
if (el.btnSettings) el.btnSettings.addEventListener('click', showModal);
|
||||||
|
if (el.btnCloseModal) el.btnCloseModal.addEventListener('click', hideModal);
|
||||||
|
if (el.modalSettings) el.modalSettings.addEventListener('click', function(e) {
|
||||||
|
if (e.target === el.modalSettings) hideModal();
|
||||||
|
});
|
||||||
|
if (el.modalSave) el.modalSave.addEventListener('click', function() {
|
||||||
|
var k = (el.modalKey.value || '').trim();
|
||||||
|
if (!k) return;
|
||||||
|
setApiKey(k);
|
||||||
|
hideModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Model change: disable 0.5K for non-Gemini
|
||||||
|
if (el.selModel) el.selModel.addEventListener('change', function() {
|
||||||
|
var isGemini = el.selModel.value.indexOf('google/') === 0;
|
||||||
|
var opt05 = el.selSize.querySelector('option[value="512x512"]');
|
||||||
|
if (opt05) opt05.disabled = !isGemini;
|
||||||
|
if (!isGemini && el.selSize.value === '512x512') el.selSize.value = '1024x1024';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate
|
||||||
|
if (el.btnGenerate) el.btnGenerate.addEventListener('click', handleGenerate);
|
||||||
|
|
||||||
|
console.log('vibegif.lol booted ✓');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive: run on DOMContentLoaded OR immediately if already loaded
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', boot);
|
||||||
|
} else {
|
||||||
|
boot();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user