import { getApiKey, setApiKey, hasApiKey } from './core/storage.js'; import { buildFirstMessage, buildNextMessage } from './core/messages.js'; import { generateFrame } from './services/openrouter.js'; import { assembleGif } from './services/gif.js'; import { ELEMENT_IDS, grabElements, show, hide, setProgress, addFramePreview, resetProgress, showResult, hideResult, setGenerating, syncSizeByModel } from './app/dom.js'; const WINDOW = 1; let el = {}; let currentGifUrl = ''; 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 clearOldResultUrl() { if (!currentGifUrl) return; URL.revokeObjectURL(currentGifUrl); currentGifUrl = ''; } async function handleGenerate() { const prompt = (el.inpPrompt?.value || '').trim(); if (!prompt) { el.inpPrompt?.focus(); return; } if (!hasApiKey()) { showSetup(); return; } const model = el.selModel?.value || 'google/gemini-3.1-flash-image-preview'; const frameCount = Math.max(2, Math.min(24, Number.parseInt(el.inpFrames?.value || '4', 10) || 4)); const fps = Math.max(1, Math.min(30, Number.parseInt(el.inpFps?.value || '4', 10) || 4)); const imageSize = el.selSize?.value || '1K'; const aspectRatio = el.selRatio?.value || '1:1'; setGenerating(el, true); resetProgress(el); clearOldResultUrl(); hideResult(el); const allFrames = []; const history = []; const firstMsg = buildFirstMessage(prompt); try { setProgress(el, 0, `generating frame 1 of ${frameCount}...`); const first = await generateFrame({ model, messages: [firstMsg], imageSize, aspectRatio, apiKey: getApiKey() }); allFrames.push(first.base64); history.push(firstMsg, first.assistantMsg); addFramePreview(el, first.base64, 0); setProgress(el, Math.round(100 / frameCount), `frame 1 of ${frameCount} done`); for (let i = 2; i <= frameCount; i++) { setProgress(el, Math.round(((i - 1) / frameCount) * 100), `generating frame ${i} of ${frameCount}...`); const nextUserMsg = buildNextMessage(i, frameCount); const startIdx = Math.max(1, history.length - WINDOW * 2); const windowMessages = [firstMsg, ...history.slice(startIdx), nextUserMsg]; const next = await generateFrame({ model, messages: windowMessages, imageSize, aspectRatio, apiKey: getApiKey() }); allFrames.push(next.base64); history.push(nextUserMsg, next.assistantMsg); addFramePreview(el, next.base64, i - 1); setProgress(el, Math.round((i / frameCount) * 100), `frame ${i} of ${frameCount} done`); } setProgress(el, 100, 'assembling gif...'); const blob = await assembleGif(allFrames, fps); currentGifUrl = URL.createObjectURL(blob); showResult(el, currentGifUrl); setProgress(el, 100, 'done! 🎉'); } catch (err) { const msg = err?.message || String(err); setProgress(el, 0, `error: ${msg}`); console.error('vibegif error:', err); } finally { setGenerating(el, false); } } function boot() { el = grabElements(ELEMENT_IDS); if (window.lucide?.createIcons) { try { window.lucide.createIcons(); } catch (e) { console.warn('lucide init error:', e); } } if (hasApiKey()) showMain(); else showSetup(); syncSizeByModel(el); el.setupSave?.addEventListener('click', () => { const key = (el.setupKey?.value || '').trim(); if (!key) return; setApiKey(key); if (el.setupKey) el.setupKey.value = ''; showMain(); }); el.btnSettings?.addEventListener('click', showModal); el.btnCloseModal?.addEventListener('click', hideModal); el.modalSettings?.addEventListener('click', (e) => { if (e.target === el.modalSettings) hideModal(); }); el.modalSave?.addEventListener('click', () => { const key = (el.modalKey?.value || '').trim(); if (!key) return; setApiKey(key); hideModal(); }); el.selModel?.addEventListener('change', () => syncSizeByModel(el)); el.btnGenerate?.addEventListener('click', handleGenerate); el.inpPrompt?.addEventListener('keydown', (e) => { if (e.key === 'Enter') handleGenerate(); }); window.addEventListener('beforeunload', clearOldResultUrl); console.log('vibegif.lol booted ✓'); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot();