diff --git a/app.js b/app.js new file mode 100644 index 0000000..5bede13 --- /dev/null +++ b/app.js @@ -0,0 +1,115 @@ +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();