triflix commited on
Commit
6fdab27
·
verified ·
1 Parent(s): c4f3cf3

Create templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +353 -0
templates/index.html ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Image Generator</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ @keyframes pulse {
10
+ 0%, 100% { opacity: 1; }
11
+ 50% { opacity: 0.5; }
12
+ }
13
+ .animate-pulse {
14
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
15
+ }
16
+ .image-preview {
17
+ transition: all 0.3s ease;
18
+ }
19
+ </style>
20
+ </head>
21
+ <body class="bg-gray-100 min-h-screen">
22
+ <div class="container mx-auto px-4 py-8">
23
+ <header class="mb-8 text-center">
24
+ <h1 class="text-3xl font-bold text-indigo-700">AI Image Generator</h1>
25
+ <p class="text-gray-600 mt-2">Generate stunning images with AI</p>
26
+ </header>
27
+
28
+ <div class="grid md:grid-cols-2 gap-8">
29
+ <!-- Form Section -->
30
+ <div class="bg-white rounded-lg shadow-md p-6">
31
+ <form id="imageForm" class="space-y-6">
32
+ <div>
33
+ <label for="prompt" class="block text-sm font-medium text-gray-700 mb-1">Image Prompt</label>
34
+ <textarea id="prompt" name="prompt" rows="4" required
35
+ class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border"></textarea>
36
+ </div>
37
+
38
+ <div>
39
+ <label for="negative_prompt" class="block text-sm font-medium text-gray-700 mb-1">Negative Prompt (Optional)</label>
40
+ <textarea id="negative_prompt" name="negative_prompt" rows="2"
41
+ class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border"></textarea>
42
+ </div>
43
+
44
+ <div class="grid grid-cols-2 gap-4">
45
+ <div>
46
+ <label for="resolution" class="block text-sm font-medium text-gray-700 mb-1">Resolution</label>
47
+ <select id="resolution" class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border">
48
+ <option value="1024,1024">Square (1024×1024)</option>
49
+ <option value="1024,768">Landscape (1024×768)</option>
50
+ <option value="768,1024">Portrait (768×1024)</option>
51
+ <option value="1152,896">Widescreen (1152×896)</option>
52
+ <option value="896,1152">Tall (896×1152)</option>
53
+ <option value="1216,832">Cinematic (1216×832)</option>
54
+ <option value="custom">Custom...</option>
55
+ </select>
56
+ </div>
57
+ <div id="customResolution" class="hidden grid grid-cols-2 gap-2">
58
+ <div>
59
+ <label for="width" class="block text-sm font-medium text-gray-700 mb-1">Width</label>
60
+ <input type="number" id="width" name="width" value="1024" min="256" max="2048" step="64"
61
+ class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border">
62
+ </div>
63
+ <div>
64
+ <label for="height" class="block text-sm font-medium text-gray-700 mb-1">Height</label>
65
+ <input type="number" id="height" name="height" value="1024" min="256" max="2048" step="64"
66
+ class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border">
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="grid grid-cols-2 gap-4">
72
+ <div>
73
+ <label for="steps" class="block text-sm font-medium text-gray-700 mb-1">Steps</label>
74
+ <input type="number" id="steps" name="steps" value="4" min="1" max="50"
75
+ class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border">
76
+ <p class="text-xs text-gray-500 mt-1">Higher values give more detail but take longer</p>
77
+ </div>
78
+ <div>
79
+ <label for="guidance_scale" class="block text-sm font-medium text-gray-700 mb-1">Guidance Scale</label>
80
+ <input type="number" id="guidance_scale" name="guidance_scale" value="60" min="0" max="100"
81
+ class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border">
82
+ <p class="text-xs text-gray-500 mt-1">How closely to follow your prompt</p>
83
+ </div>
84
+ </div>
85
+
86
+ <div>
87
+ <label class="inline-flex items-center">
88
+ <input type="checkbox" id="use_random_seed" name="use_random_seed" checked
89
+ class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
90
+ <span class="ml-2 text-sm text-gray-700">Use random seed</span>
91
+ </label>
92
+ </div>
93
+
94
+ <div id="seedContainer" class="hidden">
95
+ <label for="seed" class="block text-sm font-medium text-gray-700 mb-1">Seed</label>
96
+ <input type="number" id="seed" name="seed" value="0" min="0" max="2147483647"
97
+ class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border">
98
+ </div>
99
+
100
+ <div class="flex space-x-4">
101
+ <button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 flex-grow">
102
+ Generate Image
103
+ </button>
104
+ <button type="button" id="clearCacheBtn" class="bg-gray-500 text-white px-4 py-2 rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
105
+ Clear Cache
106
+ </button>
107
+ </div>
108
+ </form>
109
+ </div>
110
+
111
+ <!-- Output Section -->
112
+ <div class="bg-white rounded-lg shadow-md p-6 flex flex-col">
113
+ <h2 class="text-xl font-semibold mb-4">Generated Image</h2>
114
+
115
+ <!-- Loading State -->
116
+ <div id="loadingContainer" class="hidden flex-grow flex flex-col items-center justify-center space-y-4">
117
+ <div class="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
118
+ <p class="text-gray-700">Generating your image...</p>
119
+ <div id="progressContainer" class="hidden w-full">
120
+ <div class="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
121
+ <div id="progressBar" class="h-full bg-indigo-500 rounded-full transition-all duration-300" style="width: 0%"></div>
122
+ </div>
123
+ <p id="progressText" class="text-xs text-gray-500 mt-1 text-center">Starting generation...</p>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- Preview Container -->
128
+ <div id="previewContainer" class="flex-grow flex flex-col items-center justify-center">
129
+ <img id="noImagePlaceholder" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 24 24' fill='none' stroke='%23d1d5db' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'%3E%3C/circle%3E%3Cpolyline points='21 15 16 10 5 21'%3E%3C/polyline%3E%3C/svg%3E"
130
+ class="w-24 h-24 opacity-30" alt="No image generated">
131
+ <p class="text-gray-500 mt-4">No image generated yet</p>
132
+ </div>
133
+
134
+ <!-- Result Container -->
135
+ <div id="resultContainer" class="hidden flex-grow flex flex-col">
136
+ <div class="flex-grow flex items-center justify-center mb-4">
137
+ <img id="resultImage" src="" alt="Generated image" class="max-w-full max-h-[500px] rounded-lg shadow">
138
+ </div>
139
+ <div class="text-sm text-gray-700">
140
+ <div class="mb-2">
141
+ <span class="font-medium">Image URL:</span>
142
+ <a id="imageUrl" href="#" target="_blank" class="text-indigo-600 hover:underline break-all"></a>
143
+ </div>
144
+ <button id="downloadBtn" class="mt-2 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 w-full">
145
+ Download Image
146
+ </button>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+
153
+ <script>
154
+ document.addEventListener('DOMContentLoaded', () => {
155
+ const form = document.getElementById('imageForm');
156
+ const loadingContainer = document.getElementById('loadingContainer');
157
+ const previewContainer = document.getElementById('previewContainer');
158
+ const resultContainer = document.getElementById('resultContainer');
159
+ const progressContainer = document.getElementById('progressContainer');
160
+ const progressBar = document.getElementById('progressBar');
161
+ const progressText = document.getElementById('progressText');
162
+ const resultImage = document.getElementById('resultImage');
163
+ const imageUrl = document.getElementById('imageUrl');
164
+ const downloadBtn = document.getElementById('downloadBtn');
165
+ const clearCacheBtn = document.getElementById('clearCacheBtn');
166
+ const resolutionSelect = document.getElementById('resolution');
167
+ const customResolution = document.getElementById('customResolution');
168
+ const widthInput = document.getElementById('width');
169
+ const heightInput = document.getElementById('height');
170
+ const useRandomSeedCheckbox = document.getElementById('use_random_seed');
171
+ const seedContainer = document.getElementById('seedContainer');
172
+
173
+ // Handle resolution selection
174
+ resolutionSelect.addEventListener('change', (e) => {
175
+ if (e.target.value === 'custom') {
176
+ customResolution.classList.remove('hidden');
177
+ } else {
178
+ customResolution.classList.add('hidden');
179
+ const [width, height] = e.target.value.split(',');
180
+ widthInput.value = width;
181
+ heightInput.value = height;
182
+ }
183
+ });
184
+
185
+ // Handle seed checkbox
186
+ useRandomSeedCheckbox.addEventListener('change', (e) => {
187
+ if (e.target.checked) {
188
+ seedContainer.classList.add('hidden');
189
+ } else {
190
+ seedContainer.classList.remove('hidden');
191
+ }
192
+ });
193
+
194
+ // Clear cache button
195
+ clearCacheBtn.addEventListener('click', async () => {
196
+ try {
197
+ clearCacheBtn.disabled = true;
198
+ clearCacheBtn.textContent = 'Clearing...';
199
+
200
+ const response = await fetch('/clear-cache', {
201
+ method: 'POST'
202
+ });
203
+
204
+ const data = await response.json();
205
+
206
+ if (response.ok) {
207
+ alert('Cache cleared successfully!');
208
+ } else {
209
+ alert(`Error: ${data.error}`);
210
+ }
211
+ } catch (error) {
212
+ alert(`Error: ${error.message}`);
213
+ } finally {
214
+ clearCacheBtn.disabled = false;
215
+ clearCacheBtn.textContent = 'Clear Cache';
216
+ }
217
+ });
218
+
219
+ // Form submission
220
+ form.addEventListener('submit', async (e) => {
221
+ e.preventDefault();
222
+
223
+ // Show loading UI
224
+ previewContainer.classList.add('hidden');
225
+ resultContainer.classList.add('hidden');
226
+ loadingContainer.classList.remove('hidden');
227
+ progressContainer.classList.remove('hidden');
228
+
229
+ // Reset progress
230
+ progressBar.style.width = '0%';
231
+ progressText.textContent = 'Starting generation...';
232
+
233
+ try {
234
+ // Get form data
235
+ const formData = new FormData(form);
236
+
237
+ // Add width and height from the resolution dropdown
238
+ formData.set('width', widthInput.value);
239
+ formData.set('height', heightInput.value);
240
+
241
+ // Handle seed based on checkbox
242
+ formData.set('use_random_seed', useRandomSeedCheckbox.checked);
243
+
244
+ // Disable form elements during generation
245
+ Array.from(form.elements).forEach(element => {
246
+ element.disabled = true;
247
+ });
248
+
249
+ // Send the initial request
250
+ const response = await fetch('/generate', {
251
+ method: 'POST',
252
+ body: formData
253
+ });
254
+
255
+ if (!response.ok) {
256
+ const error = await response.json();
257
+ throw new Error(error.error || 'Failed to start image generation');
258
+ }
259
+
260
+ const data = await response.json();
261
+ const eventId = data.event_id;
262
+
263
+ // Poll for updates
264
+ await pollForResults(eventId);
265
+
266
+ } catch (error) {
267
+ alert(`Error: ${error.message}`);
268
+ loadingContainer.classList.add('hidden');
269
+ previewContainer.classList.remove('hidden');
270
+ } finally {
271
+ // Re-enable form elements
272
+ Array.from(form.elements).forEach(element => {
273
+ element.disabled = false;
274
+ });
275
+ }
276
+ });
277
+
278
+ async function pollForResults(eventId) {
279
+ let progress = 0;
280
+ let pollInterval;
281
+ let finalImageUrl = null;
282
+
283
+ const poll = async () => {
284
+ try {
285
+ const response = await fetch(`/poll/${eventId}`);
286
+
287
+ if (!response.ok) {
288
+ clearInterval(pollInterval);
289
+ throw new Error('Failed to poll for updates');
290
+ }
291
+
292
+ const data = await response.json();
293
+
294
+ // Update progress
295
+ if (data.status === 'generating') {
296
+ progress += 2;
297
+ progress = Math.min(progress, 96); // Cap at 96% until complete
298
+ progressBar.style.width = `${progress}%`;
299
+ progressText.textContent = 'Generating image...';
300
+
301
+ // Show latest progress image if available
302
+ if (data.image_urls && data.image_urls.length > 0) {
303
+ loadingContainer.classList.add('hidden');
304
+ resultContainer.classList.remove('hidden');
305
+ resultImage.src = data.image_urls[data.image_urls.length - 1];
306
+ resultImage.classList.add('animate-pulse');
307
+ }
308
+ } else if (data.status === 'complete') {
309
+ clearInterval(pollInterval);
310
+ progressBar.style.width = '100%';
311
+ progressText.textContent = 'Complete!';
312
+
313
+ // Show the final image
314
+ finalImageUrl = data.final_image;
315
+ loadingContainer.classList.add('hidden');
316
+ resultContainer.classList.remove('hidden');
317
+
318
+ if (finalImageUrl) {
319
+ resultImage.src = finalImageUrl;
320
+ resultImage.classList.remove('animate-pulse');
321
+ imageUrl.href = finalImageUrl;
322
+ imageUrl.textContent = finalImageUrl;
323
+
324
+ // Setup download button
325
+ downloadBtn.onclick = () => {
326
+ const link = document.createElement('a');
327
+ link.href = finalImageUrl;
328
+ link.download = 'generated-image.webp';
329
+ document.body.appendChild(link);
330
+ link.click();
331
+ document.body.removeChild(link);
332
+ };
333
+ } else {
334
+ throw new Error('No final image URL received');
335
+ }
336
+ }
337
+
338
+ } catch (error) {
339
+ clearInterval(pollInterval);
340
+ alert(`Error polling for results: ${error.message}`);
341
+ loadingContainer.classList.add('hidden');
342
+ previewContainer.classList.remove('hidden');
343
+ }
344
+ };
345
+
346
+ // Start polling every 1s
347
+ await poll(); // Initial poll
348
+ pollInterval = setInterval(poll, 1000);
349
+ }
350
+ });
351
+ </script>
352
+ </body>
353
+ </html>