From b785fd3d13e024a65a2f5aaefdeb0c045ceeb2a1 Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Fri, 20 Mar 2026 20:59:29 -0700 Subject: [PATCH] Feat: Alpine state + generation loop --- assets/js/app.js | 198 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 assets/js/app.js diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..ea702bd --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,198 @@ +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);