diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..3ca92bd --- /dev/null +++ b/src/main.js @@ -0,0 +1,177 @@ +import './styles.css'; +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();