mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 02:12:12 +00:00
206 lines
8.3 KiB
HTML
206 lines
8.3 KiB
HTML
<!doctype html>
|
|
<html lang="en" class="h-full">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>vibegif.lol</title>
|
|
|
|
<!-- Tailwind -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
ui: {
|
|
bg: "#fafafa",
|
|
panel: "#ffffff",
|
|
border: "#e5e7eb",
|
|
text: "#111827",
|
|
sub: "#6b7280",
|
|
accent: "#374151"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<!-- Lucide -->
|
|
<!-- Development version -->
|
|
<!-- <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script> -->
|
|
<!-- Production version -->
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
|
|
<!-- gif.js + worker -->
|
|
<script src="https://cdn.jsdelivr.net/npm/gif.js.optimized/dist/gif.js"></script>
|
|
|
|
<!-- Alpine -->
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
|
<link rel="stylesheet" href="./assets/css/styles.css" />
|
|
</head>
|
|
|
|
<body class="h-full bg-ui-bg text-ui-text">
|
|
<div x-data="vibeGifApp()" x-init="init()" class="min-h-full">
|
|
<header class="sticky top-0 z-20 border-b border-ui-border bg-ui-panel/95 backdrop-blur">
|
|
<div class="mx-auto max-w-6xl px-4 py-3 flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<button
|
|
@click="settingsOpen = true"
|
|
class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-ui-border hover:bg-gray-100"
|
|
title="Account settings"
|
|
aria-label="Open account settings"
|
|
>
|
|
<i data-lucide="panel-left" class="h-5 w-5 text-ui-accent"></i>
|
|
</button>
|
|
<div>
|
|
<h1 class="text-xl font-semibold leading-tight">vibegif.lol</h1>
|
|
<p class="text-xs text-ui-sub">AI Generated Gifs (BYOK OpenRouter)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="mx-auto max-w-6xl px-4 py-6 grid gap-6 lg:grid-cols-3">
|
|
<section class="lg:col-span-1 rounded-2xl border border-ui-border bg-ui-panel p-4 space-y-4">
|
|
<h2 class="font-semibold">Generate</h2>
|
|
|
|
<div class="space-y-1">
|
|
<label class="text-sm text-ui-sub">Model</label>
|
|
<select x-model="form.model" class="w-full rounded-xl border border-ui-border bg-white px-3 py-2">
|
|
<option value="google/gemini-3.1-flash-image-preview">google/gemini-3.1-flash-image-preview</option>
|
|
<option value="bytedance-seed/seedream-4.5">bytedance-seed/seedream-4.5</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label class="text-sm text-ui-sub">Simple user prompt</label>
|
|
<input
|
|
x-model.trim="form.userPrompt"
|
|
placeholder="e.g. rolling cat"
|
|
class="w-full rounded-xl border border-ui-border bg-white px-3 py-2"
|
|
/>
|
|
<p class="text-xs text-ui-sub">Keep it very simple.</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="space-y-1">
|
|
<label class="text-sm text-ui-sub">Frames</label>
|
|
<input x-model.number="form.frameCount" type="number" min="2" max="24" class="w-full rounded-xl border border-ui-border bg-white px-3 py-2" />
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="text-sm text-ui-sub">FPS</label>
|
|
<input x-model.number="form.fps" type="number" min="1" max="24" class="w-full rounded-xl border border-ui-border bg-white px-3 py-2" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="space-y-1">
|
|
<label class="text-sm text-ui-sub">Image size</label>
|
|
|
|
<!-- Gemini: 1K + 0.5K -->
|
|
<template x-if="isGeminiModel(form.model)">
|
|
<select x-model="form.imageSize" class="w-full rounded-xl border border-ui-border bg-white px-3 py-2">
|
|
<option value="1K">1K</option>
|
|
<option value="0.5K">0.5K</option>
|
|
</select>
|
|
</template>
|
|
|
|
<!-- Non-Gemini: 1K only -->
|
|
<template x-if="!isGeminiModel(form.model)">
|
|
<select x-model="form.imageSize" class="w-full rounded-xl border border-ui-border bg-white px-3 py-2">
|
|
<option value="1K">1K</option>
|
|
</select>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label class="text-sm text-ui-sub">Aspect ratio</label>
|
|
<select x-model="form.aspectRatio" class="w-full rounded-xl border border-ui-border bg-white px-3 py-2">
|
|
<option>1:1</option>
|
|
<option>16:9</option>
|
|
<option>9:16</option>
|
|
<option>4:3</option>
|
|
<option>3:4</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
@click="generate()"
|
|
:disabled="loading"
|
|
class="w-full rounded-xl bg-gray-900 text-white px-4 py-2.5 disabled:opacity-50"
|
|
>
|
|
<span x-show="!loading">Generate GIF</span>
|
|
<span x-show="loading">Generating…</span>
|
|
</button>
|
|
|
|
<p x-show="error" x-text="error" class="text-sm text-red-600"></p>
|
|
|
|
<div x-show="loading" class="text-sm text-ui-sub">
|
|
<p x-text="progressLabel"></p>
|
|
<div class="mt-2 h-2 rounded-full bg-gray-100 overflow-hidden">
|
|
<div class="h-full bg-gray-800 transition-all" :style="`width:${progressPct}%`"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="lg:col-span-2 rounded-2xl border border-ui-border bg-ui-panel p-4 space-y-4">
|
|
<h2 class="font-semibold">Output</h2>
|
|
|
|
<template x-if="gifUrl">
|
|
<div class="space-y-3">
|
|
<img :src="gifUrl" alt="Generated gif" class="w-full max-w-lg rounded-xl border border-ui-border bg-white" />
|
|
<a :href="gifUrl" download="vibegif.gif" class="inline-flex rounded-xl border border-ui-border px-4 py-2 hover:bg-gray-100">
|
|
Download GIF
|
|
</a>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="!gifUrl">
|
|
<p class="text-ui-sub text-sm">No GIF yet.</p>
|
|
</template>
|
|
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3" x-show="frames.length">
|
|
<template x-for="(frame, idx) in frames" :key="idx">
|
|
<div class="rounded-xl border border-ui-border p-2 bg-white">
|
|
<img :src="frame" class="w-full rounded-lg" :alt="`frame ${idx+1}`" />
|
|
<p class="mt-1 text-xs text-ui-sub">Frame <span x-text="idx+1"></span></p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<div x-show="settingsOpen" x-cloak class="fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4">
|
|
<div @click.outside="settingsOpen=false" class="w-full max-w-md rounded-2xl border border-ui-border bg-white p-4 space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="font-semibold">Account Settings</h3>
|
|
<button @click="settingsOpen=false" class="rounded-lg p-2 hover:bg-gray-100" aria-label="Close modal">
|
|
<i data-lucide="x" class="h-4 w-4"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<label class="text-sm text-ui-sub">OpenRouter API Key</label>
|
|
<input
|
|
x-model.trim="apiKeyInput"
|
|
type="password"
|
|
placeholder="sk-or-v1-..."
|
|
class="w-full rounded-xl border border-ui-border bg-white px-3 py-2"
|
|
/>
|
|
|
|
<div class="flex gap-2">
|
|
<button @click="saveApiKey()" class="rounded-xl bg-gray-900 text-white px-4 py-2">Save</button>
|
|
<button @click="clearApiKey()" class="rounded-xl border border-ui-border px-4 py-2">Clear</button>
|
|
</div>
|
|
|
|
<p class="text-xs text-ui-sub">Stored in localStorage on this browser.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module" src="./assets/js/app.js"></script>
|
|
</body>
|
|
</html>
|