bibibi12345 commited on
Commit
fb44180
·
1 Parent(s): 771a66e
Files changed (3) hide show
  1. static/script.js +197 -18
  2. static/style.css +115 -0
  3. templates/index.html +6 -1
static/script.js CHANGED
@@ -76,33 +76,111 @@ window.addEventListener('DOMContentLoaded', () => {
76
  async function handleFileUpload(event) {
77
  const files = Array.from(event.target.files);
78
 
 
 
 
 
 
 
 
 
 
 
79
  for (const file of files) {
80
  if (uploadedImages.length >= 10) {
81
- showStatus('Maximum 10 images allowed', 'error');
82
  break;
83
  }
84
 
85
  if (file.type.startsWith('image/')) {
86
- const reader = new FileReader();
87
- reader.onload = (e) => {
88
- const dataUrl = e.target.result;
89
- uploadedImages.push(dataUrl);
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- // Get image dimensions
92
- const img = new Image();
93
- img.onload = function() {
94
- imageDimensions.push({
95
- width: this.width,
96
- height: this.height
97
- });
98
- addImagePreview(dataUrl, uploadedImages.length - 1);
99
- updateCustomSizeFromLastImage();
100
  };
101
- img.src = dataUrl;
102
- };
103
- reader.readAsDataURL(file);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  }
105
  }
 
 
 
106
  }
107
 
108
  // Add image preview
@@ -453,8 +531,15 @@ function displayResults(response) {
453
  if (!imgSrc) {
454
  console.error(`[DEBUG Frontend] No source for image ${index + 1}`);
455
  }
 
 
 
 
456
  imageItem.innerHTML = `
457
- <img src="${imgSrc}" alt="Result ${index + 1}" onerror="console.error('Failed to load image:', this.src)">
 
 
 
458
  `;
459
  resultImages.appendChild(imageItem);
460
  });
@@ -471,3 +556,97 @@ function displayResults(response) {
471
  addLog(`Seed used: ${response.seed}`);
472
  }
473
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  async function handleFileUpload(event) {
77
  const files = Array.from(event.target.files);
78
 
79
+ if (files.length === 0) {
80
+ return;
81
+ }
82
+
83
+ // Show immediate feedback
84
+ showStatus(`Processing ${files.length} image(s)...`, 'info');
85
+
86
+ let processedCount = 0;
87
+ let errorCount = 0;
88
+
89
  for (const file of files) {
90
  if (uploadedImages.length >= 10) {
91
+ showStatus('Maximum 10 images allowed. Some images were not added.', 'error');
92
  break;
93
  }
94
 
95
  if (file.type.startsWith('image/')) {
96
+ try {
97
+ // Create a temporary loading placeholder
98
+ const tempIndex = uploadedImages.length;
99
+ const loadingId = `loading-${Date.now()}-${tempIndex}`;
100
+
101
+ // Add loading placeholder immediately
102
+ const loadingPreview = document.createElement('div');
103
+ loadingPreview.className = 'image-preview-item loading-preview';
104
+ loadingPreview.id = loadingId;
105
+ loadingPreview.innerHTML = `
106
+ <div class="loading-placeholder">
107
+ <div class="spinner"></div>
108
+ <p>${file.name}</p>
109
+ </div>
110
+ `;
111
+ imagePreview.appendChild(loadingPreview);
112
 
113
+ const reader = new FileReader();
114
+
115
+ reader.onerror = (error) => {
116
+ console.error('Error reading file:', file.name, error);
117
+ errorCount++;
118
+ // Remove loading placeholder
119
+ const placeholder = document.getElementById(loadingId);
120
+ if (placeholder) placeholder.remove();
121
+ showStatus(`Failed to read file: ${file.name}`, 'error');
122
  };
123
+
124
+ reader.onload = (e) => {
125
+ const dataUrl = e.target.result;
126
+
127
+ // Remove loading placeholder
128
+ const placeholder = document.getElementById(loadingId);
129
+ if (placeholder) placeholder.remove();
130
+
131
+ // Add to uploaded images
132
+ uploadedImages.push(dataUrl);
133
+ processedCount++;
134
+
135
+ // Get image dimensions
136
+ const img = new Image();
137
+ img.onload = function() {
138
+ imageDimensions.push({
139
+ width: this.width,
140
+ height: this.height
141
+ });
142
+ addImagePreview(dataUrl, uploadedImages.length - 1);
143
+ updateCustomSizeFromLastImage();
144
+
145
+ // Show success message when all files are processed
146
+ if (processedCount + errorCount === files.length) {
147
+ if (errorCount === 0) {
148
+ showStatus(`Successfully added ${processedCount} image(s) (${uploadedImages.length}/10 slots used)`, 'success');
149
+ } else {
150
+ showStatus(`Added ${processedCount} image(s), ${errorCount} failed (${uploadedImages.length}/10 slots used)`, 'warning');
151
+ }
152
+ }
153
+ };
154
+
155
+ img.onerror = () => {
156
+ console.error('Error loading image dimensions for:', file.name);
157
+ // Still add the image even if we can't get dimensions
158
+ imageDimensions.push({
159
+ width: 1280,
160
+ height: 1280
161
+ });
162
+ addImagePreview(dataUrl, uploadedImages.length - 1);
163
+ updateCustomSizeFromLastImage();
164
+ };
165
+
166
+ img.src = dataUrl;
167
+ };
168
+
169
+ reader.readAsDataURL(file);
170
+
171
+ } catch (error) {
172
+ console.error('Error processing file:', file.name, error);
173
+ errorCount++;
174
+ showStatus(`Error processing ${file.name}`, 'error');
175
+ }
176
+ } else {
177
+ errorCount++;
178
+ showStatus(`${file.name} is not an image file`, 'error');
179
  }
180
  }
181
+
182
+ // Clear the file input so the same files can be selected again if needed
183
+ event.target.value = '';
184
  }
185
 
186
  // Add image preview
 
531
  if (!imgSrc) {
532
  console.error(`[DEBUG Frontend] No source for image ${index + 1}`);
533
  }
534
+
535
+ // Create unique ID for this result image
536
+ const imageId = `result-img-${Date.now()}-${index}`;
537
+
538
  imageItem.innerHTML = `
539
+ <img id="${imageId}" src="${imgSrc}" alt="Result ${index + 1}" onerror="console.error('Failed to load image:', this.src)">
540
+ <button class="use-as-input-btn" onclick="useResultAsInput('${imageId}', '${imgSrc}')" title="Use this image as input for next generation">
541
+ ↻ Use as Input
542
+ </button>
543
  `;
544
  resultImages.appendChild(imageItem);
545
  });
 
556
  addLog(`Seed used: ${response.seed}`);
557
  }
558
  }
559
+
560
+ // Use generated result as input for next generation
561
+ async function useResultAsInput(imageId, imageSrc) {
562
+ try {
563
+ console.log('[DEBUG Frontend] Using result as input:', imageSrc);
564
+
565
+ // Check if we're in text-to-image mode and switch to edit mode
566
+ const currentModel = modelSelect.value;
567
+ if (currentModel === 'fal-ai/bytedance/seedream/v4/text-to-image') {
568
+ // Switch to edit mode
569
+ modelSelect.value = 'fal-ai/bytedance/seedream/v4/edit';
570
+ handleModelChange();
571
+ showStatus('Switched to Image Edit mode to use generated image as input', 'info');
572
+ }
573
+
574
+ // Check if we've reached the maximum number of images
575
+ if (uploadedImages.length >= 10) {
576
+ showStatus('Maximum 10 images allowed. Please remove some images first.', 'error');
577
+ return;
578
+ }
579
+
580
+ // Add the image URL to uploaded images
581
+ uploadedImages.push(imageSrc);
582
+
583
+ // Get dimensions of the result image
584
+ const imgElement = document.getElementById(imageId);
585
+ if (imgElement) {
586
+ // Wait for image to load if needed
587
+ if (!imgElement.complete) {
588
+ await new Promise((resolve) => {
589
+ imgElement.onload = resolve;
590
+ imgElement.onerror = resolve;
591
+ });
592
+ }
593
+
594
+ // Store dimensions
595
+ imageDimensions.push({
596
+ width: imgElement.naturalWidth || imgElement.width,
597
+ height: imgElement.naturalHeight || imgElement.height
598
+ });
599
+
600
+ // Update custom size based on this image
601
+ updateCustomSizeFromLastImage();
602
+ } else {
603
+ // Fallback: try to get dimensions from a new Image object
604
+ const img = new Image();
605
+ img.onload = function() {
606
+ imageDimensions.push({
607
+ width: this.width,
608
+ height: this.height
609
+ });
610
+ updateCustomSizeFromLastImage();
611
+ };
612
+ img.onerror = function() {
613
+ // If we can't get dimensions, add default ones
614
+ imageDimensions.push({
615
+ width: 1280,
616
+ height: 1280
617
+ });
618
+ updateCustomSizeFromLastImage();
619
+ };
620
+ img.src = imageSrc;
621
+ }
622
+
623
+ // Re-render image previews
624
+ renderImagePreviews();
625
+
626
+ // Scroll to the image input section
627
+ imageInputCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
628
+
629
+ // Show success message with count
630
+ const totalImages = uploadedImages.length;
631
+ showStatus(`Image added as input (${totalImages}/10 slots used)`, 'success');
632
+ addLog(`Added generated image as input for next round (${totalImages}/10 images)`);
633
+
634
+ // Flash the image preview area to draw attention
635
+ imagePreview.style.animation = 'flash 0.5s';
636
+ setTimeout(() => {
637
+ imagePreview.style.animation = '';
638
+ }, 500);
639
+
640
+ } catch (error) {
641
+ console.error('[DEBUG Frontend] Error using result as input:', error);
642
+ showStatus('Failed to add image as input', 'error');
643
+ }
644
+ }
645
+
646
+ // Add function to clear all input images
647
+ function clearAllInputImages() {
648
+ uploadedImages = [];
649
+ imageDimensions = [];
650
+ renderImagePreviews();
651
+ showStatus('All input images cleared', 'info');
652
+ }
static/style.css CHANGED
@@ -42,12 +42,47 @@ header h1 {
42
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
43
  }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  .card h2 {
46
  color: #764ba2;
47
  margin-bottom: 20px;
48
  font-size: 1.4rem;
49
  }
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  .form-group {
52
  margin-bottom: 20px;
53
  }
@@ -149,6 +184,38 @@ header h1 {
149
  object-fit: cover;
150
  }
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  .image-preview-item .remove-btn {
153
  position: absolute;
154
  top: 5px;
@@ -277,6 +344,7 @@ header h1 {
277
  }
278
 
279
  .result-image-item {
 
280
  border-radius: 8px;
281
  overflow: hidden;
282
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
@@ -293,6 +361,39 @@ header h1 {
293
  display: block;
294
  }
295
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  .result-info {
297
  margin-top: 20px;
298
  padding: 15px;
@@ -340,4 +441,18 @@ footer a {
340
  .loading {
341
  pointer-events: none;
342
  opacity: 0.6;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  }
 
42
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
43
  }
44
 
45
+ .card-header {
46
+ display: flex;
47
+ justify-content: space-between;
48
+ align-items: center;
49
+ margin-bottom: 20px;
50
+ }
51
+
52
+ .card-header h2 {
53
+ color: #764ba2;
54
+ margin: 0;
55
+ font-size: 1.4rem;
56
+ }
57
+
58
  .card h2 {
59
  color: #764ba2;
60
  margin-bottom: 20px;
61
  font-size: 1.4rem;
62
  }
63
 
64
+ .clear-all-btn {
65
+ padding: 6px 12px;
66
+ background: #ff4444;
67
+ color: white;
68
+ border: none;
69
+ border-radius: 6px;
70
+ font-size: 0.85rem;
71
+ font-weight: 600;
72
+ cursor: pointer;
73
+ transition: all 0.3s ease;
74
+ }
75
+
76
+ .clear-all-btn:hover {
77
+ background: #cc0000;
78
+ transform: translateY(-1px);
79
+ box-shadow: 0 2px 4px rgba(204, 0, 0, 0.3);
80
+ }
81
+
82
+ .clear-all-btn:active {
83
+ transform: translateY(0);
84
+ }
85
+
86
  .form-group {
87
  margin-bottom: 20px;
88
  }
 
184
  object-fit: cover;
185
  }
186
 
187
+ /* Loading preview styles */
188
+ .loading-preview {
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ background: #f0f2f5;
193
+ border: 2px dashed #d1d9e6;
194
+ }
195
+
196
+ .loading-placeholder {
197
+ text-align: center;
198
+ padding: 20px;
199
+ }
200
+
201
+ .loading-placeholder .spinner {
202
+ width: 30px;
203
+ height: 30px;
204
+ margin: 0 auto 10px;
205
+ border: 3px solid rgba(118, 75, 162, 0.2);
206
+ border-top-color: #764ba2;
207
+ border-radius: 50%;
208
+ animation: spin 0.8s linear infinite;
209
+ }
210
+
211
+ .loading-placeholder p {
212
+ font-size: 0.8rem;
213
+ color: #666;
214
+ margin: 0;
215
+ word-break: break-all;
216
+ max-width: 120px;
217
+ }
218
+
219
  .image-preview-item .remove-btn {
220
  position: absolute;
221
  top: 5px;
 
344
  }
345
 
346
  .result-image-item {
347
+ position: relative;
348
  border-radius: 8px;
349
  overflow: hidden;
350
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
 
361
  display: block;
362
  }
363
 
364
+ .use-as-input-btn {
365
+ position: absolute;
366
+ bottom: 10px;
367
+ right: 10px;
368
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
369
+ color: white;
370
+ border: none;
371
+ padding: 8px 16px;
372
+ border-radius: 6px;
373
+ font-size: 0.9rem;
374
+ font-weight: 600;
375
+ cursor: pointer;
376
+ transition: all 0.3s ease;
377
+ opacity: 0;
378
+ transform: translateY(10px);
379
+ box-shadow: 0 2px 8px rgba(118, 75, 162, 0.3);
380
+ }
381
+
382
+ .result-image-item:hover .use-as-input-btn {
383
+ opacity: 1;
384
+ transform: translateY(0);
385
+ }
386
+
387
+ .use-as-input-btn:hover {
388
+ background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
389
+ box-shadow: 0 4px 12px rgba(118, 75, 162, 0.5);
390
+ transform: translateY(-2px);
391
+ }
392
+
393
+ .use-as-input-btn:active {
394
+ transform: translateY(0);
395
+ }
396
+
397
  .result-info {
398
  margin-top: 20px;
399
  padding: 15px;
 
441
  .loading {
442
  pointer-events: none;
443
  opacity: 0.6;
444
+ }
445
+
446
+ /* Flash animation for visual feedback */
447
+ @keyframes flash {
448
+ 0% {
449
+ background-color: rgba(118, 75, 162, 0.1);
450
+ }
451
+ 50% {
452
+ background-color: rgba(118, 75, 162, 0.2);
453
+ border-color: #764ba2;
454
+ }
455
+ 100% {
456
+ background-color: rgba(118, 75, 162, 0.1);
457
+ }
458
  }
templates/index.html CHANGED
@@ -42,7 +42,12 @@
42
  </div>
43
 
44
  <div class="card" id="imageInputCard">
45
- <h2>Input Images</h2>
 
 
 
 
 
46
  <div class="form-group">
47
  <label>Upload Images (Max 10)</label>
48
  <input type="file" id="fileInput" multiple accept="image/*" />
 
42
  </div>
43
 
44
  <div class="card" id="imageInputCard">
45
+ <div class="card-header">
46
+ <h2>Input Images</h2>
47
+ <button id="clearAllBtn" class="clear-all-btn" onclick="clearAllInputImages()" title="Clear all input images">
48
+ Clear All
49
+ </button>
50
+ </div>
51
  <div class="form-group">
52
  <label>Upload Images (Max 10)</label>
53
  <input type="file" id="fileInput" multiple accept="image/*" />