Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>LOKI.AI IMAGE PLAYGROUND</title> | |
<!-- Use more weights for DM Sans --> | |
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;0,9..40,800;1,9..40,400&display=swap" rel="stylesheet"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"> | |
<style> | |
:root { | |
--primary-bg: #ffffff; | |
--secondary-bg: #f7f7f9; /* Slightly different secondary */ | |
--text-color: #1a1a1a; /* Darker text */ | |
--text-muted: #666666; | |
--border-color: #e0e0e0; | |
--accent-color: #000000; | |
--accent-rgb: 0, 0, 0; /* For rgba usage */ | |
--primary-button-text: #ffffff; | |
--card-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); /* Softer, deeper shadow */ | |
--input-bg: #ffffff; | |
--transition-speed: 0.3s; | |
--transition-ease: ease-in-out; | |
} | |
.dark { | |
--primary-bg: #16161a; /* Slightly richer dark */ | |
--secondary-bg: #0d0d0f; | |
--text-color: #f0f0f0; | |
--text-muted: #a0a0a0; | |
--border-color: #3a3a40; | |
--accent-color: #ffffff; | |
--accent-rgb: 255, 255, 255; | |
--primary-button-text: #000000; | |
--card-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); | |
--input-bg: #242428; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
font-family: 'DM Sans', sans-serif; | |
} | |
body { | |
background-color: var(--secondary-bg); | |
color: var(--text-color); | |
transition: background-color var(--transition-speed) var(--transition-ease), color var(--transition-speed) var(--transition-ease); | |
padding: 40px 20px; /* More vertical padding */ | |
font-weight: 400; /* Base weight */ | |
line-height: 1.6; | |
} | |
.container { | |
max-width: 1100px; /* Slightly wider */ | |
margin: 0 auto; | |
} | |
.card { | |
background-color: var(--primary-bg); | |
border-radius: 16px; /* Larger radius */ | |
padding: 32px; /* More padding */ | |
box-shadow: var(--card-shadow); | |
transition: all var(--transition-speed) var(--transition-ease); | |
border: 1px solid var(--border-color); /* Subtle border */ | |
} | |
.dark .card { | |
border: 1px solid transparent; /* Remove border in dark mode if bg is dark enough */ | |
} | |
.header { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 32px; /* More space */ | |
flex-wrap: wrap; | |
gap: 16px; | |
} | |
.title { | |
font-size: 32px; /* Larger title */ | |
font-weight: 700; /* Bolder */ | |
color: var(--text-color); | |
letter-spacing: -0.5px; /* Slightly tighter */ | |
} | |
/* --- Theme Toggle --- */ | |
.theme-toggle { | |
display: flex; | |
align-items: center; | |
gap: 12px; /* More space */ | |
} | |
.toggle-label { | |
font-size: 14px; | |
font-weight: 500; | |
color: var(--text-muted); | |
transition: color var(--transition-speed) var(--transition-ease); | |
} | |
.toggle-switch { | |
position: relative; | |
width: 52px; /* Slightly larger */ | |
height: 28px; | |
background-color: var(--border-color); | |
border-radius: 14px; | |
cursor: pointer; | |
transition: background-color var(--transition-speed) var(--transition-ease); | |
} | |
.toggle-switch:hover { | |
background-color: color-mix(in srgb, var(--border-color) 80%, var(--accent-color) 20%); | |
} | |
.toggle-thumb { | |
position: absolute; | |
top: 3px; | |
left: 3px; | |
width: 22px; /* Larger */ | |
height: 22px; | |
border-radius: 50%; | |
background-color: white; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
transition: transform var(--transition-speed) var(--transition-ease), background-color var(--transition-speed) var(--transition-ease); | |
} | |
.dark .toggle-switch { | |
background-color: var(--border-color); /* Use border color for consistency */ | |
} | |
.dark .toggle-thumb { | |
transform: translateX(24px); | |
background-color: var(--secondary-bg); /* Match dark secondary bg */ | |
} | |
/* --- Form Styling --- */ | |
.form-row { | |
display: grid; | |
grid-template-columns: 1fr; | |
gap: 20px; /* Increased gap */ | |
margin-bottom: 24px; /* Increased gap */ | |
} | |
@media (min-width: 768px) { | |
.form-row { | |
grid-template-columns: 1fr 1fr; | |
} | |
.form-row.three-cols { | |
grid-template-columns: 1fr 1fr 1fr; | |
} | |
} | |
.form-group { | |
display: flex; | |
flex-direction: column; | |
gap: 8px; /* More space */ | |
} | |
.form-label { | |
font-size: 14px; | |
font-weight: 600; /* Bolder label */ | |
color: var(--text-color); | |
opacity: 0.9; | |
} | |
.form-control { | |
padding: 12px 16px; /* More padding */ | |
border-radius: 10px; /* Slightly larger radius */ | |
border: 1px solid var(--border-color); | |
background-color: var(--input-bg); | |
color: var(--text-color); | |
font-size: 15px; /* Slightly larger text */ | |
font-weight: 400; | |
transition: all var(--transition-speed) var(--transition-ease); | |
appearance: none; /* Remove default styling */ | |
width: 100%; | |
} | |
.form-control::placeholder { | |
color: var(--text-muted); | |
opacity: 0.7; | |
} | |
.form-control:hover { | |
border-color: color-mix(in srgb, var(--border-color) 70%, var(--text-color) 30%); | |
box-shadow: 0 2px 5px rgba(var(--accent-rgb), 0.05); | |
} | |
.form-control:focus, | |
.form-control:focus-visible { /* Use focus-visible for keyboard nav */ | |
outline: none; | |
border-color: var(--accent-color); | |
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15); /* More prominent focus ring */ | |
} | |
/* --- Select Wrapper --- */ | |
.select-wrapper { | |
position: relative; | |
} | |
.select-wrapper::after { | |
content: ''; | |
position: absolute; | |
top: 50%; | |
right: 16px; /* Adjusted position */ | |
width: 8px; /* Larger arrow */ | |
height: 8px; | |
border-style: solid; | |
border-width: 0 2px 2px 0; | |
border-color: var(--text-muted); | |
transform: translateY(-70%) rotate(45deg); /* Centered better */ | |
pointer-events: none; | |
transition: border-color var(--transition-speed) var(--transition-ease), transform var(--transition-speed) var(--transition-ease); | |
} | |
.select-wrapper:hover::after { | |
border-color: var(--text-color); | |
} | |
.select-wrapper select:focus + ::after { /* Style arrow on focus too */ | |
border-color: var(--accent-color); | |
} | |
/* --- Buttons --- */ | |
.btn { | |
padding: 12px 24px; /* More padding */ | |
border-radius: 10px; /* Match inputs */ | |
font-weight: 600; /* Bolder text */ | |
font-size: 15px; | |
cursor: pointer; | |
transition: all var(--transition-speed) var(--transition-ease); | |
border: none; | |
text-align: center; | |
display: inline-flex; /* Align icon/text if needed */ | |
align-items: center; | |
justify-content: center; | |
gap: 8px; | |
} | |
.btn-primary { | |
background-color: var(--accent-color); | |
color: var(--primary-button-text); | |
box-shadow: 0 4px 10px rgba(var(--accent-rgb), 0.15); | |
} | |
.btn-primary:hover { | |
background-color: color-mix(in srgb, var(--accent-color) 90%, var(--primary-bg) 10%); | |
transform: translateY(-3px); /* More lift */ | |
box-shadow: 0 7px 15px rgba(var(--accent-rgb), 0.25); /* Larger shadow on hover */ | |
} | |
.btn-primary:active { | |
transform: translateY(-1px); | |
box-shadow: 0 3px 8px rgba(var(--accent-rgb), 0.2); | |
} | |
.btn-primary:disabled { | |
opacity: 0.6; | |
cursor: not-allowed; | |
transform: translateY(0); | |
box-shadow: none; | |
} | |
/* --- Download Button on Image --- */ | |
.btn-download { | |
background-color: rgba(255, 255, 255, 0.3); | |
backdrop-filter: blur(8px); /* Stronger blur */ | |
-webkit-backdrop-filter: blur(8px); /* Safari */ | |
padding: 10px; /* Slightly larger */ | |
border-radius: 50%; | |
position: absolute; | |
bottom: 12px; | |
right: 12px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15); | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
opacity: 0.8; | |
transform: scale(0.95); | |
transition: all var(--transition-speed) var(--transition-ease); | |
} | |
.dark .btn-download { | |
background-color: rgba(0, 0, 0, 0.3); | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
.image-wrapper:hover .btn-download { | |
opacity: 1; | |
transform: scale(1); | |
} | |
.btn-download:hover { | |
background-color: rgba(255, 255, 255, 0.5); | |
transform: scale(1.05) ; /* Override wrapper hover scale */ | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
} | |
.dark .btn-download:hover { | |
background-color: rgba(0, 0, 0, 0.5); | |
} | |
.btn-download svg { | |
width: 22px; /* Larger icon */ | |
height: 22px; | |
stroke: var(--accent-color); /* Use accent color for icon */ | |
stroke-width: 2; | |
} | |
.btn-full { | |
width: 100%; | |
height: 48px; /* Match input height */ | |
} | |
/* --- Result Area --- */ | |
.result { | |
margin-top: 40px; /* More space */ | |
display: none; /* Initially hidden */ | |
} | |
.result-title { | |
font-size: 22px; /* Larger */ | |
font-weight: 700; /* Bold */ | |
margin-bottom: 6px; | |
} | |
.result-subtitle { | |
font-size: 15px; | |
color: var(--text-muted); | |
margin-bottom: 24px; /* More space */ | |
} | |
.result-subtitle span { | |
font-weight: 600; | |
color: var(--text-color); | |
} | |
.images-grid { | |
display: grid; | |
grid-template-columns: 1fr; | |
gap: 24px; /* More gap */ | |
} | |
.images-grid.multi-column { | |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* Responsive grid */ | |
@media (min-width: 768px) { | |
grid-template-columns: repeat(2, 1fr); /* Force 2 columns on medium+ */ | |
} | |
} | |
.image-wrapper { | |
position: relative; | |
border-radius: 12px; /* Consistent radius */ | |
overflow: hidden; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
transition: transform var(--transition-speed) var(--transition-ease), box-shadow var(--transition-speed) var(--transition-ease); | |
background-color: var(--secondary-bg); /* Placeholder bg */ | |
} | |
.dark .image-wrapper { | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); | |
} | |
.image-wrapper img { | |
width: 100%; | |
height: auto; | |
display: block; | |
transition: transform var(--transition-speed) var(--transition-ease), filter 0.4s ease; /* Smoother, slightly longer transform */ | |
} | |
.image-wrapper:hover { | |
transform: translateY(-5px); /* Lift effect */ | |
box-shadow: var(--card-shadow); /* Use main card shadow on hover */ | |
} | |
.image-wrapper:hover img { | |
transform: scale(1.05); /* Slightly larger scale */ | |
filter: brightness(1.02); /* Subtle brightness */ | |
} | |
/* --- Loading --- */ | |
.loading { | |
display: none; /* Initially hidden */ | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
padding: 48px 0; /* More padding */ | |
} | |
.loading-spinner { | |
width: 48px; /* Larger */ | |
height: 48px; | |
border: 5px solid rgba(var(--accent-rgb), 0.1); | |
border-left-color: var(--accent-color); | |
border-radius: 50%; | |
animation: spinner 0.8s linear infinite; /* Faster spin */ | |
margin-bottom: 20px; /* More space */ | |
} | |
@keyframes spinner { | |
to { | |
transform: rotate(360deg); | |
} | |
} | |
.loading-text { | |
font-size: 15px; | |
color: var(--text-muted); | |
font-weight: 500; | |
} | |
/* --- Error --- */ | |
.error { | |
background-color: rgba(255, 87, 87, 0.1); /* Standard red, subtle bg */ | |
color: #d93030; /* Darker red for text */ | |
padding: 20px 24px; /* More padding */ | |
border-radius: 12px; /* Consistent radius */ | |
margin-top: 32px; | |
display: none; /* Initially hidden */ | |
border: 1px solid rgba(255, 87, 87, 0.3); | |
} | |
.dark .error { | |
background-color: rgba(255, 87, 87, 0.1); | |
color: #ff8a8a; /* Lighter red in dark mode */ | |
border-color: rgba(255, 87, 87, 0.2); | |
} | |
.error-title { | |
font-size: 17px; /* Slightly larger */ | |
font-weight: 700; /* Bold */ | |
margin-bottom: 8px; | |
} | |
.error-message { | |
font-size: 15px; | |
opacity: 0.9; | |
} | |
/* --- Footer --- */ | |
.footer { | |
margin-top: 40px; /* More space */ | |
text-align: center; | |
font-size: 13px; /* Slightly larger */ | |
color: var(--text-muted); | |
transition: color var(--transition-speed) var(--transition-ease); | |
} | |
/* --- Confetti (keep as is) --- */ | |
.confetti { | |
position: fixed; | |
width: 10px; | |
height: 10px; | |
background-color: #f00; | |
opacity: 0; | |
top: 0; | |
left: 0; | |
pointer-events: none; /* Prevent interaction */ | |
z-index: 9999; /* Ensure it's on top */ | |
} | |
/* --- Utility --- */ | |
.text-center { | |
text-align: center; | |
} | |
/* --- Animation Delays (keep using inline styles for flexibility) --- */ | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<!-- Card gets initial fade in --> | |
<div class="card animate__animated animate__fadeIn animate__delay-0.2s"> | |
<div class="header"> | |
<!-- Title slides in --> | |
<h1 class="title animate__animated animate__fadeInLeft animate__delay-0.3s">LOKI.AI IMAGE PLAYGROUND</h1> | |
<!-- Theme toggle fades in --> | |
<div class="theme-toggle animate__animated animate__fadeInRight animate__delay-0.4s"> | |
<span class="toggle-label">Theme</span> | |
<div id="theme-toggle" class="toggle-switch"> | |
<div class="toggle-thumb"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Form rows fade up with delays --> | |
<div class="form-row animate__animated animate__fadeInUp animate__delay-0.5s"> | |
<div class="form-group"> | |
<label for="model" class="form-label">Select Model</label> | |
<div class="select-wrapper"> | |
<select id="model" class="form-control"> | |
<option value="Flux Realism">Flux Realism</option> | |
<option value="Flux Pro Ultra">Flux Pro Ultra</option> | |
<option value="grok-2-aurora">grok-2-aurora</option> | |
<option value="Flux Pro">Flux Pro</option> | |
<option value="Flux Pro Ultra Raw">Flux Pro Ultra Raw</option> | |
<option value="Flux Dev">Flux Dev</option> | |
<option value="Flux Schnell">Flux Schnell</option> | |
<option value="stable-diffusion-3-large-turbo">stable-diffusion-3-large-turbo</option> | |
<option value="sdxl-lightning-4step">sdxl-lightning-4step</option> | |
<option value="dall-e-3">dall-e-3</option> | |
</select> | |
</div> | |
</div> | |
<div class="form-group"> | |
<label for="prompt" class="form-label">Enter Prompt</label> | |
<input type="text" id="prompt" class="form-control" placeholder="Describe what you want to see..." value="cinematic shot, mystical forest path at twilight, glowing mushrooms"> | |
</div> | |
</div> | |
<div class="form-row three-cols animate__animated animate__fadeInUp animate__delay-0.6s"> | |
<div class="form-group"> | |
<label for="image-size" class="form-label">Image Size</label> | |
<div class="select-wrapper"> | |
<select id="image-size" class="form-control"> | |
<option value="512">512 x 512</option> | |
<option value="768">768 x 768</option> | |
<option value="1024" selected>1024 x 1024</option> | |
<option value="1536">1536 x 1536</option> | |
</select> | |
</div> | |
</div> | |
<div class="form-group"> | |
<label for="image-count" class="form-label">Number of Images</label> | |
<div class="select-wrapper"> | |
<select id="image-count" class="form-control"> | |
<option value="1" selected>1</option> | |
<option value="2">2</option> | |
<option value="4">4</option> | |
</select> | |
</div> | |
</div> | |
<div class="form-group"> | |
<!-- Label for alignment, content via button --> | |
<label class="form-label"> </label> | |
<!-- Removed infinite pulse, rely on hover/active states --> | |
<button id="generate" class="btn btn-primary btn-full"> | |
Generate Image | |
</button> | |
</div> | |
</div> | |
<!-- Result area will fade in when populated --> | |
<div id="result" class="result"> | |
<div class="text-center animate__animated animate__fadeIn"> | |
<h2 class="result-title">Your Creation</h2> | |
<p class="result-subtitle">Created with <span id="model-used"></span></p> | |
</div> | |
<div id="images-container" class="images-grid"> | |
<!-- Images will be added here dynamically --> | |
</div> | |
</div> | |
<!-- Loading indicator will fade in/out --> | |
<div id="loading" class="loading animate__animated"> | |
<div class="loading-spinner"></div> | |
<p class="loading-text">Conjuring pixels... Please wait.</p> | |
</div> | |
<!-- Error message will fade in --> | |
<div id="error" class="error animate__animated"> | |
<h3 class="error-title">Oops! Something went wrong.</h3> | |
<p class="error-message">Please try adjusting your prompt or try again later.</p> | |
</div> | |
</div> | |
<div class="footer animate__animated animate__fadeInUp animate__delay-0.8s"> | |
© 2025 LOKI.AI IMAGE PLAYGROUND | All rights reserved | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// Elements | |
const themeToggle = document.getElementById('theme-toggle'); | |
const generateBtn = document.getElementById('generate'); | |
const promptInput = document.getElementById('prompt'); | |
const modelSelect = document.getElementById('model'); | |
const imageSizeSelect = document.getElementById('image-size'); | |
const imageCountSelect = document.getElementById('image-count'); | |
const resultDiv = document.getElementById('result'); | |
const loadingDiv = document.getElementById('loading'); | |
const errorDiv = document.getElementById('error'); | |
const imagesContainer = document.getElementById('images-container'); | |
const modelUsed = document.getElementById('model-used'); | |
const body = document.body; // Reference body for class toggling | |
// --- Theme Logic --- | |
const applyTheme = (theme) => { | |
if (theme === 'dark') { | |
body.classList.add('dark'); | |
} else { | |
body.classList.remove('dark'); | |
} | |
localStorage.setItem('theme', theme); | |
}; | |
themeToggle.addEventListener('click', () => { | |
const currentTheme = body.classList.contains('dark') ? 'light' : 'dark'; | |
applyTheme(currentTheme); | |
}); | |
// Check system preference and saved theme | |
const savedTheme = localStorage.getItem('theme'); | |
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | |
if (savedTheme) { | |
applyTheme(savedTheme); | |
} else { | |
applyTheme(prefersDark ? 'dark' : 'light'); | |
} | |
// --- Confetti --- | |
function createConfetti() { | |
const colors = ['#ff5757', '#57ff87', '#5787ff', '#f0ff57', '#ff57f0', '#57f0ff']; // Adjusted colors | |
const confettiCount = 100; // Keep it reasonable | |
for (let i = 0; i < confettiCount; i++) { | |
const confetti = document.createElement('div'); | |
confetti.className = 'confetti'; | |
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)]; | |
confetti.style.left = Math.random() * 100 + 'vw'; | |
confetti.style.opacity = Math.random() * 0.5 + 0.5; // 0.5 to 1.0 | |
confetti.style.width = Math.random() * 8 + 6 + 'px'; // 6px to 14px | |
confetti.style.height = confetti.style.width; // Keep square-ish | |
confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0'; // Mix circles and squares | |
document.body.appendChild(confetti); | |
const fallDuration = Math.random() * 3000 + 3000; // 3-6 seconds | |
const rotation = Math.random() * 720 - 360; // Random rotation | |
const animation = confetti.animate([ | |
{ transform: `translateY(-20px) rotate(0deg)`, opacity: 1 }, // Start slightly above | |
{ transform: `translateY(${window.innerHeight + 20}px) rotate(${rotation}deg)`, opacity: 0 } | |
], { | |
duration: fallDuration, | |
easing: 'ease-out' // More natural fall | |
}); | |
animation.onfinish = () => confetti.remove(); | |
} | |
} | |
// --- Form Validation --- | |
const validateInput = (inputElement) => { | |
if (!inputElement.value.trim()) { | |
inputElement.style.borderColor = '#d93030'; // Use error color | |
inputElement.classList.add('animate__animated', 'animate__headShake'); // More subtle shake | |
setTimeout(() => { | |
inputElement.style.borderColor = ''; // Reset border color | |
inputElement.classList.remove('animate__animated', 'animate__headShake'); | |
}, 1000); | |
return false; | |
} | |
inputElement.style.borderColor = ''; // Ensure reset if valid | |
return true; | |
} | |
// --- Image Generation --- | |
generateBtn.addEventListener('click', async () => { | |
// Basic validation | |
if (!validateInput(promptInput)) return; | |
const prompt = promptInput.value.trim(); | |
const model = modelSelect.value; | |
const size = parseInt(imageSizeSelect.value); | |
const number = parseInt(imageCountSelect.value); | |
// UI updates for loading state | |
resultDiv.style.display = 'none'; | |
errorDiv.style.display = 'none'; | |
errorDiv.classList.remove('animate__fadeIn'); // Reset animation class | |
loadingDiv.style.display = 'flex'; | |
loadingDiv.classList.remove('animate__fadeOut'); | |
loadingDiv.classList.add('animate__fadeIn'); | |
generateBtn.disabled = true; | |
generateBtn.textContent = 'Generating...'; // Change button text | |
try { | |
const response = await fetch('https://parthsadaria-lokiai.hf.space/images/generations', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
model: model, | |
prompt: prompt, | |
size: size, | |
number: number | |
}) | |
}); | |
// Handle API errors more gracefully | |
if (!response.ok) { | |
let errorMsg = `API request failed with status ${response.status}.`; | |
try { | |
const errorData = await response.json(); | |
errorMsg += ` ${errorData.detail || ''}`; | |
} catch (e) { /* Ignore if response body is not JSON */ } | |
throw new Error(errorMsg); | |
} | |
const data = await response.json(); | |
// --- Display Results --- | |
loadingDiv.classList.replace('animate__fadeIn', 'animate__fadeOut'); | |
loadingDiv.addEventListener('animationend', () => { | |
loadingDiv.style.display = 'none'; | |
}, { once: true }); // Ensure runs only once | |
modelUsed.textContent = model; | |
// Clear previous images | |
imagesContainer.innerHTML = ''; | |
// Set grid columns (using auto-fit now, but keep class for potential overrides) | |
imagesContainer.className = number > 1 ? 'images-grid multi-column' : 'images-grid'; | |
// Add new images with animation | |
if (data.data && data.data.length > 0) { | |
data.data.forEach((item, index) => { | |
const imgWrapper = document.createElement('div'); | |
// Use fadeInUp for a nicer entrance | |
imgWrapper.className = 'image-wrapper animate__animated animate__fadeInUp'; | |
imgWrapper.style.animationDelay = `${index * 0.15}s`; // Slightly faster stagger | |
const img = document.createElement('img'); | |
img.src = item.url; | |
img.alt = `Generated image ${index + 1} for prompt: ${prompt}`; | |
img.loading = 'lazy'; // Improve performance for many images | |
const downloadBtn = document.createElement('button'); | |
downloadBtn.className = 'btn-download'; | |
downloadBtn.setAttribute('aria-label', 'Download image'); // Accessibility | |
downloadBtn.innerHTML = ` | |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
<polyline points="7 10 12 15 17 10"></polyline> | |
<line x1="12" y1="15" x2="12" y2="3"></line> | |
</svg> | |
`; | |
downloadBtn.addEventListener('click', (e) => { | |
e.stopPropagation(); // Prevent triggering wrapper hover effects if any | |
const a = document.createElement('a'); | |
a.href = item.url; | |
// Create a safer filename | |
const safePrompt = prompt.substring(0, 20).replace(/[^a-z0-9]/gi, '_').toLowerCase(); | |
a.download = `loki-ai-${model}-${safePrompt}-${index + 1}.jpg`; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
}); | |
imgWrapper.appendChild(img); | |
imgWrapper.appendChild(downloadBtn); | |
imagesContainer.appendChild(imgWrapper); | |
}); | |
// Show result section after images are ready to be animated | |
resultDiv.style.display = 'block'; | |
resultDiv.classList.add('animate__animated', 'animate__fadeIn'); | |
// Trigger confetti only on success | |
createConfetti(); | |
} else { | |
throw new Error("Received empty data from API."); // Handle cases with OK status but no images | |
} | |
} catch (error) { | |
console.error('Error generating image:', error); | |
loadingDiv.style.display = 'none'; // Ensure loading is hidden on error | |
loadingDiv.classList.remove('animate__fadeIn', 'animate__fadeOut'); | |
// Display error message | |
const errorMessageElement = errorDiv.querySelector('.error-message'); | |
errorMessageElement.textContent = error.message || 'An unknown error occurred. Please check the console or try again.'; | |
errorDiv.style.display = 'block'; | |
errorDiv.classList.add('animate__animated', 'animate__fadeIn'); | |
} finally { | |
// Reset button state regardless of success/error | |
generateBtn.disabled = false; | |
generateBtn.textContent = 'Generate Image'; | |
} | |
}); | |
// --- Enter Key Submission --- | |
promptInput.addEventListener('keypress', (e) => { | |
if (e.key === 'Enter' && !generateBtn.disabled) { // Prevent multiple submits | |
e.preventDefault(); // Prevent potential form submission | |
generateBtn.click(); | |
} | |
}); | |
// --- Initial Animation Trigger --- | |
// Small delay to ensure CSS is loaded before animations start | |
setTimeout(() => { | |
document.querySelectorAll('.animate__animated').forEach(el => { | |
// This is mostly handled by animate.css, but ensures visibility if needed | |
// You might not strictly need this if using animate.css correctly | |
}); | |
}, 100); | |
}); | |
</script> | |
</body> | |
</html> |