From dcc334479f26956ffc8b8edd44802927d76b315c Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Fri, 20 Mar 2026 21:14:39 -0700 Subject: [PATCH] Feat: Alpine app orchestration and generation loop --- assets/js/app.js | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 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..c06fcb6 --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,115 @@ +import { generateImageFrame } from "./openrouter.js"; +import { buildGifFromFrames } from "./gif.js"; +import { firstPrompt, nextFramePrompt, clampForm } from "./ui.js"; + +window.vibeGifApp = function () { + return { + settingsOpen: false, + loading: false, + error: "", + apiKeyInput: "", + apiKey: "", + progressLabel: "", + progressPct: 0, + frames: [], + gifUrl: "", + + form: { + model: "google/gemini-3.1-flash-image-preview", + userPrompt: "", + frameCount: 4, + fps: 6, + imageSize: "1K", + aspectRatio: "1:1" + }, + + init() { + this.apiKey = localStorage.getItem("openrouter_api_key") || ""; + this.apiKeyInput = this.apiKey || ""; + window.lucide?.createIcons(); + this.$watch("settingsOpen", () => setTimeout(() => window.lucide?.createIcons(), 0)); + this.$watch("form.model", () => { + if (this.form.model !== "google/gemini-3.1-flash-image-preview" && this.form.imageSize === "0.5K") { + this.form.imageSize = "1K"; + } + }); + }, + + saveApiKey() { + this.apiKey = this.apiKeyInput.trim(); + localStorage.setItem("openrouter_api_key", this.apiKey); + this.settingsOpen = false; + }, + + clearApiKey() { + this.apiKey = ""; + this.apiKeyInput = ""; + localStorage.removeItem("openrouter_api_key"); + }, + + async generate() { + this.error = ""; + this.gifUrl = ""; + this.frames = []; + this.progressLabel = ""; + this.progressPct = 0; + + clampForm(this.form); + + if (!this.apiKey) { + this.error = "Add your OpenRouter API key first (panel-left icon)."; + this.settingsOpen = true; + return; + } + + if (!this.form.userPrompt) { + this.error = "Please enter a simple prompt (e.g. rolling cat)."; + return; + } + + this.loading = true; + + try { + const total = this.form.frameCount; + + // Frame 1 prompt must be exactly this template + userPrompt + const p1 = firstPrompt(this.form.userPrompt); + this.progressLabel = `Generating frame 1/${total}...`; + const frame1 = await generateImageFrame({ + apiKey: this.apiKey, + model: this.form.model, + textPrompt: p1, + previousFrames: [], + imageSize: this.form.imageSize, + aspectRatio: this.form.aspectRatio + }); + this.frames.push(frame1); + this.progressPct = Math.round((1 / total) * 100); + + for (let i = 2; i <= total; i++) { + this.progressLabel = `Generating frame ${i}/${total}...`; + const p = nextFramePrompt(total); + const next = await generateImageFrame({ + apiKey: this.apiKey, + model: this.form.model, + textPrompt: p, + previousFrames: this.frames.slice(-2), + imageSize: this.form.imageSize, + aspectRatio: this.form.aspectRatio + }); + this.frames.push(next); + this.progressPct = Math.round((i / total) * 100); + } + + this.progressLabel = "Building GIF..."; + this.gifUrl = await buildGifFromFrames(this.frames, this.form.fps); + this.progressLabel = "Done."; + this.progressPct = 100; + } catch (e) { + this.error = e?.message || "Failed to generate."; + } finally { + this.loading = false; + } + } + }; +};