ParthSadaria commited on
Commit
58ebc79
·
verified ·
1 Parent(s): 1100b6b

Update image-playground.html

Browse files
Files changed (1) hide show
  1. image-playground.html +407 -217
image-playground.html CHANGED
@@ -4,25 +4,36 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>LOKI.AI IMAGE PLAYGROUND</title>
7
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400&display=swap" rel="stylesheet">
 
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
9
  <style>
10
  :root {
11
  --primary-bg: #ffffff;
12
- --secondary-bg: #f8f8f8;
13
- --text-color: #000000;
 
14
  --border-color: #e0e0e0;
15
  --accent-color: #000000;
16
- --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 
 
 
 
 
17
  }
18
 
19
  .dark {
20
- --primary-bg: #121212;
21
- --secondary-bg: #1e1e1e;
22
- --text-color: #ffffff;
23
- --border-color: #333333;
 
24
  --accent-color: #ffffff;
25
- --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
 
 
 
26
  }
27
 
28
  * {
@@ -35,85 +46,102 @@
35
  body {
36
  background-color: var(--secondary-bg);
37
  color: var(--text-color);
38
- transition: all 0.3s ease;
39
- padding: 20px;
 
 
40
  }
41
 
42
  .container {
43
- max-width: 1000px;
44
  margin: 0 auto;
45
  }
46
 
47
  .card {
48
  background-color: var(--primary-bg);
49
- border-radius: 12px;
50
- padding: 24px;
51
  box-shadow: var(--card-shadow);
52
- transition: all 0.3s ease;
 
53
  }
54
 
 
 
 
 
 
55
  .header {
56
  display: flex;
57
  justify-content: space-between;
58
  align-items: center;
59
- margin-bottom: 24px;
60
  flex-wrap: wrap;
61
- gap: 12px;
62
  }
63
 
64
  .title {
65
- font-size: 28px;
66
- font-weight: 700;
67
  color: var(--text-color);
 
68
  }
69
 
 
70
  .theme-toggle {
71
  display: flex;
72
  align-items: center;
73
- gap: 8px;
74
  }
75
 
76
  .toggle-label {
77
  font-size: 14px;
78
- color: var(--text-color);
79
- opacity: 0.7;
 
80
  }
81
 
82
  .toggle-switch {
83
  position: relative;
84
- width: 48px;
85
- height: 24px;
86
- background-color: #e5e5e5;
87
- border-radius: 12px;
88
  cursor: pointer;
89
- transition: background-color 0.3s;
90
  }
91
-
92
- .dark .toggle-switch {
93
- background-color: #555;
94
  }
95
 
 
96
  .toggle-thumb {
97
  position: absolute;
98
- top: 2px;
99
- left: 2px;
100
- width: 20px;
101
- height: 20px;
102
  border-radius: 50%;
103
  background-color: white;
104
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
105
- transition: transform 0.3s;
106
  }
107
 
 
 
 
108
  .dark .toggle-thumb {
109
- transform: translateX(24px);
 
110
  }
111
 
 
 
112
  .form-row {
113
  display: grid;
114
  grid-template-columns: 1fr;
115
- gap: 16px;
116
- margin-bottom: 16px;
117
  }
118
 
119
  @media (min-width: 768px) {
@@ -128,182 +156,247 @@
128
  .form-group {
129
  display: flex;
130
  flex-direction: column;
131
- gap: 6px;
132
  }
133
 
134
  .form-label {
135
  font-size: 14px;
136
- font-weight: 500;
137
  color: var(--text-color);
138
- opacity: 0.8;
139
  }
140
 
141
  .form-control {
142
- padding: 10px 12px;
143
- border-radius: 8px;
144
  border: 1px solid var(--border-color);
145
- background-color: var(--secondary-bg);
146
  color: var(--text-color);
147
- font-size: 14px;
148
- transition: all 0.2s;
149
- appearance: none;
 
 
150
  }
151
 
152
- .form-control:focus {
153
- outline: none;
154
- border-color: var(--accent-color);
155
- box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
156
  }
157
 
158
- .dark .form-control:focus {
159
- box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
 
160
  }
161
 
 
 
 
 
 
 
 
 
162
  .select-wrapper {
163
  position: relative;
164
  }
165
 
166
- .select-wrapper:after {
167
  content: '';
168
  position: absolute;
169
  top: 50%;
170
- right: 12px;
171
- transform: translateY(-50%);
172
- width: 0;
173
- height: 0;
174
- border-left: 5px solid transparent;
175
- border-right: 5px solid transparent;
176
- border-top: 5px solid var(--text-color);
177
  pointer-events: none;
 
 
 
 
 
 
 
 
178
  }
179
 
 
 
180
  .btn {
181
- padding: 10px 18px;
182
- border-radius: 8px;
183
- font-weight: 500;
 
184
  cursor: pointer;
185
- transition: all 0.2s;
186
  border: none;
187
  text-align: center;
 
 
 
 
188
  }
189
 
190
  .btn-primary {
191
  background-color: var(--accent-color);
192
- color: var(--primary-bg);
 
193
  }
194
 
195
  .btn-primary:hover {
196
- opacity: 0.9;
197
- transform: translateY(-2px);
 
198
  }
199
 
200
  .btn-primary:active {
201
- transform: translateY(0);
 
 
 
 
 
 
 
 
202
  }
203
 
 
204
  .btn-download {
205
- background-color: rgba(255, 255, 255, 0.2);
206
- backdrop-filter: blur(4px);
207
- padding: 8px;
 
208
  border-radius: 50%;
209
  position: absolute;
210
- bottom: 10px;
211
- right: 10px;
212
  display: flex;
213
  align-items: center;
214
  justify-content: center;
215
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
 
 
 
 
216
  }
217
 
218
  .dark .btn-download {
219
  background-color: rgba(0, 0, 0, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  }
221
 
222
  .btn-download svg {
223
- width: 20px;
224
- height: 20px;
225
- stroke: var(--accent-color);
 
226
  }
227
 
228
  .btn-full {
229
  width: 100%;
 
230
  }
231
 
232
- .text-center {
233
- text-align: center;
234
- }
235
-
236
  .result {
237
- margin-top: 24px;
238
- display: none;
239
  }
240
 
241
  .result-title {
242
- font-size: 18px;
243
- font-weight: 500;
244
- margin-bottom: 4px;
245
  }
246
 
247
  .result-subtitle {
248
- font-size: 14px;
249
- opacity: 0.7;
250
- margin-bottom: 16px;
 
 
 
 
251
  }
252
 
253
  .images-grid {
254
  display: grid;
255
  grid-template-columns: 1fr;
256
- gap: 16px;
257
  }
258
 
259
  .images-grid.multi-column {
260
- grid-template-columns: repeat(2, 1fr);
 
 
 
261
  }
262
 
263
  .image-wrapper {
264
  position: relative;
265
- border-radius: 8px;
266
  overflow: hidden;
267
- box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
268
- transform-origin: center;
 
269
  }
270
 
271
  .dark .image-wrapper {
272
- box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
273
  }
274
 
275
  .image-wrapper img {
276
  width: 100%;
277
  height: auto;
278
  display: block;
279
- transition: transform 0.3s ease;
 
 
 
 
 
280
  }
281
 
282
  .image-wrapper:hover img {
283
- transform: scale(1.03);
 
284
  }
285
 
 
286
  .loading {
287
- display: none;
288
  flex-direction: column;
289
  align-items: center;
290
  justify-content: center;
291
- padding: 32px 0;
292
  }
293
 
294
  .loading-spinner {
295
- width: 40px;
296
- height: 40px;
297
- border: 4px solid rgba(0, 0, 0, 0.1);
298
  border-left-color: var(--accent-color);
299
  border-radius: 50%;
300
- animation: spinner 1s linear infinite;
301
- margin-bottom: 16px;
302
- }
303
-
304
- .dark .loading-spinner {
305
- border-color: rgba(255, 255, 255, 0.1);
306
- border-left-color: var(--accent-color);
307
  }
308
 
309
  @keyframes spinner {
@@ -313,41 +406,49 @@
313
  }
314
 
315
  .loading-text {
316
- font-size: 14px;
317
- opacity: 0.7;
 
318
  }
319
 
 
320
  .error {
321
- background-color: rgba(255, 0, 0, 0.1);
322
- color: #e53e3e;
323
- padding: 16px;
324
- border-radius: 8px;
325
- margin-top: 24px;
326
- display: none;
 
327
  }
328
 
329
  .dark .error {
330
- background-color: rgba(255, 0, 0, 0.05);
 
 
331
  }
332
 
333
  .error-title {
334
- font-size: 16px;
335
- font-weight: 500;
336
- margin-bottom: 4px;
337
  }
338
 
339
  .error-message {
340
- font-size: 14px;
341
- opacity: 0.8;
342
  }
343
 
 
344
  .footer {
345
- margin-top: 16px;
346
  text-align: center;
347
- font-size: 12px;
348
- opacity: 0.5;
 
349
  }
350
 
 
351
  .confetti {
352
  position: fixed;
353
  width: 10px;
@@ -356,15 +457,28 @@
356
  opacity: 0;
357
  top: 0;
358
  left: 0;
 
 
359
  }
 
 
 
 
 
 
 
 
360
  </style>
361
  </head>
362
  <body>
363
  <div class="container">
364
- <div class="card animate__animated animate__fadeIn">
 
365
  <div class="header">
366
- <h1 class="title animate__animated animate__slideInLeft">LOKI.AI IMAGE PLAYGROUND</h1>
367
- <div class="theme-toggle animate__animated animate__slideInRight">
 
 
368
  <span class="toggle-label">Theme</span>
369
  <div id="theme-toggle" class="toggle-switch">
370
  <div class="toggle-thumb"></div>
@@ -372,7 +486,8 @@
372
  </div>
373
  </div>
374
 
375
- <div class="form-row animate__animated animate__fadeInUp" style="animation-delay: 0.1s;">
 
376
  <div class="form-group">
377
  <label for="model" class="form-label">Select Model</label>
378
  <div class="select-wrapper">
@@ -392,11 +507,11 @@
392
  </div>
393
  <div class="form-group">
394
  <label for="prompt" class="form-label">Enter Prompt</label>
395
- <input type="text" id="prompt" class="form-control" placeholder="Describe what you want to see..." value="sky">
396
  </div>
397
  </div>
398
 
399
- <div class="form-row three-cols animate__animated animate__fadeInUp" style="animation-delay: 0.2s;">
400
  <div class="form-group">
401
  <label for="image-size" class="form-label">Image Size</label>
402
  <div class="select-wrapper">
@@ -419,33 +534,40 @@
419
  </div>
420
  </div>
421
  <div class="form-group">
422
- <label class="form-label">&nbsp;</label>
423
- <button id="generate" class="btn btn-primary btn-full animate__animated animate__pulse animate__infinite">
 
 
424
  Generate Image
425
  </button>
426
  </div>
427
  </div>
428
 
 
429
  <div id="result" class="result">
430
  <div class="text-center animate__animated animate__fadeIn">
431
  <h2 class="result-title">Your Creation</h2>
432
  <p class="result-subtitle">Created with <span id="model-used"></span></p>
433
  </div>
434
- <div id="images-container" class="images-grid"></div>
 
 
435
  </div>
436
 
437
- <div id="loading" class="loading">
 
438
  <div class="loading-spinner"></div>
439
- <p class="loading-text">Creating your masterpiece...</p>
440
  </div>
441
 
442
- <div id="error" class="error">
 
443
  <h3 class="error-title">Oops! Something went wrong.</h3>
444
- <p class="error-message">Please try again or check your connection.</p>
445
  </div>
446
  </div>
447
-
448
- <div class="footer">
449
  © 2025 LOKI.AI IMAGE PLAYGROUND | All rights reserved
450
  </div>
451
  </div>
@@ -464,69 +586,99 @@
464
  const errorDiv = document.getElementById('error');
465
  const imagesContainer = document.getElementById('images-container');
466
  const modelUsed = document.getElementById('model-used');
467
-
468
- // Theme Toggle
 
 
 
 
 
 
 
 
 
 
469
  themeToggle.addEventListener('click', () => {
470
- document.body.classList.toggle('dark');
471
- localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
472
  });
473
-
474
- // Check for saved theme preference
475
- if (localStorage.getItem('theme') === 'dark' ||
476
- (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
477
- document.body.classList.add('dark');
 
 
 
 
478
  }
479
-
480
- // Create confetti
481
  function createConfetti() {
482
- const colors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff'];
483
- const confettiCount = 100;
484
-
485
  for (let i = 0; i < confettiCount; i++) {
486
  const confetti = document.createElement('div');
487
  confetti.className = 'confetti';
488
  confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
489
  confetti.style.left = Math.random() * 100 + 'vw';
490
- confetti.style.opacity = Math.random() + 0.5;
491
- confetti.style.width = Math.random() * 10 + 5 + 'px';
492
- confetti.style.height = Math.random() * 10 + 5 + 'px';
 
493
  document.body.appendChild(confetti);
494
-
 
 
 
495
  const animation = confetti.animate([
496
- { transform: 'translateY(0) rotate(0)', opacity: 1 },
497
- { transform: `translateY(${window.innerHeight}px) rotate(${Math.random() * 360}deg)`, opacity: 0 }
498
  ], {
499
- duration: Math.random() * 2000 + 2000,
500
- easing: 'cubic-bezier(0, 0.55, 0.45, 1)'
501
  });
502
-
503
  animation.onfinish = () => confetti.remove();
504
  }
505
  }
506
-
507
- // Generate image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  generateBtn.addEventListener('click', async () => {
 
 
 
509
  const prompt = promptInput.value.trim();
510
  const model = modelSelect.value;
511
  const size = parseInt(imageSizeSelect.value);
512
  const number = parseInt(imageCountSelect.value);
513
-
514
- if (!prompt) {
515
- promptInput.style.borderColor = 'red';
516
- promptInput.classList.add('animate__animated', 'animate__shakeX');
517
- setTimeout(() => {
518
- promptInput.style.borderColor = '';
519
- promptInput.classList.remove('animate__animated', 'animate__shakeX');
520
- }, 1000);
521
- return;
522
- }
523
-
524
- // Show loading state
525
  resultDiv.style.display = 'none';
526
  errorDiv.style.display = 'none';
 
527
  loadingDiv.style.display = 'flex';
 
 
528
  generateBtn.disabled = true;
529
-
 
530
  try {
531
  const response = await fetch('https://parthsadaria-lokiai.hf.space/images/generations', {
532
  method: 'POST',
@@ -540,83 +692,121 @@
540
  number: number
541
  })
542
  });
543
-
 
544
  if (!response.ok) {
545
- throw new Error('API request failed');
 
 
 
 
 
546
  }
547
-
548
  const data = await response.json();
549
-
550
- // Display result
551
- loadingDiv.style.display = 'none';
552
- resultDiv.style.display = 'block';
 
 
 
 
553
  modelUsed.textContent = model;
554
-
555
  // Clear previous images
556
  imagesContainer.innerHTML = '';
557
-
558
- // Trigger confetti animation
559
- createConfetti();
560
-
561
- // Set grid columns based on number of images
562
- if (number > 1) {
563
- imagesContainer.className = 'images-grid multi-column';
564
- } else {
565
- imagesContainer.className = 'images-grid';
566
- }
567
-
568
- // Add generated images
569
  if (data.data && data.data.length > 0) {
570
  data.data.forEach((item, index) => {
571
  const imgWrapper = document.createElement('div');
572
- imgWrapper.className = 'image-wrapper animate__animated animate__zoomIn';
573
- imgWrapper.style.animationDelay = `${index * 0.2}s`;
574
-
 
575
  const img = document.createElement('img');
576
  img.src = item.url;
577
- img.alt = `Generated image ${index + 1}`;
578
-
 
579
  const downloadBtn = document.createElement('button');
580
  downloadBtn.className = 'btn-download';
 
581
  downloadBtn.innerHTML = `
582
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
583
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
584
  <polyline points="7 10 12 15 17 10"></polyline>
585
  <line x1="12" y1="15" x2="12" y2="3"></line>
586
  </svg>
587
  `;
588
-
589
- downloadBtn.addEventListener('click', () => {
 
590
  const a = document.createElement('a');
591
  a.href = item.url;
592
- a.download = `loki-ai-${model}-${index + 1}.jpg`;
 
 
593
  document.body.appendChild(a);
594
  a.click();
595
  document.body.removeChild(a);
596
  });
597
-
598
  imgWrapper.appendChild(img);
599
  imgWrapper.appendChild(downloadBtn);
600
  imagesContainer.appendChild(imgWrapper);
601
  });
 
 
 
 
 
 
 
 
 
 
602
  }
603
-
604
  } catch (error) {
605
- console.error('Error:', error);
606
- loadingDiv.style.display = 'none';
 
 
 
 
 
607
  errorDiv.style.display = 'block';
608
  errorDiv.classList.add('animate__animated', 'animate__fadeIn');
 
609
  } finally {
 
610
  generateBtn.disabled = false;
 
611
  }
612
  });
613
-
614
- // Enter key to generate
615
  promptInput.addEventListener('keypress', (e) => {
616
- if (e.key === 'Enter') {
 
617
  generateBtn.click();
618
  }
619
  });
 
 
 
 
 
 
 
 
 
 
620
  });
621
  </script>
622
  </body>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>LOKI.AI IMAGE PLAYGROUND</title>
7
+ <!-- Use more weights for DM Sans -->
8
+ <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">
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
10
  <style>
11
  :root {
12
  --primary-bg: #ffffff;
13
+ --secondary-bg: #f7f7f9; /* Slightly different secondary */
14
+ --text-color: #1a1a1a; /* Darker text */
15
+ --text-muted: #666666;
16
  --border-color: #e0e0e0;
17
  --accent-color: #000000;
18
+ --accent-rgb: 0, 0, 0; /* For rgba usage */
19
+ --primary-button-text: #ffffff;
20
+ --card-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); /* Softer, deeper shadow */
21
+ --input-bg: #ffffff;
22
+ --transition-speed: 0.3s;
23
+ --transition-ease: ease-in-out;
24
  }
25
 
26
  .dark {
27
+ --primary-bg: #16161a; /* Slightly richer dark */
28
+ --secondary-bg: #0d0d0f;
29
+ --text-color: #f0f0f0;
30
+ --text-muted: #a0a0a0;
31
+ --border-color: #3a3a40;
32
  --accent-color: #ffffff;
33
+ --accent-rgb: 255, 255, 255;
34
+ --primary-button-text: #000000;
35
+ --card-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
36
+ --input-bg: #242428;
37
  }
38
 
39
  * {
 
46
  body {
47
  background-color: var(--secondary-bg);
48
  color: var(--text-color);
49
+ transition: background-color var(--transition-speed) var(--transition-ease), color var(--transition-speed) var(--transition-ease);
50
+ padding: 40px 20px; /* More vertical padding */
51
+ font-weight: 400; /* Base weight */
52
+ line-height: 1.6;
53
  }
54
 
55
  .container {
56
+ max-width: 1100px; /* Slightly wider */
57
  margin: 0 auto;
58
  }
59
 
60
  .card {
61
  background-color: var(--primary-bg);
62
+ border-radius: 16px; /* Larger radius */
63
+ padding: 32px; /* More padding */
64
  box-shadow: var(--card-shadow);
65
+ transition: all var(--transition-speed) var(--transition-ease);
66
+ border: 1px solid var(--border-color); /* Subtle border */
67
  }
68
 
69
+ .dark .card {
70
+ border: 1px solid transparent; /* Remove border in dark mode if bg is dark enough */
71
+ }
72
+
73
+
74
  .header {
75
  display: flex;
76
  justify-content: space-between;
77
  align-items: center;
78
+ margin-bottom: 32px; /* More space */
79
  flex-wrap: wrap;
80
+ gap: 16px;
81
  }
82
 
83
  .title {
84
+ font-size: 32px; /* Larger title */
85
+ font-weight: 700; /* Bolder */
86
  color: var(--text-color);
87
+ letter-spacing: -0.5px; /* Slightly tighter */
88
  }
89
 
90
+ /* --- Theme Toggle --- */
91
  .theme-toggle {
92
  display: flex;
93
  align-items: center;
94
+ gap: 12px; /* More space */
95
  }
96
 
97
  .toggle-label {
98
  font-size: 14px;
99
+ font-weight: 500;
100
+ color: var(--text-muted);
101
+ transition: color var(--transition-speed) var(--transition-ease);
102
  }
103
 
104
  .toggle-switch {
105
  position: relative;
106
+ width: 52px; /* Slightly larger */
107
+ height: 28px;
108
+ background-color: var(--border-color);
109
+ border-radius: 14px;
110
  cursor: pointer;
111
+ transition: background-color var(--transition-speed) var(--transition-ease);
112
  }
113
+ .toggle-switch:hover {
114
+ background-color: color-mix(in srgb, var(--border-color) 80%, var(--accent-color) 20%);
 
115
  }
116
 
117
+
118
  .toggle-thumb {
119
  position: absolute;
120
+ top: 3px;
121
+ left: 3px;
122
+ width: 22px; /* Larger */
123
+ height: 22px;
124
  border-radius: 50%;
125
  background-color: white;
126
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
127
+ transition: transform var(--transition-speed) var(--transition-ease), background-color var(--transition-speed) var(--transition-ease);
128
  }
129
 
130
+ .dark .toggle-switch {
131
+ background-color: var(--border-color); /* Use border color for consistency */
132
+ }
133
  .dark .toggle-thumb {
134
+ transform: translateX(24px);
135
+ background-color: var(--secondary-bg); /* Match dark secondary bg */
136
  }
137
 
138
+
139
+ /* --- Form Styling --- */
140
  .form-row {
141
  display: grid;
142
  grid-template-columns: 1fr;
143
+ gap: 20px; /* Increased gap */
144
+ margin-bottom: 24px; /* Increased gap */
145
  }
146
 
147
  @media (min-width: 768px) {
 
156
  .form-group {
157
  display: flex;
158
  flex-direction: column;
159
+ gap: 8px; /* More space */
160
  }
161
 
162
  .form-label {
163
  font-size: 14px;
164
+ font-weight: 600; /* Bolder label */
165
  color: var(--text-color);
166
+ opacity: 0.9;
167
  }
168
 
169
  .form-control {
170
+ padding: 12px 16px; /* More padding */
171
+ border-radius: 10px; /* Slightly larger radius */
172
  border: 1px solid var(--border-color);
173
+ background-color: var(--input-bg);
174
  color: var(--text-color);
175
+ font-size: 15px; /* Slightly larger text */
176
+ font-weight: 400;
177
+ transition: all var(--transition-speed) var(--transition-ease);
178
+ appearance: none; /* Remove default styling */
179
+ width: 100%;
180
  }
181
 
182
+ .form-control::placeholder {
183
+ color: var(--text-muted);
184
+ opacity: 0.7;
 
185
  }
186
 
187
+ .form-control:hover {
188
+ border-color: color-mix(in srgb, var(--border-color) 70%, var(--text-color) 30%);
189
+ box-shadow: 0 2px 5px rgba(var(--accent-rgb), 0.05);
190
  }
191
 
192
+ .form-control:focus,
193
+ .form-control:focus-visible { /* Use focus-visible for keyboard nav */
194
+ outline: none;
195
+ border-color: var(--accent-color);
196
+ box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15); /* More prominent focus ring */
197
+ }
198
+
199
+ /* --- Select Wrapper --- */
200
  .select-wrapper {
201
  position: relative;
202
  }
203
 
204
+ .select-wrapper::after {
205
  content: '';
206
  position: absolute;
207
  top: 50%;
208
+ right: 16px; /* Adjusted position */
209
+ width: 8px; /* Larger arrow */
210
+ height: 8px;
211
+ border-style: solid;
212
+ border-width: 0 2px 2px 0;
213
+ border-color: var(--text-muted);
214
+ transform: translateY(-70%) rotate(45deg); /* Centered better */
215
  pointer-events: none;
216
+ transition: border-color var(--transition-speed) var(--transition-ease), transform var(--transition-speed) var(--transition-ease);
217
+ }
218
+
219
+ .select-wrapper:hover::after {
220
+ border-color: var(--text-color);
221
+ }
222
+ .select-wrapper select:focus + ::after { /* Style arrow on focus too */
223
+ border-color: var(--accent-color);
224
  }
225
 
226
+
227
+ /* --- Buttons --- */
228
  .btn {
229
+ padding: 12px 24px; /* More padding */
230
+ border-radius: 10px; /* Match inputs */
231
+ font-weight: 600; /* Bolder text */
232
+ font-size: 15px;
233
  cursor: pointer;
234
+ transition: all var(--transition-speed) var(--transition-ease);
235
  border: none;
236
  text-align: center;
237
+ display: inline-flex; /* Align icon/text if needed */
238
+ align-items: center;
239
+ justify-content: center;
240
+ gap: 8px;
241
  }
242
 
243
  .btn-primary {
244
  background-color: var(--accent-color);
245
+ color: var(--primary-button-text);
246
+ box-shadow: 0 4px 10px rgba(var(--accent-rgb), 0.15);
247
  }
248
 
249
  .btn-primary:hover {
250
+ background-color: color-mix(in srgb, var(--accent-color) 90%, var(--primary-bg) 10%);
251
+ transform: translateY(-3px); /* More lift */
252
+ box-shadow: 0 7px 15px rgba(var(--accent-rgb), 0.25); /* Larger shadow on hover */
253
  }
254
 
255
  .btn-primary:active {
256
+ transform: translateY(-1px);
257
+ box-shadow: 0 3px 8px rgba(var(--accent-rgb), 0.2);
258
+ }
259
+
260
+ .btn-primary:disabled {
261
+ opacity: 0.6;
262
+ cursor: not-allowed;
263
+ transform: translateY(0);
264
+ box-shadow: none;
265
  }
266
 
267
+ /* --- Download Button on Image --- */
268
  .btn-download {
269
+ background-color: rgba(255, 255, 255, 0.3);
270
+ backdrop-filter: blur(8px); /* Stronger blur */
271
+ -webkit-backdrop-filter: blur(8px); /* Safari */
272
+ padding: 10px; /* Slightly larger */
273
  border-radius: 50%;
274
  position: absolute;
275
+ bottom: 12px;
276
+ right: 12px;
277
  display: flex;
278
  align-items: center;
279
  justify-content: center;
280
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
281
+ border: 1px solid rgba(255, 255, 255, 0.2);
282
+ opacity: 0.8;
283
+ transform: scale(0.95);
284
+ transition: all var(--transition-speed) var(--transition-ease);
285
  }
286
 
287
  .dark .btn-download {
288
  background-color: rgba(0, 0, 0, 0.3);
289
+ border: 1px solid rgba(255, 255, 255, 0.1);
290
+ }
291
+
292
+ .image-wrapper:hover .btn-download {
293
+ opacity: 1;
294
+ transform: scale(1);
295
+ }
296
+
297
+ .btn-download:hover {
298
+ background-color: rgba(255, 255, 255, 0.5);
299
+ transform: scale(1.05) !important; /* Override wrapper hover scale */
300
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
301
+ }
302
+ .dark .btn-download:hover {
303
+ background-color: rgba(0, 0, 0, 0.5);
304
  }
305
 
306
  .btn-download svg {
307
+ width: 22px; /* Larger icon */
308
+ height: 22px;
309
+ stroke: var(--accent-color); /* Use accent color for icon */
310
+ stroke-width: 2;
311
  }
312
 
313
  .btn-full {
314
  width: 100%;
315
+ height: 48px; /* Match input height */
316
  }
317
 
318
+ /* --- Result Area --- */
 
 
 
319
  .result {
320
+ margin-top: 40px; /* More space */
321
+ display: none; /* Initially hidden */
322
  }
323
 
324
  .result-title {
325
+ font-size: 22px; /* Larger */
326
+ font-weight: 700; /* Bold */
327
+ margin-bottom: 6px;
328
  }
329
 
330
  .result-subtitle {
331
+ font-size: 15px;
332
+ color: var(--text-muted);
333
+ margin-bottom: 24px; /* More space */
334
+ }
335
+ .result-subtitle span {
336
+ font-weight: 600;
337
+ color: var(--text-color);
338
  }
339
 
340
  .images-grid {
341
  display: grid;
342
  grid-template-columns: 1fr;
343
+ gap: 24px; /* More gap */
344
  }
345
 
346
  .images-grid.multi-column {
347
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* Responsive grid */
348
+ @media (min-width: 768px) {
349
+ grid-template-columns: repeat(2, 1fr); /* Force 2 columns on medium+ */
350
+ }
351
  }
352
 
353
  .image-wrapper {
354
  position: relative;
355
+ border-radius: 12px; /* Consistent radius */
356
  overflow: hidden;
357
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
358
+ transition: transform var(--transition-speed) var(--transition-ease), box-shadow var(--transition-speed) var(--transition-ease);
359
+ background-color: var(--secondary-bg); /* Placeholder bg */
360
  }
361
 
362
  .dark .image-wrapper {
363
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
364
  }
365
 
366
  .image-wrapper img {
367
  width: 100%;
368
  height: auto;
369
  display: block;
370
+ transition: transform var(--transition-speed) var(--transition-ease), filter 0.4s ease; /* Smoother, slightly longer transform */
371
+ }
372
+
373
+ .image-wrapper:hover {
374
+ transform: translateY(-5px); /* Lift effect */
375
+ box-shadow: var(--card-shadow); /* Use main card shadow on hover */
376
  }
377
 
378
  .image-wrapper:hover img {
379
+ transform: scale(1.05); /* Slightly larger scale */
380
+ filter: brightness(1.02); /* Subtle brightness */
381
  }
382
 
383
+ /* --- Loading --- */
384
  .loading {
385
+ display: none; /* Initially hidden */
386
  flex-direction: column;
387
  align-items: center;
388
  justify-content: center;
389
+ padding: 48px 0; /* More padding */
390
  }
391
 
392
  .loading-spinner {
393
+ width: 48px; /* Larger */
394
+ height: 48px;
395
+ border: 5px solid rgba(var(--accent-rgb), 0.1);
396
  border-left-color: var(--accent-color);
397
  border-radius: 50%;
398
+ animation: spinner 0.8s linear infinite; /* Faster spin */
399
+ margin-bottom: 20px; /* More space */
 
 
 
 
 
400
  }
401
 
402
  @keyframes spinner {
 
406
  }
407
 
408
  .loading-text {
409
+ font-size: 15px;
410
+ color: var(--text-muted);
411
+ font-weight: 500;
412
  }
413
 
414
+ /* --- Error --- */
415
  .error {
416
+ background-color: rgba(255, 87, 87, 0.1); /* Standard red, subtle bg */
417
+ color: #d93030; /* Darker red for text */
418
+ padding: 20px 24px; /* More padding */
419
+ border-radius: 12px; /* Consistent radius */
420
+ margin-top: 32px;
421
+ display: none; /* Initially hidden */
422
+ border: 1px solid rgba(255, 87, 87, 0.3);
423
  }
424
 
425
  .dark .error {
426
+ background-color: rgba(255, 87, 87, 0.1);
427
+ color: #ff8a8a; /* Lighter red in dark mode */
428
+ border-color: rgba(255, 87, 87, 0.2);
429
  }
430
 
431
  .error-title {
432
+ font-size: 17px; /* Slightly larger */
433
+ font-weight: 700; /* Bold */
434
+ margin-bottom: 8px;
435
  }
436
 
437
  .error-message {
438
+ font-size: 15px;
439
+ opacity: 0.9;
440
  }
441
 
442
+ /* --- Footer --- */
443
  .footer {
444
+ margin-top: 40px; /* More space */
445
  text-align: center;
446
+ font-size: 13px; /* Slightly larger */
447
+ color: var(--text-muted);
448
+ transition: color var(--transition-speed) var(--transition-ease);
449
  }
450
 
451
+ /* --- Confetti (keep as is) --- */
452
  .confetti {
453
  position: fixed;
454
  width: 10px;
 
457
  opacity: 0;
458
  top: 0;
459
  left: 0;
460
+ pointer-events: none; /* Prevent interaction */
461
+ z-index: 9999; /* Ensure it's on top */
462
  }
463
+
464
+ /* --- Utility --- */
465
+ .text-center {
466
+ text-align: center;
467
+ }
468
+
469
+ /* --- Animation Delays (keep using inline styles for flexibility) --- */
470
+
471
  </style>
472
  </head>
473
  <body>
474
  <div class="container">
475
+ <!-- Card gets initial fade in -->
476
+ <div class="card animate__animated animate__fadeIn animate__delay-0.2s">
477
  <div class="header">
478
+ <!-- Title slides in -->
479
+ <h1 class="title animate__animated animate__fadeInLeft animate__delay-0.3s">LOKI.AI IMAGE PLAYGROUND</h1>
480
+ <!-- Theme toggle fades in -->
481
+ <div class="theme-toggle animate__animated animate__fadeInRight animate__delay-0.4s">
482
  <span class="toggle-label">Theme</span>
483
  <div id="theme-toggle" class="toggle-switch">
484
  <div class="toggle-thumb"></div>
 
486
  </div>
487
  </div>
488
 
489
+ <!-- Form rows fade up with delays -->
490
+ <div class="form-row animate__animated animate__fadeInUp animate__delay-0.5s">
491
  <div class="form-group">
492
  <label for="model" class="form-label">Select Model</label>
493
  <div class="select-wrapper">
 
507
  </div>
508
  <div class="form-group">
509
  <label for="prompt" class="form-label">Enter Prompt</label>
510
+ <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">
511
  </div>
512
  </div>
513
 
514
+ <div class="form-row three-cols animate__animated animate__fadeInUp animate__delay-0.6s">
515
  <div class="form-group">
516
  <label for="image-size" class="form-label">Image Size</label>
517
  <div class="select-wrapper">
 
534
  </div>
535
  </div>
536
  <div class="form-group">
537
+ <!-- Label for alignment, content via button -->
538
+ <label class="form-label"> </label>
539
+ <!-- Removed infinite pulse, rely on hover/active states -->
540
+ <button id="generate" class="btn btn-primary btn-full">
541
  Generate Image
542
  </button>
543
  </div>
544
  </div>
545
 
546
+ <!-- Result area will fade in when populated -->
547
  <div id="result" class="result">
548
  <div class="text-center animate__animated animate__fadeIn">
549
  <h2 class="result-title">Your Creation</h2>
550
  <p class="result-subtitle">Created with <span id="model-used"></span></p>
551
  </div>
552
+ <div id="images-container" class="images-grid">
553
+ <!-- Images will be added here dynamically -->
554
+ </div>
555
  </div>
556
 
557
+ <!-- Loading indicator will fade in/out -->
558
+ <div id="loading" class="loading animate__animated">
559
  <div class="loading-spinner"></div>
560
+ <p class="loading-text">Conjuring pixels... Please wait.</p>
561
  </div>
562
 
563
+ <!-- Error message will fade in -->
564
+ <div id="error" class="error animate__animated">
565
  <h3 class="error-title">Oops! Something went wrong.</h3>
566
+ <p class="error-message">Please try adjusting your prompt or try again later.</p>
567
  </div>
568
  </div>
569
+
570
+ <div class="footer animate__animated animate__fadeInUp animate__delay-0.8s">
571
  © 2025 LOKI.AI IMAGE PLAYGROUND | All rights reserved
572
  </div>
573
  </div>
 
586
  const errorDiv = document.getElementById('error');
587
  const imagesContainer = document.getElementById('images-container');
588
  const modelUsed = document.getElementById('model-used');
589
+ const body = document.body; // Reference body for class toggling
590
+
591
+ // --- Theme Logic ---
592
+ const applyTheme = (theme) => {
593
+ if (theme === 'dark') {
594
+ body.classList.add('dark');
595
+ } else {
596
+ body.classList.remove('dark');
597
+ }
598
+ localStorage.setItem('theme', theme);
599
+ };
600
+
601
  themeToggle.addEventListener('click', () => {
602
+ const currentTheme = body.classList.contains('dark') ? 'light' : 'dark';
603
+ applyTheme(currentTheme);
604
  });
605
+
606
+ // Check system preference and saved theme
607
+ const savedTheme = localStorage.getItem('theme');
608
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
609
+
610
+ if (savedTheme) {
611
+ applyTheme(savedTheme);
612
+ } else {
613
+ applyTheme(prefersDark ? 'dark' : 'light');
614
  }
615
+
616
+ // --- Confetti ---
617
  function createConfetti() {
618
+ const colors = ['#ff5757', '#57ff87', '#5787ff', '#f0ff57', '#ff57f0', '#57f0ff']; // Adjusted colors
619
+ const confettiCount = 100; // Keep it reasonable
620
+
621
  for (let i = 0; i < confettiCount; i++) {
622
  const confetti = document.createElement('div');
623
  confetti.className = 'confetti';
624
  confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
625
  confetti.style.left = Math.random() * 100 + 'vw';
626
+ confetti.style.opacity = Math.random() * 0.5 + 0.5; // 0.5 to 1.0
627
+ confetti.style.width = Math.random() * 8 + 6 + 'px'; // 6px to 14px
628
+ confetti.style.height = confetti.style.width; // Keep square-ish
629
+ confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0'; // Mix circles and squares
630
  document.body.appendChild(confetti);
631
+
632
+ const fallDuration = Math.random() * 3000 + 3000; // 3-6 seconds
633
+ const rotation = Math.random() * 720 - 360; // Random rotation
634
+
635
  const animation = confetti.animate([
636
+ { transform: `translateY(-20px) rotate(0deg)`, opacity: 1 }, // Start slightly above
637
+ { transform: `translateY(${window.innerHeight + 20}px) rotate(${rotation}deg)`, opacity: 0 }
638
  ], {
639
+ duration: fallDuration,
640
+ easing: 'ease-out' // More natural fall
641
  });
642
+
643
  animation.onfinish = () => confetti.remove();
644
  }
645
  }
646
+
647
+ // --- Form Validation ---
648
+ const validateInput = (inputElement) => {
649
+ if (!inputElement.value.trim()) {
650
+ inputElement.style.borderColor = '#d93030'; // Use error color
651
+ inputElement.classList.add('animate__animated', 'animate__headShake'); // More subtle shake
652
+ setTimeout(() => {
653
+ inputElement.style.borderColor = ''; // Reset border color
654
+ inputElement.classList.remove('animate__animated', 'animate__headShake');
655
+ }, 1000);
656
+ return false;
657
+ }
658
+ inputElement.style.borderColor = ''; // Ensure reset if valid
659
+ return true;
660
+ }
661
+
662
+ // --- Image Generation ---
663
  generateBtn.addEventListener('click', async () => {
664
+ // Basic validation
665
+ if (!validateInput(promptInput)) return;
666
+
667
  const prompt = promptInput.value.trim();
668
  const model = modelSelect.value;
669
  const size = parseInt(imageSizeSelect.value);
670
  const number = parseInt(imageCountSelect.value);
671
+
672
+ // UI updates for loading state
 
 
 
 
 
 
 
 
 
 
673
  resultDiv.style.display = 'none';
674
  errorDiv.style.display = 'none';
675
+ errorDiv.classList.remove('animate__fadeIn'); // Reset animation class
676
  loadingDiv.style.display = 'flex';
677
+ loadingDiv.classList.remove('animate__fadeOut');
678
+ loadingDiv.classList.add('animate__fadeIn');
679
  generateBtn.disabled = true;
680
+ generateBtn.textContent = 'Generating...'; // Change button text
681
+
682
  try {
683
  const response = await fetch('https://parthsadaria-lokiai.hf.space/images/generations', {
684
  method: 'POST',
 
692
  number: number
693
  })
694
  });
695
+
696
+ // Handle API errors more gracefully
697
  if (!response.ok) {
698
+ let errorMsg = `API request failed with status ${response.status}.`;
699
+ try {
700
+ const errorData = await response.json();
701
+ errorMsg += ` ${errorData.detail || ''}`;
702
+ } catch (e) { /* Ignore if response body is not JSON */ }
703
+ throw new Error(errorMsg);
704
  }
705
+
706
  const data = await response.json();
707
+
708
+ // --- Display Results ---
709
+ loadingDiv.classList.replace('animate__fadeIn', 'animate__fadeOut');
710
+ loadingDiv.addEventListener('animationend', () => {
711
+ loadingDiv.style.display = 'none';
712
+ }, { once: true }); // Ensure runs only once
713
+
714
+
715
  modelUsed.textContent = model;
716
+
717
  // Clear previous images
718
  imagesContainer.innerHTML = '';
719
+
720
+ // Set grid columns (using auto-fit now, but keep class for potential overrides)
721
+ imagesContainer.className = number > 1 ? 'images-grid multi-column' : 'images-grid';
722
+
723
+ // Add new images with animation
 
 
 
 
 
 
 
724
  if (data.data && data.data.length > 0) {
725
  data.data.forEach((item, index) => {
726
  const imgWrapper = document.createElement('div');
727
+ // Use fadeInUp for a nicer entrance
728
+ imgWrapper.className = 'image-wrapper animate__animated animate__fadeInUp';
729
+ imgWrapper.style.animationDelay = `${index * 0.15}s`; // Slightly faster stagger
730
+
731
  const img = document.createElement('img');
732
  img.src = item.url;
733
+ img.alt = `Generated image ${index + 1} for prompt: ${prompt}`;
734
+ img.loading = 'lazy'; // Improve performance for many images
735
+
736
  const downloadBtn = document.createElement('button');
737
  downloadBtn.className = 'btn-download';
738
+ downloadBtn.setAttribute('aria-label', 'Download image'); // Accessibility
739
  downloadBtn.innerHTML = `
740
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
741
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
742
  <polyline points="7 10 12 15 17 10"></polyline>
743
  <line x1="12" y1="15" x2="12" y2="3"></line>
744
  </svg>
745
  `;
746
+
747
+ downloadBtn.addEventListener('click', (e) => {
748
+ e.stopPropagation(); // Prevent triggering wrapper hover effects if any
749
  const a = document.createElement('a');
750
  a.href = item.url;
751
+ // Create a safer filename
752
+ const safePrompt = prompt.substring(0, 20).replace(/[^a-z0-9]/gi, '_').toLowerCase();
753
+ a.download = `loki-ai-${model}-${safePrompt}-${index + 1}.jpg`;
754
  document.body.appendChild(a);
755
  a.click();
756
  document.body.removeChild(a);
757
  });
758
+
759
  imgWrapper.appendChild(img);
760
  imgWrapper.appendChild(downloadBtn);
761
  imagesContainer.appendChild(imgWrapper);
762
  });
763
+
764
+ // Show result section after images are ready to be animated
765
+ resultDiv.style.display = 'block';
766
+ resultDiv.classList.add('animate__animated', 'animate__fadeIn');
767
+
768
+ // Trigger confetti only on success
769
+ createConfetti();
770
+
771
+ } else {
772
+ throw new Error("Received empty data from API."); // Handle cases with OK status but no images
773
  }
774
+
775
  } catch (error) {
776
+ console.error('Error generating image:', error);
777
+ loadingDiv.style.display = 'none'; // Ensure loading is hidden on error
778
+ loadingDiv.classList.remove('animate__fadeIn', 'animate__fadeOut');
779
+
780
+ // Display error message
781
+ const errorMessageElement = errorDiv.querySelector('.error-message');
782
+ errorMessageElement.textContent = error.message || 'An unknown error occurred. Please check the console or try again.';
783
  errorDiv.style.display = 'block';
784
  errorDiv.classList.add('animate__animated', 'animate__fadeIn');
785
+
786
  } finally {
787
+ // Reset button state regardless of success/error
788
  generateBtn.disabled = false;
789
+ generateBtn.textContent = 'Generate Image';
790
  }
791
  });
792
+
793
+ // --- Enter Key Submission ---
794
  promptInput.addEventListener('keypress', (e) => {
795
+ if (e.key === 'Enter' && !generateBtn.disabled) { // Prevent multiple submits
796
+ e.preventDefault(); // Prevent potential form submission
797
  generateBtn.click();
798
  }
799
  });
800
+
801
+ // --- Initial Animation Trigger ---
802
+ // Small delay to ensure CSS is loaded before animations start
803
+ setTimeout(() => {
804
+ document.querySelectorAll('.animate__animated').forEach(el => {
805
+ // This is mostly handled by animate.css, but ensures visibility if needed
806
+ // You might not strictly need this if using animate.css correctly
807
+ });
808
+ }, 100);
809
+
810
  });
811
  </script>
812
  </body>