Files
vibegif.lol/assets/js/app.js

273 lines
7.0 KiB
JavaScript

import { generateImageFrame } from "./openrouter.js";
import { buildGifFromFrames } from "./gif.js";
import { firstPrompt, nextFramePrompt } from "./ui.js";
const GEMINI_MODEL = "google/gemini-3.1-flash-image-preview";
// Wrap in try-catch to prevent fatal crashes in strict iframes/sandboxes
let savedApiKey = "";
try {
savedApiKey = localStorage.getItem("openrouter_api_key") || "";
} catch (err) {
console.warn("localStorage is blocked in this environment.");
}
const state = {
apiKey: savedApiKey,
frames: [],
gifUrl: "",
loading: false
};
const el = {};
const q = (id) => document.getElementById(id);
function clamp(n, min, max, fallback) {
n = Number(n);
if (Number.isNaN(n)) return fallback;
return Math.min(max, Math.max(min, n));
}
function show(node) {
node.classList.remove("hidden");
if (node.id === "settings-modal") node.classList.add("flex");
}
function hide(node) {
node.classList.add("hidden");
if (node.id === "settings-modal") node.classList.remove("flex");
}
function setError(msg = "") {
if (!msg) {
el.error.textContent = "";
hide(el.error);
return;
}
el.error.textContent = msg;
show(el.error);
}
function setProgress(label = "", pct = 0, visible = false) {
el.progressLabel.textContent = label;
el.progressBar.style.width = `${Math.max(0, Math.min(100, pct))}%`;
visible ? show(el.progressWrap) : hide(el.progressWrap);
}
function setLoading(v) {
state.loading = !!v;
el.generateBtn.disabled = state.loading;
el.generateBtn.textContent = state.loading ? "Generating…" : "Generate GIF";
}
function updateImageSizeOptions() {
const model = el.model.value;
const current = el.imageSize.value;
const options = model === GEMINI_MODEL
? [
{ value: "1K", label: "1K" },
{ value: "0.5K", label: "0.5K" }
]
: [{ value: "1K", label: "1K" }];
el.imageSize.innerHTML = "";
for (const opt of options) {
const o = document.createElement("option");
o.value = opt.value;
o.textContent = opt.label;
el.imageSize.appendChild(o);
}
const allowed = options.map((o) => o.value);
el.imageSize.value = allowed.includes(current) ? current : "1K";
}
function renderFrames() {
el.framesGrid.innerHTML = "";
for (let i = 0; i < state.frames.length; i++) {
const card = document.createElement("div");
card.className = "rounded-xl border border-ui-border p-2 bg-white";
const img = document.createElement("img");
img.src = state.frames[i];
img.alt = `frame ${i + 1}`;
img.className = "w-full rounded-lg";
const p = document.createElement("p");
p.className = "mt-1 text-xs text-ui-sub";
p.textContent = `Frame ${i + 1}`;
card.appendChild(img);
card.appendChild(p);
el.framesGrid.appendChild(card);
}
}
function renderGif() {
if (!state.gifUrl) {
hide(el.gifWrap);
show(el.emptyOutput);
return;
}
el.gifImg.src = state.gifUrl;
el.gifDownload.href = state.gifUrl;
show(el.gifWrap);
hide(el.emptyOutput);
}
function openSettings() {
el.apiKeyInput.value = state.apiKey;
show(el.settingsModal);
}
function closeSettings() {
hide(el.settingsModal);
}
function saveApiKey() {
state.apiKey = el.apiKeyInput.value.trim();
try {
localStorage.setItem("openrouter_api_key", state.apiKey);
} catch (err) {
console.warn("Could not save to localStorage.");
}
closeSettings();
}
function clearApiKey() {
state.apiKey = "";
el.apiKeyInput.value = "";
try {
localStorage.removeItem("openrouter_api_key");
} catch (err) {
console.warn("Could not modify localStorage.");
}
}
async function generate() {
setError("");
setLoading(true);
setProgress("", 0, true);
if (state.gifUrl) URL.revokeObjectURL(state.gifUrl);
state.gifUrl = "";
state.frames = [];
renderGif();
renderFrames();
const model = el.model.value;
const userPrompt = el.userPrompt.value.trim();
const frameCount = clamp(el.frameCount.value, 2, 24, 4);
const fps = clamp(el.fps.value, 1, 24, 6);
const imageSize = el.imageSize.value;
const aspectRatio = el.aspectRatio.value;
if (!state.apiKey) {
setError("Add your OpenRouter API key first (☰ button).");
openSettings();
setLoading(false);
setProgress("", 0, false);
return;
}
if (!userPrompt) {
setError("Please enter a simple prompt (e.g. rolling cat).");
setLoading(false);
setProgress("", 0, false);
return;
}
try {
const p1 = firstPrompt(userPrompt);
setProgress(`Generating frame 1/${frameCount}...`, Math.round(100 / frameCount), true);
const frame1 = await generateImageFrame({
apiKey: state.apiKey,
model,
textPrompt: p1,
previousFrames: [],
imageSize,
aspectRatio
});
state.frames.push(frame1);
renderFrames();
for (let i = 2; i <= frameCount; i++) {
setProgress(`Generating frame ${i}/${frameCount}...`, Math.round((i / frameCount) * 100), true);
const next = await generateImageFrame({
apiKey: state.apiKey,
model,
textPrompt: nextFramePrompt(frameCount),
previousFrames: state.frames.slice(-2),
imageSize,
aspectRatio
});
state.frames.push(next);
renderFrames();
}
setProgress("Building GIF...", 100, true);
state.gifUrl = await buildGifFromFrames(state.frames, fps);
renderGif();
setProgress("Done.", 100, true);
} catch (e) {
setError(e?.message || "Failed to generate.");
setProgress("", 0, false);
} finally {
setLoading(false);
}
}
function bind() {
el.openSettingsBtn.addEventListener("click", openSettings);
el.closeSettingsBtn.addEventListener("click", closeSettings);
el.saveApiKeyBtn.addEventListener("click", saveApiKey);
el.clearApiKeyBtn.addEventListener("click", clearApiKey);
el.generateBtn.addEventListener("click", generate);
el.model.addEventListener("change", updateImageSizeOptions);
el.settingsModal.addEventListener("click", (e) => {
if (e.target === el.settingsModal) closeSettings();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeSettings();
});
}
function init() {
el.model = q("model");
el.userPrompt = q("user-prompt");
el.frameCount = q("frame-count");
el.fps = q("fps");
el.imageSize = q("image-size");
el.aspectRatio = q("aspect-ratio");
el.generateBtn = q("generate-btn");
el.error = q("error");
el.progressWrap = q("progress-wrap");
el.progressLabel = q("progress-label");
el.progressBar = q("progress-bar");
el.gifWrap = q("gif-wrap");
el.gifImg = q("gif-img");
el.gifDownload = q("gif-download");
el.emptyOutput = q("empty-output");
el.framesGrid = q("frames-grid");
el.settingsModal = q("settings-modal");
el.openSettingsBtn = q("open-settings-btn");
el.closeSettingsBtn = q("close-settings-btn");
el.apiKeyInput = q("api-key-input");
el.saveApiKeyBtn = q("save-api-key-btn");
el.clearApiKeyBtn = q("clear-api-key-btn");
updateImageSizeOptions();
renderGif();
bind();
}
// Since module scripts are natively deferred, the DOM is guaranteed to be fully parsed.
init();