import { STORAGE_KEYS, MASTER_PROMPT, MODELS, BASE_ASPECT_RATIOS, GEMINI_EXTRA_ASPECT_RATIOS, } from "./config.js"; import { requestFrameImage } from "./openrouter.js"; import { createGifFromFrames } from "./gif-builder.js"; const clamp = (n, min, max) => Math.max(min, Math.min(max, Number(n) || min)); const modelById = (id) => MODELS.find((m) => m.id === id) || MODELS[0]; function vibeGifApp() { return { apiKey: "", apiKeyInput: "", showSettings: false, model: MODELS[0].id, imageSize: "1K", aspectRatio: "1:1", frameCount: 4, fps: 4, userPrompt: "rolling cat", masterPrompt: MASTER_PROMPT, frames: [], gifUrl: "", gifBlob: null, isGenerating: false, progressText: "", errorText: "", init() { const saved = localStorage.getItem(STORAGE_KEYS.apiKey) || ""; this.apiKey = saved; this.apiKeyInput = saved; this.showSettings = !saved; this.normalizeSelections(); this.$watch("model", () => this.normalizeSelections()); this.refreshIcons(); }, get modelOptions() { return MODELS; }, get selectedModel() { return modelById(this.model); }, get imageSizeOptions() { const opts = ["1K"]; if (this.selectedModel.supportsHalfK) opts.push("0.5K"); return opts; }, get aspectRatioOptions() { const opts = [...BASE_ASPECT_RATIOS]; if (this.selectedModel.supportsExtendedAspectRatios) { opts.push(...GEMINI_EXTRA_ASPECT_RATIOS); } return opts; }, get hasApiKey() { return !!this.apiKey; }, get canGenerate() { return this.hasApiKey && !this.isGenerating && this.userPrompt.trim().length > 0; }, normalizeSelections() { if (!this.imageSizeOptions.includes(this.imageSize)) { this.imageSize = this.imageSizeOptions[0]; } if (!this.aspectRatioOptions.includes(this.aspectRatio)) { this.aspectRatio = "1:1"; } }, openSettings() { this.apiKeyInput = this.apiKey; this.showSettings = true; this.refreshIcons(); }, closeSettings() { this.showSettings = false; }, saveApiKey() { const key = (this.apiKeyInput || "").trim(); this.apiKey = key; if (key) localStorage.setItem(STORAGE_KEYS.apiKey, key); else localStorage.removeItem(STORAGE_KEYS.apiKey); this.showSettings = false; }, clearApiKey() { this.apiKey = ""; this.apiKeyInput = ""; localStorage.removeItem(STORAGE_KEYS.apiKey); this.showSettings = true; }, resetOutput() { this.frames = []; this.errorText = ""; this.progressText = ""; this.gifBlob = null; if (this.gifUrl) { URL.revokeObjectURL(this.gifUrl); this.gifUrl = ""; } }, async generateGif() { if (!this.canGenerate) { if (!this.hasApiKey) this.openSettings(); return; } const frameCount = clamp(this.frameCount, 2, 24); const fps = clamp(this.fps, 1, 24); const prompt = this.userPrompt.trim(); this.frameCount = frameCount; this.fps = fps; this.resetOutput(); this.isGenerating = true; try { for (let i = 1; i <= frameCount; i++) { this.progressText = `Generating frame ${i}/${frameCount}...`; const image = await requestFrameImage({ apiKey: this.apiKey, model: this.model, userPrompt: prompt, masterPrompt: this.masterPrompt, frameCount, frameIndex: i, previousFrames: this.frames.slice(-2), // rolling window = 2 imageSize: this.imageSize, aspectRatio: this.aspectRatio, }); this.frames.push(image); } this.progressText = "Rendering GIF..."; this.gifBlob = await createGifFromFrames(this.frames, { fps }); this.gifUrl = URL.createObjectURL(this.gifBlob); this.progressText = "Done ✨"; } catch (err) { this.errorText = err?.message || "Generation failed."; } finally { this.isGenerating = false; } }, downloadGif() { if (!this.gifUrl) return; const a = document.createElement("a"); a.href = this.gifUrl; a.download = `vibegif-${Date.now()}.gif`; document.body.appendChild(a); a.click(); a.remove(); }, refreshIcons() { this.$nextTick(() => window.lucide?.createIcons()); }, }; } let registered = false; const register = () => { if (registered) return; registered = true; window.Alpine.data("vibeGifApp", vibeGifApp); }; if (window.Alpine) register(); document.addEventListener("alpine:init", register);