Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>AI Image Creator - Next Gen</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<script> | |
tailwind.config = { | |
theme: { | |
extend: { | |
colors: { | |
'neon-blue': '#00d4ff', | |
'neon-purple': '#8b5cf6', | |
'neon-pink': '#ec4899', | |
'dark-bg': '#0a0a0a', | |
'card-bg': '#1a1a1a', | |
'border-glow': '#00d4ff40' | |
}, | |
animation: { | |
'float': 'float 6s ease-in-out infinite', | |
'glow': 'glow 2s ease-in-out infinite alternate', | |
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', | |
} | |
} | |
} | |
} | |
</script> | |
<style> | |
@keyframes float { | |
0%, 100% { transform: translateY(0px); } | |
50% { transform: translateY(-20px); } | |
} | |
@keyframes glow { | |
from { box-shadow: 0 0 20px #00d4ff; } | |
to { box-shadow: 0 0 30px #00d4ff, 0 0 40px #00d4ff; } | |
} | |
.gradient-text { | |
background: linear-gradient(45deg, #00d4ff, #8b5cf6, #ec4899); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
background-clip: text; | |
} | |
.glass-effect { | |
background: rgba(255, 255, 255, 0.05); | |
backdrop-filter: blur(10px); | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
.neon-border { | |
border: 1px solid #00d4ff; | |
box-shadow: 0 0 10px #00d4ff40; | |
} | |
.neon-border:hover { | |
box-shadow: 0 0 20px #00d4ff, 0 0 30px #00d4ff40; | |
} | |
</style> | |
</head> | |
<body class="bg-dark-bg text-white min-h-screen font-sans"> | |
<main class="pt-20"> | |
<section class="min-h-screen flex flex-col items-center justify-center"> | |
<div class="max-w-xl w-full mx-auto px-4 py-12"> | |
<div class="text-center mb-10"> | |
<h2 class="text-5xl font-bold gradient-text mb-4 animate-float">AI Image Generator</h2> | |
<p class="text-lg text-gray-400 mb-6">Choose a model, enter your prompt, and create stunning images instantly!</p> | |
</div> | |
<div class="glass-effect rounded-3xl p-8 neon-border"> | |
<div class="mb-6"> | |
<label class="block mb-2 text-neon-blue font-semibold">Select Model</label> | |
<select id="modelSelect" class="w-full bg-card-bg border border-white/20 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-neon-blue" onchange="toggleFluxOptions()"> | |
<option value="heartsync">Heartsync (SSE, best for anime)</option> | |
<option value="flux">FLUX.1-dev (fast, general purpose)</option> | |
</select> | |
</div> | |
<div id="fluxOptions" class="mb-6 hidden"> | |
<label class="block mb-2 text-neon-blue font-semibold">Aspect Ratio (FLUX only)</label> | |
<select id="fluxAspect" class="w-full bg-card-bg border border-white/20 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-neon-blue" onchange="updateFluxResolution()"> | |
<option value="1:1" selected>1:1 (Square)</option> | |
<option value="16:9">16:9 (Landscape)</option> | |
<option value="9:16">9:16 (Portrait)</option> | |
<option value="4:3">4:3</option> | |
<option value="3:2">3:2</option> | |
<option value="2:3">2:3</option> | |
</select> | |
</div> | |
<div class="mb-6"> | |
<label class="block mb-2 text-neon-blue font-semibold">Prompt</label> | |
<textarea id="promptInput" class="w-full h-32 bg-card-bg border border-white/20 rounded-2xl px-6 py-4 text-white placeholder-gray-500 focus:outline-none focus:border-neon-blue focus:ring-2 focus:ring-neon-blue/20 resize-none" placeholder="Describe your vision... (Resolution options below are for FLUX only)"></textarea> | |
</div> | |
<div class="mb-6"> | |
<button id="generateBtn" onclick="generateImage()" class="w-full px-8 py-3 bg-gradient-to-r from-neon-blue to-neon-purple rounded-full font-semibold hover:opacity-90 transition-opacity animate-glow"> | |
<i class="fas fa-magic mr-2"></i>Generate | |
</button> | |
</div> | |
<div id="loadingState" class="hidden text-center py-6"> | |
<div class="inline-flex items-center space-x-4"> | |
<div class="w-8 h-8 border-4 border-neon-blue border-t-transparent rounded-full animate-spin"></div> | |
<span class="text-neon-blue text-lg">Generating image...</span> | |
</div> | |
<div id="queueInfo" class="mt-4 text-gray-400"></div> | |
</div> | |
<div id="resultSection" class="hidden mt-8"> | |
<div class="glass-effect rounded-2xl p-6 neon-border"> | |
<img id="generatedImage" class="w-full rounded-xl shadow-2xl" alt="Generated image"> | |
<div class="mt-6 flex justify-center"> | |
<button onclick="downloadImage()" class="px-6 py-3 glass-effect rounded-full hover:neon-border transition-all"> | |
<i class="fas fa-download mr-2"></i>Download | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</section> | |
</main> | |
<script> | |
let generatedImageUrl = null; | |
function showLoading(show, queueText = '') { | |
document.getElementById('loadingState').classList.toggle('hidden', !show); | |
document.getElementById('queueInfo').textContent = queueText; | |
} | |
function showResult(url) { | |
generatedImageUrl = url; | |
document.getElementById('generatedImage').src = url; | |
document.getElementById('resultSection').classList.remove('hidden'); | |
} | |
function hideResult() { | |
document.getElementById('resultSection').classList.add('hidden'); | |
} | |
function toggleFluxOptions() { | |
const model = document.getElementById('modelSelect').value; | |
document.getElementById('fluxOptions').classList.toggle('hidden', model !== 'flux'); | |
} | |
function updateFluxResolution() { | |
// This function is for future extensibility if you want to display the actual width/height to the user | |
} | |
function getFluxResolution() { | |
const aspect = document.getElementById('fluxAspect').value; | |
switch (aspect) { | |
case '1:1': return { width: 1024, height: 1024 }; | |
case '16:9': return { width: 1280, height: 720 }; | |
case '9:16': return { width: 720, height: 1280 }; | |
case '4:3': return { width: 1024, height: 768 }; | |
case '3:2': return { width: 1200, height: 800 }; | |
case '2:3': return { width: 800, height: 1200 }; | |
default: return { width: 1024, height: 1024 }; | |
} | |
} | |
async function generateImage() { | |
const prompt = document.getElementById('promptInput').value.trim(); | |
const model = document.getElementById('modelSelect').value; | |
let width, height; | |
if (!prompt) { | |
alert('Please enter a prompt'); | |
return; | |
} | |
hideResult(); | |
showLoading(true); | |
if (model === 'heartsync' || model === 'flux') { | |
// SSE logic for both models | |
if (model === 'flux') { | |
const res = getFluxResolution(); | |
width = res.width; | |
height = res.height; | |
} | |
try { | |
const url = model === 'heartsync' ? '/api/generate' : '/api/generate/flux'; | |
const body = model === 'heartsync' ? { prompt } : { prompt, width, height }; | |
const response = await fetch(url, { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify(body) | |
}); | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) break; | |
const chunk = decoder.decode(value); | |
const lines = chunk.split('\n'); | |
for (const line of lines) { | |
if (line.startsWith('data: ')) { | |
try { | |
const data = JSON.parse(line.substring(6)); | |
if (data.type === 'estimation') { | |
showLoading(true, `Queue position: ${data.queueSize}, ETA: ${data.eta}s`); | |
} else if (data.type === 'processing') { | |
showLoading(true, 'Processing your image...'); | |
} else if (data.type === 'success') { | |
showLoading(false); | |
if (model === 'flux') { | |
showResult(data.imageUrl); | |
} else { | |
showResult(data.originalUrl); | |
} | |
return; | |
} else if (data.type === 'error') { | |
throw new Error(data.message); | |
} | |
} catch (e) { console.error('Error parsing SSE data:', e); } | |
} | |
} | |
} | |
} catch (error) { | |
showLoading(false); | |
alert('Generation failed: ' + error.message); | |
} | |
} | |
} | |
function downloadImage() { | |
if (!generatedImageUrl) return; | |
const link = document.createElement('a'); | |
link.href = generatedImageUrl; | |
link.download = 'generated-image.jpg'; | |
link.click(); | |
} | |
window.onload = function() { toggleFluxOptions(); updateFluxResolution(); }; | |
</script> | |
</body> | |
</html> |