Feat: Main orchestrator — generation loop and events

This commit is contained in:
2026-03-20 22:08:01 -07:00
parent 72b69455e9
commit 50e8223bbb

115
app.js Normal file
View File

@@ -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();