mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 02:12:12 +00:00
187 lines
9.3 KiB
HTML
187 lines
9.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>vibegif.lol — AI Generated Gifs</title>
|
|
<link rel="stylesheet" href="/styles.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js"></script>
|
|
</head>
|
|
<body class="min-h-screen bg-white text-neutral-800" x-data="app()" x-init="init()">
|
|
|
|
<!-- Header -->
|
|
<header class="fixed top-0 inset-x-0 z-50 bg-white/80 backdrop-blur border-b border-neutral-200">
|
|
<div class="max-w-2xl mx-auto px-4 h-14 flex items-center justify-between">
|
|
<h1 class="text-2xl tracking-tight">vibegif<span class="text-neutral-400">.lol</span></h1>
|
|
<button @click="showSettings = true" class="p-2 rounded-lg hover:bg-neutral-100 transition">
|
|
<i data-lucide="panel-left" class="w-5 h-5 text-neutral-500"></i>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Settings Modal -->
|
|
<template x-if="showSettings">
|
|
<div class="fixed inset-0 z-[60] flex items-center justify-center bg-black/30 backdrop-blur-sm" @click.self="showSettings = false">
|
|
<div class="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6" @click.stop>
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl">account settings</h2>
|
|
<button @click="showSettings = false" class="p-1 rounded-lg hover:bg-neutral-100">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
<label class="block text-sm text-neutral-500 mb-2">openrouter api key</label>
|
|
<input type="password" x-model="apiKey" @input="saveApiKey()"
|
|
placeholder="sk-or-..."
|
|
class="w-full px-4 py-3 rounded-xl border border-neutral-200 bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300 font-mono text-sm">
|
|
<p class="mt-3 text-xs text-neutral-400">your key is stored locally and never sent anywhere except openrouter.</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Main Content -->
|
|
<main class="pt-20 pb-32 px-4 max-w-2xl mx-auto">
|
|
|
|
<!-- Onboarding / Key Prompt -->
|
|
<template x-if="!apiKey">
|
|
<div class="text-center py-20">
|
|
<div class="text-6xl mb-4">✏️</div>
|
|
<h2 class="text-2xl mb-2">welcome to vibegif</h2>
|
|
<p class="text-neutral-500 mb-6">ai-generated gifs from simple prompts</p>
|
|
<label class="block text-sm text-neutral-500 mb-2">enter your openrouter api key to start</label>
|
|
<input type="password" x-model="apiKey" @input="saveApiKey()"
|
|
placeholder="sk-or-..."
|
|
class="w-full max-w-sm mx-auto px-4 py-3 rounded-xl border border-neutral-200 bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300 font-mono text-sm">
|
|
<p class="mt-3 text-xs text-neutral-400">get one at <a href="https://openrouter.ai/keys" target="_blank" class="underline">openrouter.ai/keys</a></p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Generator -->
|
|
<template x-if="apiKey">
|
|
<div>
|
|
<!-- Prompt Input -->
|
|
<div class="space-y-4 mb-8">
|
|
<div>
|
|
<label class="block text-sm text-neutral-500 mb-2">prompt</label>
|
|
<input type="text" x-model="userPrompt"
|
|
placeholder="rolling cat"
|
|
class="w-full px-4 py-3 rounded-xl border border-neutral-200 bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300"
|
|
:disabled="generating">
|
|
<p class="mt-1 text-xs text-neutral-400">keep it simple — e.g. "dancing star", "bouncing ball", "waving hand"</p>
|
|
</div>
|
|
|
|
<!-- Options Row -->
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
<!-- Model -->
|
|
<div>
|
|
<label class="block text-xs text-neutral-500 mb-1">model</label>
|
|
<select x-model="model" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none" :disabled="generating">
|
|
<option value="google/gemini-3.1-flash-image-preview">gemini flash</option>
|
|
<option value="bytedance-seed/seedream-4.5">seedream 4.5</option>
|
|
</select>
|
|
</div>
|
|
<!-- Frames -->
|
|
<div>
|
|
<label class="block text-xs text-neutral-500 mb-1">frames</label>
|
|
<select x-model.number="frameCount" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none" :disabled="generating">
|
|
<template x-for="n in [2,3,4,5,6,7,8]" :key="n">
|
|
<option :value="n" x-text="n"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<!-- FPS -->
|
|
<div>
|
|
<label class="block text-xs text-neutral-500 mb-1">fps</label>
|
|
<select x-model.number="fps" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none" :disabled="generating">
|
|
<template x-for="f in [2,4,6,8,10,12]" :key="f">
|
|
<option :value="f" x-text="f"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<!-- Size -->
|
|
<div>
|
|
<label class="block text-xs text-neutral-500 mb-1">size</label>
|
|
<select x-model="imageSize" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none" :disabled="generating">
|
|
<option value="1K">1K</option>
|
|
<option value="0.5K" x-show="model === 'google/gemini-3.1-flash-image-preview'">0.5K</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Aspect Ratio -->
|
|
<div>
|
|
<label class="block text-xs text-neutral-500 mb-1">aspect ratio</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
<template x-for="ar in ['1:1','16:9','9:16','4:3','3:4']" :key="ar">
|
|
<button
|
|
@click="aspectRatio = ar"
|
|
:class="aspectRatio === ar ? 'bg-neutral-800 text-white' : 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200'"
|
|
class="px-3 py-1.5 rounded-lg text-xs font-medium transition"
|
|
:disabled="generating"
|
|
x-text="ar">
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Generate Button -->
|
|
<button @click="generate()"
|
|
:disabled="generating || !userPrompt.trim()"
|
|
class="w-full py-3 rounded-xl font-medium transition"
|
|
:class="generating || !userPrompt.trim() ? 'bg-neutral-100 text-neutral-400 cursor-not-allowed' : 'bg-neutral-800 text-white hover:bg-neutral-700'">
|
|
<span x-show="!generating">generate gif</span>
|
|
<span x-show="generating" class="flex items-center justify-center gap-2">
|
|
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
|
<span x-text="statusText">generating...</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Frames Preview -->
|
|
<div x-show="frames.length > 0" class="mb-8">
|
|
<h3 class="text-sm text-neutral-500 mb-3">frames <span x-text="frames.length + '/' + frameCount"></span></h3>
|
|
<div class="grid grid-cols-4 gap-2">
|
|
<template x-for="(frame, i) in frames" :key="i">
|
|
<div class="aspect-square rounded-lg overflow-hidden border border-neutral-200 bg-neutral-50">
|
|
<img :src="frame" class="w-full h-full object-cover">
|
|
</div>
|
|
</template>
|
|
<template x-for="i in Math.max(0, frameCount - frames.length)" :key="'p'+i">
|
|
<div class="aspect-square rounded-lg border border-dashed border-neutral-200 bg-neutral-50 flex items-center justify-center">
|
|
<div x-show="generating && i === 1" class="w-5 h-5 border-2 border-neutral-300 border-t-neutral-600 rounded-full animate-spin"></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Result -->
|
|
<div x-show="gifUrl" class="text-center">
|
|
<h3 class="text-sm text-neutral-500 mb-3">your gif</h3>
|
|
<div class="inline-block rounded-2xl overflow-hidden border border-neutral-200 shadow-lg">
|
|
<img :src="gifUrl" class="max-w-full">
|
|
</div>
|
|
<div class="mt-4">
|
|
<a :href="gifUrl" :download="'vibegif-' + Date.now() + '.gif'"
|
|
class="inline-flex items-center gap-2 px-6 py-3 bg-neutral-800 text-white rounded-xl hover:bg-neutral-700 transition">
|
|
<i data-lucide="download" class="w-4 h-4"></i>
|
|
download gif
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<div x-show="errorMsg" class="mt-4 p-4 rounded-xl bg-red-50 text-red-600 text-sm" x-text="errorMsg"></div>
|
|
</div>
|
|
</template>
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="fixed bottom-0 inset-x-0 bg-white/80 backdrop-blur border-t border-neutral-100 py-3 text-center text-xs text-neutral-400">
|
|
byok · your key never leaves your browser
|
|
</footer>
|
|
|
|
<script src="/app.js"></script>
|
|
</body>
|
|
</html>
|