mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 02:12:12 +00:00
Feat: full single-page UI scaffold
This commit is contained in:
307
index.html
307
index.html
@@ -1,210 +1,197 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>vibegif.lol</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="AI generated GIFs with BYOK OpenRouter. Minimal kawaii line doodles."
|
||||
/>
|
||||
|
||||
<!-- Tailwind -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
ui: {
|
||||
bg: "#fafafa",
|
||||
panel: "#ffffff",
|
||||
border: "#e5e7eb",
|
||||
text: "#111827",
|
||||
sub: "#6b7280",
|
||||
accent: "#374151"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<link rel="stylesheet" href="./assets/css/styles.css" />
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.js"></script>
|
||||
|
||||
<!-- Alpine -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="./assets/js/app.js" defer></script>
|
||||
|
||||
<!-- Lucide -->
|
||||
<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>
|
||||
|
||||
<link rel="stylesheet" href="./assets/css/styles.css" />
|
||||
</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">
|
||||
<body class="h-full bg-ui-bg text-ui-text">
|
||||
<div x-data="vibeGifApp()" x-init="init()" class="min-h-full">
|
||||
<!-- Top bar -->
|
||||
<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
|
||||
class="inline-flex items-center justify-center rounded-lg border border-gray-200 p-2 text-gray-600 hover:bg-gray-100"
|
||||
@click="openSettings()"
|
||||
@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"
|
||||
>
|
||||
<i data-lucide="panel-left" class="h-5 w-5"></i>
|
||||
<i data-lucide="panel-left" class="h-5 w-5 text-ui-accent"></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>
|
||||
<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 class="text-xs text-ui-sub">
|
||||
Stack: Tailwind · Alpine · Lucide
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto grid max-w-6xl gap-6 px-4 py-6 lg:grid-cols-2">
|
||||
<main class="mx-auto max-w-6xl px-4 py-6 grid gap-6 lg:grid-cols-3">
|
||||
<!-- 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>
|
||||
<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 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 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="grid gap-4 sm:grid-cols-2">
|
||||
<label class="field">
|
||||
<span class="label">Model</span>
|
||||
<select class="input" x-model="model" @change="normalizeSelections()">
|
||||
<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>
|
||||
<div class="space-y-1">
|
||||
<label class="text-sm text-ui-sub">Simple user prompt</label>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
x-model.trim="userPrompt"
|
||||
placeholder="rolling cat"
|
||||
maxlength="120"
|
||||
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"
|
||||
/>
|
||||
<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-text="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 class="flex items-center gap-2">
|
||||
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
||||
</svg>
|
||||
Generating…
|
||||
</span>
|
||||
</button>
|
||||
<span class="text-sm text-gray-600" x-text="progressText"></span>
|
||||
<p class="text-xs text-ui-sub">Keep it very simple.</p>
|
||||
</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>
|
||||
<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>
|
||||
<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" x-show="form.model === 'google/gemini-3.1-flash-image-preview'">0.5K</option>
|
||||
</select>
|
||||
</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>
|
||||
|
||||
<!-- Output -->
|
||||
<section class="card p-5">
|
||||
<h2 class="mb-3 text-lg font-semibold">Output</h2>
|
||||
<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>
|
||||
|
||||
<div x-show="frames.length === 0 && !isGenerating" 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()"
|
||||
>
|
||||
<i data-lucide="download" class="mr-2 h-4 w-4"></i>
|
||||
<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
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<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">
|
||||
<i data-lucide="x" class="h-4 w-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Account Settings Modal -->
|
||||
<div
|
||||
x-show="showSettings"
|
||||
x-cloak
|
||||
x-transition.opacity
|
||||
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" @click.stop>
|
||||
<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>
|
||||
<label class="text-sm text-ui-sub">OpenRouter API Key</label>
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
x-model.trim="apiKeyInput"
|
||||
type="password"
|
||||
placeholder="sk-or-v1-..."
|
||||
autocomplete="off"
|
||||
class="w-full rounded-xl border border-ui-border bg-white px-3 py-2"
|
||||
/>
|
||||
<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 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>
|
||||
|
||||
Reference in New Issue
Block a user