lokiai / image-playground.html
ParthSadaria's picture
Update image-playground.html
58ebc79 verified
<!DOCTYPE html>
<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) !important; /* 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>