import { getApiKey, setApiKey, hasApiKey } from './storage.js'; import { generateFrame, buildFirstMessage, buildNextMessage } from './api.js'; import { assembleGif } from './gifmaker.js'; import { el, showSetup, showMain, showModal, hideModal, setProgress, addFramePreview, resetProgress, showResult, hideResult, setGenerating, } from './ui.js'; // --- Init --- function init() { lucide.createIcons(); hasApiKey() ? showMain() : showSetup(); bindEvents(); } // --- Events --- function bindEvents() { el.setupSave.addEventListener('click', () => { const k = el.setupKey.value.trim(); if (!k) return; setApiKey(k); showMain(); }); el.btnSettings.addEventListener('click', () => showModal(getApiKey())); el.btnCloseModal.addEventListener('click', hideModal); el.modal.addEventListener('click', (e) => { if (e.target === el.modal) hideModal(); }); el.modalSave.addEventListener('click', () => { const k = el.modalKey.value.trim(); if (!k) return; setApiKey(k); hideModal(); }); el.selModel.addEventListener('change', () => { const isGemini = el.selModel.value.startsWith('google/'); const opt05 = el.selSize.querySelector('option[value="0.5K"]'); if (!isGemini && el.selSize.value === '0.5K') el.selSize.value = '1K'; opt05.disabled = !isGemini; }); el.btnGenerate.addEventListener('click', handleGenerate); } // --- Generation --- async function handleGenerate() { const prompt = el.inpPrompt.value.trim(); if (!prompt) { el.inpPrompt.focus(); return; } const model = el.selModel.value; const frameCount = Math.max(2, Math.min(24, parseInt(el.inpFrames.value) || 4)); const fps = Math.max(1, Math.min(30, parseInt(el.inpFps.value) || 4)); const imageSize = el.selSize.value; const aspectRatio = el.selRatio.value; const WINDOW = 2; setGenerating(true); resetProgress(); hideResult(); const allBase64 = []; const fullHistory = []; // all assistant messages for rolling window try { // --- Frame 1 --- setProgress(0, `generating frame 1 of ${frameCount}...`); const firstMsg = buildFirstMessage(prompt); const messages1 = [firstMsg]; const r1 = await generateFrame({ model, messages: messages1, imageSize, aspectRatio }); allBase64.push(r1.base64); fullHistory.push(firstMsg, r1.assistantMsg); addFramePreview(r1.base64, 0); setProgress(Math.round(100 / frameCount), `frame 1 of ${frameCount} done`); // --- Frames 2..N --- for (let i = 2; i <= frameCount; i++) { setProgress(Math.round(((i - 1) / frameCount) * 100), `generating frame ${i} of ${frameCount}...`); const nextUserMsg = buildNextMessage(i, frameCount); // Build rolling window: first user msg + last WINDOW (assistant+user) pairs const windowMessages = [firstMsg]; const pairCount = fullHistory.length; // includes user+assistant messages const startIdx = Math.max(1, pairCount - WINDOW * 2); for (let j = startIdx; j < pairCount; j++) { windowMessages.push(fullHistory[j]); } windowMessages.push(nextUserMsg); const ri = await generateFrame({ model, messages: windowMessages, imageSize, aspectRatio }); allBase64.push(ri.base64); fullHistory.push(nextUserMsg, ri.assistantMsg); addFramePreview(ri.base64, i - 1); setProgress(Math.round((i / frameCount) * 100), `frame ${i} of ${frameCount} done`); } // --- Assemble GIF --- setProgress(100, 'assembling gif...'); const blob = await assembleGif(allBase64, fps); const url = URL.createObjectURL(blob); showResult(url); setProgress(100, 'done!'); } catch (err) { setProgress(0, `error: ${err.message}`); console.error(err); } finally { setGenerating(false); } } // --- Boot --- init();