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://huggingface.co/proxy/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> |