Files
vibegif.lol/index.html

209 lines
8.3 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>vibegif.lol</title>
<meta
name="description"
content="AI generated GIFs with BYOK OpenRouter. Minimal kawaii line doodles."
/>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
},
},
},
};
</script>
<link rel="stylesheet" href="./assets/css/styles.css" />
<script defer src="https://unpkg.com/lucide@latest"></script>
<script defer src="https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script type="module" src="./assets/js/app.js"></script>
</head>
<body class="bg-gray-50 text-gray-900" x-data="vibeGifApp()" x-init="init()">
<div class="min-h-screen">
<header class="border-b border-gray-200 bg-white/90 backdrop-blur">
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
<button
class="inline-flex items-center justify-center rounded-lg border border-gray-200 p-2 text-gray-600 hover:bg-gray-100"
@click="openSettings()"
title="Account settings"
>
<i data-lucide="panel-left" class="h-5 w-5"></i>
</button>
<div class="text-lg font-semibold tracking-tight">vibegif.lol</div>
<div class="text-xs text-gray-500">
<span x-show="hasApiKey" x-cloak>BYOK connected</span>
<span x-show="!hasApiKey" x-cloak>No API key</span>
</div>
</div>
</header>
<main class="mx-auto grid max-w-6xl gap-6 px-4 py-6 lg:grid-cols-2">
<!-- Controls -->
<section class="card p-5">
<h1 class="mb-1 text-xl font-semibold">Generate a vibe GIF</h1>
<p class="mb-5 text-sm text-gray-600">
Use a very simple prompt like: <code class="chip">rolling cat</code>
</p>
<div x-show="!hasApiKey" x-cloak class="mb-4 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
OpenRouter API key is required.
<button class="ml-1 underline" @click="openSettings()">Add it in Account Settings</button>.
</div>
<div class="grid gap-4 sm:grid-cols-2">
<label class="field">
<span class="label">Model</span>
<select class="input" x-model="model">
<template x-for="m in modelOptions" :key="m.id">
<option :value="m.id" x-text="m.label"></option>
</template>
</select>
</label>
<label class="field">
<span class="label">Image size</span>
<select class="input" x-model="imageSize">
<template x-for="s in imageSizeOptions" :key="s">
<option :value="s" x-text="s"></option>
</template>
</select>
</label>
<label class="field">
<span class="label">Aspect ratio</span>
<select class="input" x-model="aspectRatio">
<template x-for="r in aspectRatioOptions" :key="r">
<option :value="r" x-text="r"></option>
</template>
</select>
</label>
<label class="field">
<span class="label">Frames</span>
<input class="input" type="number" min="2" max="24" step="1" x-model.number="frameCount" />
</label>
<label class="field">
<span class="label">Framerate (fps)</span>
<input class="input" type="number" min="1" max="24" step="1" x-model.number="fps" />
</label>
</div>
<label class="field mt-4">
<span class="label">Prompt</span>
<input
class="input"
type="text"
x-model.trim="userPrompt"
placeholder="rolling cat"
maxlength="120"
/>
<span class="hint">Keep it short/simple for better frame continuity.</span>
</label>
<label class="field mt-4">
<span class="label">Master Prompt (locked style)</span>
<textarea class="input min-h-[74px]" readonly x-model="masterPrompt"></textarea>
</label>
<div class="mt-5 flex items-center gap-3">
<button
class="inline-flex items-center justify-center rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-black disabled:cursor-not-allowed disabled:opacity-50"
:disabled="!canGenerate"
@click="generateGif()"
>
<span x-show="!isGenerating">Generate GIF</span>
<span x-show="isGenerating" x-cloak>Generating…</span>
</button>
<span class="text-sm text-gray-600" x-text="progressText"></span>
</div>
<p x-show="errorText" x-cloak class="mt-3 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700" x-text="errorText"></p>
</section>
<!-- Output -->
<section class="card p-5">
<h2 class="mb-3 text-lg font-semibold">Output</h2>
<div x-show="frames.length === 0" class="rounded-lg border border-dashed border-gray-300 p-8 text-center text-sm text-gray-500">
Your generated frames will appear here.
</div>
<div x-show="frames.length > 0" x-cloak>
<p class="mb-2 text-sm text-gray-600">
Frames (<span x-text="frames.length"></span>)
</p>
<div class="grid max-h-72 grid-cols-2 gap-2 overflow-auto sm:grid-cols-3">
<template x-for="(f, idx) in frames" :key="idx">
<figure class="rounded-lg border border-gray-200 bg-white p-1">
<img :src="f" alt="" class="h-24 w-full rounded object-contain bg-white" loading="lazy" />
<figcaption class="mt-1 text-center text-[11px] text-gray-500">Frame <span x-text="idx + 1"></span></figcaption>
</figure>
</template>
</div>
</div>
<div x-show="gifUrl" x-cloak class="mt-5 border-t border-gray-200 pt-4">
<p class="mb-2 text-sm text-gray-600">GIF preview</p>
<img :src="gifUrl" alt="Generated gif" class="max-h-80 w-full rounded-lg border border-gray-200 bg-white object-contain" />
<button
class="mt-3 inline-flex items-center justify-center rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-800 hover:bg-gray-100"
@click="downloadGif()"
>
Download GIF
</button>
</div>
</section>
</main>
</div>
<!-- Account Settings Modal -->
<div
x-show="showSettings"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black/30 p-4"
@click.self="closeSettings()"
>
<div class="w-full max-w-md rounded-2xl border border-gray-200 bg-white p-5 shadow-xl">
<div class="mb-3 flex items-center gap-2">
<i data-lucide="key-round" class="h-4 w-4 text-gray-500"></i>
<h3 class="text-base font-semibold">Account Settings</h3>
</div>
<label class="field">
<span class="label">OpenRouter API key</span>
<input
class="input"
type="password"
x-model.trim="apiKeyInput"
placeholder="sk-or-v1-..."
autocomplete="off"
/>
<span class="hint">Stored in localStorage on this browser only.</span>
</label>
<div class="mt-4 flex items-center justify-end gap-2">
<button class="rounded-lg border border-gray-300 px-3 py-2 text-sm hover:bg-gray-100" @click="clearApiKey()">Clear</button>
<button class="rounded-lg border border-gray-300 px-3 py-2 text-sm hover:bg-gray-100" @click="closeSettings()">Cancel</button>
<button class="rounded-lg bg-gray-900 px-3 py-2 text-sm font-medium text-white hover:bg-black" @click="saveApiKey()">Save</button>
</div>
</div>
</div>
</body>
</html>