bibibi12345 commited on
Commit
b252433
·
1 Parent(s): fb44180

ui change

Browse files
Files changed (3) hide show
  1. static/script.js +241 -158
  2. static/style.css +412 -228
  3. templates/index.html +134 -97
static/script.js CHANGED
@@ -1,6 +1,40 @@
1
- // Configuration
2
  let uploadedImages = [];
3
- let imageDimensions = []; // Store dimensions of uploaded images
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  // DOM Elements
6
  const fileInput = document.getElementById('fileInput');
@@ -9,15 +43,16 @@ const imageUrls = document.getElementById('imageUrls');
9
  const generateBtn = document.getElementById('generateBtn');
10
  const statusMessage = document.getElementById('statusMessage');
11
  const progressLogs = document.getElementById('progressLogs');
12
- const results = document.getElementById('results');
13
- const resultImages = document.getElementById('resultImages');
14
- const resultInfo = document.getElementById('resultInfo');
15
  const imageSizeSelect = document.getElementById('imageSize');
16
  const customSizeElements = document.querySelectorAll('.custom-size');
17
  const modelSelect = document.getElementById('modelSelect');
18
  const promptTitle = document.getElementById('promptTitle');
19
  const promptLabel = document.getElementById('promptLabel');
20
  const imageInputCard = document.getElementById('imageInputCard');
 
21
 
22
  // Event Listeners
23
  fileInput.addEventListener('change', handleFileUpload);
@@ -25,6 +60,34 @@ generateBtn.addEventListener('click', generateEdit);
25
  imageSizeSelect.addEventListener('change', handleImageSizeChange);
26
  modelSelect.addEventListener('change', handleModelChange);
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  // Handle image size dropdown change
29
  function handleImageSizeChange() {
30
  if (imageSizeSelect.value === 'custom') {
@@ -39,17 +102,14 @@ function handleModelChange() {
39
  const isTextToImage = modelSelect.value === 'fal-ai/bytedance/seedream/v4/text-to-image';
40
 
41
  if (isTextToImage) {
42
- // Text-to-image mode
43
  promptTitle.textContent = 'Generation Prompt';
44
  promptLabel.textContent = 'Generation Prompt';
45
  document.getElementById('prompt').placeholder = 'e.g., A beautiful landscape with mountains and a lake at sunset';
46
  imageInputCard.style.display = 'none';
47
- // Clear uploaded images when switching to text-to-image
48
  uploadedImages = [];
49
  imageDimensions = [];
50
  renderImagePreviews();
51
  } else {
52
- // Image edit mode
53
  promptTitle.textContent = 'Edit Instructions';
54
  promptLabel.textContent = 'Editing Prompt';
55
  document.getElementById('prompt').placeholder = 'e.g., Dress the model in the clothes and shoes.';
@@ -59,28 +119,28 @@ function handleModelChange() {
59
 
60
  // Initialize on page load
61
  window.addEventListener('DOMContentLoaded', () => {
62
- // Load saved API key if available
63
  const savedKey = localStorage.getItem('fal_api_key');
64
  if (savedKey) {
65
  document.getElementById('apiKey').value = savedKey;
66
  }
67
 
68
- // Show custom size fields by default since "custom" is the default selection
69
  handleImageSizeChange();
70
-
71
- // Initialize model selection
72
  handleModelChange();
 
 
 
 
 
73
  });
74
 
75
- // Handle file upload
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;
@@ -94,11 +154,10 @@ async function handleFileUpload(event) {
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;
@@ -115,20 +174,14 @@ async function handleFileUpload(event) {
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
 
@@ -142,7 +195,6 @@ async function handleFileUpload(event) {
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');
@@ -154,11 +206,7 @@ async function handleFileUpload(event) {
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
  };
@@ -179,7 +227,6 @@ async function handleFileUpload(event) {
179
  }
180
  }
181
 
182
- // Clear the file input so the same files can be selected again if needed
183
  event.target.value = '';
184
  }
185
 
@@ -202,28 +249,25 @@ function removeImage(index) {
202
  updateCustomSizeFromLastImage();
203
  }
204
 
205
- // Update custom size fields based on last image
206
  function updateCustomSizeFromLastImage() {
207
  if (imageDimensions.length > 0) {
208
  const lastDims = imageDimensions[imageDimensions.length - 1];
209
  let width = lastDims.width;
210
  let height = lastDims.height;
211
 
212
- // If both dimensions are less than 1000, scale up
213
  if (width < 1000 && height < 1000) {
214
  const scale = 1000 / Math.max(width, height);
215
  width = Math.round(width * scale);
216
  height = Math.round(height * scale);
217
  }
218
 
219
- // Ensure dimensions are within allowed range (1024-4096)
220
  width = Math.max(1024, Math.min(4096, width));
221
  height = Math.max(1024, Math.min(4096, height));
222
 
223
  document.getElementById('customWidth').value = width;
224
  document.getElementById('customHeight').value = height;
225
 
226
- // Auto-switch to custom size if an image is loaded
227
  if (imageSizeSelect.value !== 'custom') {
228
  imageSizeSelect.value = 'custom';
229
  handleImageSizeChange();
@@ -282,19 +326,15 @@ function getImageSize() {
282
  // Prepare image URLs for API
283
  async function getImageUrlsForAPI() {
284
  const urls = [];
285
-
286
- // Add uploaded images (as data URLs)
287
  urls.push(...uploadedImages);
288
 
289
- // Add text URLs and get their dimensions
290
  const textUrls = imageUrls.value.trim().split('\n').filter(url => url.trim());
291
  for (const url of textUrls) {
292
  urls.push(url);
293
- // Try to get dimensions for URL images
294
  await getImageDimensionsFromUrl(url);
295
  }
296
 
297
- return urls.slice(0, 10); // Maximum 10 images
298
  }
299
 
300
  // Get image dimensions from URL
@@ -310,7 +350,6 @@ async function getImageDimensionsFromUrl(url) {
310
  resolve();
311
  };
312
  img.onerror = function() {
313
- // If can't load, just resolve without adding dimensions
314
  resolve();
315
  };
316
  img.src = url;
@@ -319,47 +358,40 @@ async function getImageDimensionsFromUrl(url) {
319
 
320
  // Generate edit
321
  async function generateEdit() {
322
- // Validate inputs
323
  const prompt = document.getElementById('prompt').value.trim();
324
  if (!prompt) {
325
- showStatus('Please enter an editing prompt', 'error');
326
  return;
327
  }
328
 
329
  const selectedModel = modelSelect.value;
330
  const isTextToImage = selectedModel === 'fal-ai/bytedance/seedream/v4/text-to-image';
331
 
332
- // Only require images for edit mode
333
  const imageUrlsArray = await getImageUrlsForAPI();
334
  if (!isTextToImage && imageUrlsArray.length === 0) {
335
  showStatus('Please upload images or provide image URLs for image editing', 'error');
336
  return;
337
  }
338
 
339
- // Disable button and show loading
340
  generateBtn.disabled = true;
341
  generateBtn.querySelector('.btn-text').textContent = 'Generating...';
342
  generateBtn.querySelector('.spinner').style.display = 'block';
343
 
344
- // Clear previous results
345
- results.style.display = 'none';
346
- resultImages.innerHTML = '';
347
- resultInfo.innerHTML = '';
348
  clearLogs();
349
 
350
- // Show progress
351
  showStatus('Connecting to FAL API...', 'info');
352
  progressLogs.classList.add('active');
353
 
354
- // Prepare request data
355
  const requestData = {
356
  prompt: prompt,
357
  image_size: getImageSize(),
358
  num_images: parseInt(document.getElementById('numImages').value),
359
- enable_safety_checker: false // Always set to false
360
  };
361
 
362
- // Add image URLs only for edit mode
363
  if (!isTextToImage) {
364
  requestData.image_urls = imageUrlsArray;
365
  requestData.max_images = parseInt(document.getElementById('maxImages').value);
@@ -370,12 +402,24 @@ async function generateEdit() {
370
  requestData.seed = parseInt(seed);
371
  }
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  try {
374
- // Check if API key is set
375
  const apiKey = getAPIKey();
376
  if (!apiKey) {
377
- showStatus('Please enter your FAL API key in the API Configuration section', 'error');
378
- addLog('API key not found. Please enter your FAL API key above.');
379
  document.getElementById('apiKey').focus();
380
  return;
381
  }
@@ -387,17 +431,18 @@ async function generateEdit() {
387
  addLog(`Number of input images: ${imageUrlsArray.length}`);
388
  }
389
 
390
- // Log the request data being sent
391
- console.log('[DEBUG Frontend] Request Data:', requestData);
392
- console.log('[DEBUG Frontend] Selected Model:', selectedModel);
393
-
394
- // Make API call with selected model
395
  const response = await callFalAPI(apiKey, requestData, selectedModel);
396
 
397
- console.log('[DEBUG Frontend] Final Response:', response);
 
398
 
399
  // Display results
400
- displayResults(response);
 
 
 
 
 
401
  showStatus('Generation completed successfully!', 'success');
402
 
403
  } catch (error) {
@@ -405,19 +450,17 @@ async function generateEdit() {
405
  showStatus(`Error: ${error.message}`, 'error');
406
  addLog(`Error: ${error.message}`);
407
  } finally {
408
- // Re-enable button
409
  generateBtn.disabled = false;
410
- generateBtn.querySelector('.btn-text').textContent = 'Generate Edit';
411
  generateBtn.querySelector('.spinner').style.display = 'none';
412
  }
413
  }
414
 
415
- // Get API key from the input field
416
  function getAPIKey() {
417
  const apiKeyInput = document.getElementById('apiKey');
418
  const apiKey = apiKeyInput.value.trim();
419
 
420
- // Save to localStorage if provided
421
  if (apiKey) {
422
  localStorage.setItem('fal_api_key', apiKey);
423
  }
@@ -425,12 +468,8 @@ function getAPIKey() {
425
  return apiKey || localStorage.getItem('fal_api_key');
426
  }
427
 
428
- // Call FAL API (proxy through backend) - Non-blocking version
429
  async function callFalAPI(apiKey, requestData, model) {
430
- console.log('[DEBUG Frontend] Starting API call with model:', model);
431
- console.log('[DEBUG Frontend] Request body:', JSON.stringify(requestData).substring(0, 500) + '...');
432
-
433
- // Submit the request (non-blocking)
434
  const submitResponse = await fetch('/api/generate', {
435
  method: 'POST',
436
  headers: {
@@ -441,40 +480,32 @@ async function callFalAPI(apiKey, requestData, model) {
441
  body: JSON.stringify(requestData)
442
  });
443
 
444
- console.log('[DEBUG Frontend] Submit response status:', submitResponse.status);
445
-
446
  if (!submitResponse.ok) {
447
  const error = await submitResponse.text();
448
- console.error('[DEBUG Frontend] Submit error:', error);
449
  throw new Error(error || 'API request failed');
450
  }
451
 
452
  const submitData = await submitResponse.json();
453
- console.log('[DEBUG Frontend] Submit data:', submitData);
454
-
455
  const { request_id } = submitData;
456
  addLog(`Request submitted with ID: ${request_id}`);
457
 
458
  // Poll for results
459
  let attempts = 0;
460
- const maxAttempts = 120; // 2 minutes with 1-second intervals
461
- const pollInterval = 1000; // 1 second
462
  let previousLogCount = 0;
463
 
464
  while (attempts < maxAttempts) {
465
  await new Promise(resolve => setTimeout(resolve, pollInterval));
466
 
467
  const statusUrl = `/api/status/${request_id}`;
468
- console.log(`[DEBUG Frontend] Polling status (attempt ${attempts + 1}): ${statusUrl}`);
469
-
470
  const statusResponse = await fetch(statusUrl);
 
471
  if (!statusResponse.ok) {
472
- console.error('[DEBUG Frontend] Status check failed:', statusResponse.status);
473
  throw new Error('Failed to check request status');
474
  }
475
 
476
  const statusData = await statusResponse.json();
477
- console.log(`[DEBUG Frontend] Status data (attempt ${attempts + 1}):`, statusData);
478
 
479
  // Add any new logs
480
  if (statusData.logs && statusData.logs.length > previousLogCount) {
@@ -482,108 +513,120 @@ async function callFalAPI(apiKey, requestData, model) {
482
  newLogs.forEach(log => {
483
  if (log && !log.includes('Request submitted')) {
484
  addLog(log);
485
- console.log('[DEBUG Frontend] New log:', log);
486
  }
487
  });
488
  previousLogCount = statusData.logs.length;
489
  }
490
 
491
  if (statusData.status === 'completed') {
492
- console.log('[DEBUG Frontend] Request completed. Result:', statusData.result);
493
  return statusData.result;
494
  } else if (statusData.status === 'error') {
495
- console.error('[DEBUG Frontend] Request error:', statusData.error);
496
  throw new Error(statusData.error || 'Generation failed');
497
  }
498
 
499
  attempts++;
500
 
501
- // Update progress indicator
502
  if (attempts % 5 === 0) {
503
  addLog(`Processing... (${attempts}s elapsed)`);
504
  }
505
  }
506
 
507
- console.error('[DEBUG Frontend] Request timed out after', attempts, 'attempts');
508
  throw new Error('Request timed out after 2 minutes');
509
  }
510
 
511
- // Display results
512
- function displayResults(response) {
513
- console.log('[DEBUG Frontend] Displaying results:', response);
514
-
515
- if (!response) {
516
- console.error('[DEBUG Frontend] No response to display');
517
- addLog('Error: No response received from API');
518
  return;
519
  }
520
 
521
- results.style.display = 'block';
522
-
523
- // Display images
524
- if (response.images && response.images.length > 0) {
525
- console.log('[DEBUG Frontend] Found', response.images.length, 'images');
526
- response.images.forEach((image, index) => {
527
- console.log(`[DEBUG Frontend] Image ${index + 1}:`, image);
528
- const imageItem = document.createElement('div');
529
- imageItem.className = 'result-image-item';
530
- const imgSrc = image.url || image.file_data || '';
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
- });
546
 
547
- addLog(`Generated ${response.images.length} image(s)`);
548
- } else {
549
- console.error('[DEBUG Frontend] No images in response');
550
- addLog('Warning: No images found in response');
551
- }
 
 
 
 
 
552
 
553
- // Display info
554
  if (response.seed) {
555
- resultInfo.innerHTML = `<strong>Seed:</strong> ${response.seed}`;
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;
@@ -591,16 +634,13 @@ async function useResultAsInput(imageId, imageSrc) {
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({
@@ -610,43 +650,86 @@ async function useResultAsInput(imageId, imageSrc) {
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
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Configuration and state
2
  let uploadedImages = [];
3
+ let imageDimensions = [];
4
+ let generationHistory = [];
5
+ let currentGeneration = null;
6
+ let activeTab = 'current';
7
+
8
+ // Initialize local storage
9
+ const HISTORY_KEY = 'seedream_generation_history';
10
+
11
+ // Load history from localStorage on startup
12
+ function loadHistory() {
13
+ try {
14
+ const saved = localStorage.getItem(HISTORY_KEY);
15
+ if (saved) {
16
+ generationHistory = JSON.parse(saved);
17
+ updateHistoryCount();
18
+ }
19
+ } catch (error) {
20
+ console.error('Error loading history:', error);
21
+ generationHistory = [];
22
+ }
23
+ }
24
+
25
+ // Save history to localStorage
26
+ function saveHistory() {
27
+ try {
28
+ // Keep only last 100 generations to avoid storage limits
29
+ if (generationHistory.length > 100) {
30
+ generationHistory = generationHistory.slice(-100);
31
+ }
32
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(generationHistory));
33
+ updateHistoryCount();
34
+ } catch (error) {
35
+ console.error('Error saving history:', error);
36
+ }
37
+ }
38
 
39
  // DOM Elements
40
  const fileInput = document.getElementById('fileInput');
 
43
  const generateBtn = document.getElementById('generateBtn');
44
  const statusMessage = document.getElementById('statusMessage');
45
  const progressLogs = document.getElementById('progressLogs');
46
+ const currentResults = document.getElementById('currentResults');
47
+ const currentInfo = document.getElementById('currentInfo');
48
+ const historyGrid = document.getElementById('historyGrid');
49
  const imageSizeSelect = document.getElementById('imageSize');
50
  const customSizeElements = document.querySelectorAll('.custom-size');
51
  const modelSelect = document.getElementById('modelSelect');
52
  const promptTitle = document.getElementById('promptTitle');
53
  const promptLabel = document.getElementById('promptLabel');
54
  const imageInputCard = document.getElementById('imageInputCard');
55
+ const settingsCard = document.getElementById('settingsCard');
56
 
57
  // Event Listeners
58
  fileInput.addEventListener('change', handleFileUpload);
 
60
  imageSizeSelect.addEventListener('change', handleImageSizeChange);
61
  modelSelect.addEventListener('change', handleModelChange);
62
 
63
+ // Tab switching
64
+ function switchTab(tabName) {
65
+ activeTab = tabName;
66
+
67
+ // Update tab buttons
68
+ document.querySelectorAll('.tab-btn').forEach(btn => {
69
+ btn.classList.remove('active');
70
+ });
71
+ event.target.classList.add('active');
72
+
73
+ // Update tab content
74
+ document.querySelectorAll('.tab-content').forEach(content => {
75
+ content.classList.remove('active');
76
+ });
77
+
78
+ if (tabName === 'current') {
79
+ document.getElementById('currentTab').classList.add('active');
80
+ } else {
81
+ document.getElementById('historyTab').classList.add('active');
82
+ displayHistory();
83
+ }
84
+ }
85
+
86
+ // Toggle settings panel
87
+ function toggleSettings() {
88
+ settingsCard.classList.toggle('collapsed');
89
+ }
90
+
91
  // Handle image size dropdown change
92
  function handleImageSizeChange() {
93
  if (imageSizeSelect.value === 'custom') {
 
102
  const isTextToImage = modelSelect.value === 'fal-ai/bytedance/seedream/v4/text-to-image';
103
 
104
  if (isTextToImage) {
 
105
  promptTitle.textContent = 'Generation Prompt';
106
  promptLabel.textContent = 'Generation Prompt';
107
  document.getElementById('prompt').placeholder = 'e.g., A beautiful landscape with mountains and a lake at sunset';
108
  imageInputCard.style.display = 'none';
 
109
  uploadedImages = [];
110
  imageDimensions = [];
111
  renderImagePreviews();
112
  } else {
 
113
  promptTitle.textContent = 'Edit Instructions';
114
  promptLabel.textContent = 'Editing Prompt';
115
  document.getElementById('prompt').placeholder = 'e.g., Dress the model in the clothes and shoes.';
 
119
 
120
  // Initialize on page load
121
  window.addEventListener('DOMContentLoaded', () => {
122
+ // Load saved API key
123
  const savedKey = localStorage.getItem('fal_api_key');
124
  if (savedKey) {
125
  document.getElementById('apiKey').value = savedKey;
126
  }
127
 
128
+ // Initialize UI state
129
  handleImageSizeChange();
 
 
130
  handleModelChange();
131
+ loadHistory();
132
+ displayHistory();
133
+
134
+ // Collapse settings by default
135
+ settingsCard.classList.add('collapsed');
136
  });
137
 
138
+ // Handle file upload with immediate preview
139
  async function handleFileUpload(event) {
140
  const files = Array.from(event.target.files);
141
 
142
+ if (files.length === 0) return;
 
 
143
 
 
144
  showStatus(`Processing ${files.length} image(s)...`, 'info');
145
 
146
  let processedCount = 0;
 
154
 
155
  if (file.type.startsWith('image/')) {
156
  try {
 
157
  const tempIndex = uploadedImages.length;
158
  const loadingId = `loading-${Date.now()}-${tempIndex}`;
159
 
160
+ // Add loading placeholder
161
  const loadingPreview = document.createElement('div');
162
  loadingPreview.className = 'image-preview-item loading-preview';
163
  loadingPreview.id = loadingId;
 
174
  reader.onerror = (error) => {
175
  console.error('Error reading file:', file.name, error);
176
  errorCount++;
177
+ document.getElementById(loadingId)?.remove();
 
 
178
  showStatus(`Failed to read file: ${file.name}`, 'error');
179
  };
180
 
181
  reader.onload = (e) => {
182
  const dataUrl = e.target.result;
183
+ document.getElementById(loadingId)?.remove();
184
 
 
 
 
 
 
185
  uploadedImages.push(dataUrl);
186
  processedCount++;
187
 
 
195
  addImagePreview(dataUrl, uploadedImages.length - 1);
196
  updateCustomSizeFromLastImage();
197
 
 
198
  if (processedCount + errorCount === files.length) {
199
  if (errorCount === 0) {
200
  showStatus(`Successfully added ${processedCount} image(s) (${uploadedImages.length}/10 slots used)`, 'success');
 
206
 
207
  img.onerror = () => {
208
  console.error('Error loading image dimensions for:', file.name);
209
+ imageDimensions.push({ width: 1280, height: 1280 });
 
 
 
 
210
  addImagePreview(dataUrl, uploadedImages.length - 1);
211
  updateCustomSizeFromLastImage();
212
  };
 
227
  }
228
  }
229
 
 
230
  event.target.value = '';
231
  }
232
 
 
249
  updateCustomSizeFromLastImage();
250
  }
251
 
252
+ // Update custom size based on last image
253
  function updateCustomSizeFromLastImage() {
254
  if (imageDimensions.length > 0) {
255
  const lastDims = imageDimensions[imageDimensions.length - 1];
256
  let width = lastDims.width;
257
  let height = lastDims.height;
258
 
 
259
  if (width < 1000 && height < 1000) {
260
  const scale = 1000 / Math.max(width, height);
261
  width = Math.round(width * scale);
262
  height = Math.round(height * scale);
263
  }
264
 
 
265
  width = Math.max(1024, Math.min(4096, width));
266
  height = Math.max(1024, Math.min(4096, height));
267
 
268
  document.getElementById('customWidth').value = width;
269
  document.getElementById('customHeight').value = height;
270
 
 
271
  if (imageSizeSelect.value !== 'custom') {
272
  imageSizeSelect.value = 'custom';
273
  handleImageSizeChange();
 
326
  // Prepare image URLs for API
327
  async function getImageUrlsForAPI() {
328
  const urls = [];
 
 
329
  urls.push(...uploadedImages);
330
 
 
331
  const textUrls = imageUrls.value.trim().split('\n').filter(url => url.trim());
332
  for (const url of textUrls) {
333
  urls.push(url);
 
334
  await getImageDimensionsFromUrl(url);
335
  }
336
 
337
+ return urls.slice(0, 10);
338
  }
339
 
340
  // Get image dimensions from URL
 
350
  resolve();
351
  };
352
  img.onerror = function() {
 
353
  resolve();
354
  };
355
  img.src = url;
 
358
 
359
  // Generate edit
360
  async function generateEdit() {
 
361
  const prompt = document.getElementById('prompt').value.trim();
362
  if (!prompt) {
363
+ showStatus('Please enter a prompt', 'error');
364
  return;
365
  }
366
 
367
  const selectedModel = modelSelect.value;
368
  const isTextToImage = selectedModel === 'fal-ai/bytedance/seedream/v4/text-to-image';
369
 
 
370
  const imageUrlsArray = await getImageUrlsForAPI();
371
  if (!isTextToImage && imageUrlsArray.length === 0) {
372
  showStatus('Please upload images or provide image URLs for image editing', 'error');
373
  return;
374
  }
375
 
 
376
  generateBtn.disabled = true;
377
  generateBtn.querySelector('.btn-text').textContent = 'Generating...';
378
  generateBtn.querySelector('.spinner').style.display = 'block';
379
 
380
+ // Clear current results
381
+ currentResults.innerHTML = '<div class="empty-state"><p>Generating...</p></div>';
382
+ currentInfo.innerHTML = '';
 
383
  clearLogs();
384
 
 
385
  showStatus('Connecting to FAL API...', 'info');
386
  progressLogs.classList.add('active');
387
 
 
388
  const requestData = {
389
  prompt: prompt,
390
  image_size: getImageSize(),
391
  num_images: parseInt(document.getElementById('numImages').value),
392
+ enable_safety_checker: false
393
  };
394
 
 
395
  if (!isTextToImage) {
396
  requestData.image_urls = imageUrlsArray;
397
  requestData.max_images = parseInt(document.getElementById('maxImages').value);
 
402
  requestData.seed = parseInt(seed);
403
  }
404
 
405
+ // Store generation metadata
406
+ currentGeneration = {
407
+ id: Date.now(),
408
+ timestamp: new Date().toISOString(),
409
+ prompt: prompt,
410
+ model: selectedModel,
411
+ settings: {
412
+ image_size: requestData.image_size,
413
+ num_images: requestData.num_images,
414
+ seed: requestData.seed
415
+ }
416
+ };
417
+
418
  try {
 
419
  const apiKey = getAPIKey();
420
  if (!apiKey) {
421
+ showStatus('Please enter your FAL API key', 'error');
422
+ addLog('API key not found');
423
  document.getElementById('apiKey').focus();
424
  return;
425
  }
 
431
  addLog(`Number of input images: ${imageUrlsArray.length}`);
432
  }
433
 
 
 
 
 
 
434
  const response = await callFalAPI(apiKey, requestData, selectedModel);
435
 
436
+ // Store results in current generation
437
+ currentGeneration.results = response;
438
 
439
  // Display results
440
+ displayCurrentResults(response);
441
+
442
+ // Add to history
443
+ generationHistory.push(currentGeneration);
444
+ saveHistory();
445
+
446
  showStatus('Generation completed successfully!', 'success');
447
 
448
  } catch (error) {
 
450
  showStatus(`Error: ${error.message}`, 'error');
451
  addLog(`Error: ${error.message}`);
452
  } finally {
 
453
  generateBtn.disabled = false;
454
+ generateBtn.querySelector('.btn-text').textContent = 'Generate';
455
  generateBtn.querySelector('.spinner').style.display = 'none';
456
  }
457
  }
458
 
459
+ // Get API key
460
  function getAPIKey() {
461
  const apiKeyInput = document.getElementById('apiKey');
462
  const apiKey = apiKeyInput.value.trim();
463
 
 
464
  if (apiKey) {
465
  localStorage.setItem('fal_api_key', apiKey);
466
  }
 
468
  return apiKey || localStorage.getItem('fal_api_key');
469
  }
470
 
471
+ // Call FAL API (non-blocking)
472
  async function callFalAPI(apiKey, requestData, model) {
 
 
 
 
473
  const submitResponse = await fetch('/api/generate', {
474
  method: 'POST',
475
  headers: {
 
480
  body: JSON.stringify(requestData)
481
  });
482
 
 
 
483
  if (!submitResponse.ok) {
484
  const error = await submitResponse.text();
 
485
  throw new Error(error || 'API request failed');
486
  }
487
 
488
  const submitData = await submitResponse.json();
 
 
489
  const { request_id } = submitData;
490
  addLog(`Request submitted with ID: ${request_id}`);
491
 
492
  // Poll for results
493
  let attempts = 0;
494
+ const maxAttempts = 120;
495
+ const pollInterval = 1000;
496
  let previousLogCount = 0;
497
 
498
  while (attempts < maxAttempts) {
499
  await new Promise(resolve => setTimeout(resolve, pollInterval));
500
 
501
  const statusUrl = `/api/status/${request_id}`;
 
 
502
  const statusResponse = await fetch(statusUrl);
503
+
504
  if (!statusResponse.ok) {
 
505
  throw new Error('Failed to check request status');
506
  }
507
 
508
  const statusData = await statusResponse.json();
 
509
 
510
  // Add any new logs
511
  if (statusData.logs && statusData.logs.length > previousLogCount) {
 
513
  newLogs.forEach(log => {
514
  if (log && !log.includes('Request submitted')) {
515
  addLog(log);
 
516
  }
517
  });
518
  previousLogCount = statusData.logs.length;
519
  }
520
 
521
  if (statusData.status === 'completed') {
 
522
  return statusData.result;
523
  } else if (statusData.status === 'error') {
 
524
  throw new Error(statusData.error || 'Generation failed');
525
  }
526
 
527
  attempts++;
528
 
 
529
  if (attempts % 5 === 0) {
530
  addLog(`Processing... (${attempts}s elapsed)`);
531
  }
532
  }
533
 
 
534
  throw new Error('Request timed out after 2 minutes');
535
  }
536
 
537
+ // Display current results
538
+ function displayCurrentResults(response) {
539
+ if (!response || !response.images || response.images.length === 0) {
540
+ currentResults.innerHTML = '<div class="empty-state"><p>No images generated</p></div>';
 
 
 
541
  return;
542
  }
543
 
544
+ currentResults.innerHTML = '';
545
+
546
+ response.images.forEach((image, index) => {
547
+ const imgSrc = image.url || image.file_data || '';
548
+ const imageId = `current-img-${Date.now()}-${index}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
 
550
+ const item = document.createElement('div');
551
+ item.className = 'generation-item';
552
+ item.innerHTML = `
553
+ <img id="${imageId}" src="${imgSrc}" alt="Result ${index + 1}">
554
+ <button class="use-as-input-btn" onclick="useAsInput('${imageId}', '${imgSrc}')" title="Use as input">
555
+ ↻ Use as Input
556
+ </button>
557
+ `;
558
+ currentResults.appendChild(item);
559
+ });
560
 
561
+ // Display generation info
562
  if (response.seed) {
563
+ currentInfo.innerHTML = `<strong>Seed:</strong> ${response.seed}`;
564
  addLog(`Seed used: ${response.seed}`);
565
  }
566
+
567
+ addLog(`Generated ${response.images.length} image(s)`);
568
+ }
569
+
570
+ // Display history
571
+ function displayHistory() {
572
+ if (generationHistory.length === 0) {
573
+ historyGrid.innerHTML = `
574
+ <div class="empty-state">
575
+ <p>No generation history</p>
576
+ <small>Your generated images will be saved here</small>
577
+ </div>
578
+ `;
579
+ return;
580
+ }
581
+
582
+ historyGrid.innerHTML = '';
583
+
584
+ // Display history in reverse order (newest first)
585
+ [...generationHistory].reverse().forEach((generation) => {
586
+ if (generation.results && generation.results.images) {
587
+ generation.results.images.forEach((image, imgIndex) => {
588
+ const imgSrc = image.url || image.file_data || '';
589
+ const imageId = `history-img-${generation.id}-${imgIndex}`;
590
+
591
+ const item = document.createElement('div');
592
+ item.className = 'generation-item';
593
+ item.innerHTML = `
594
+ <img id="${imageId}" src="${imgSrc}" alt="Generation">
595
+ <button class="use-as-input-btn" onclick="useAsInput('${imageId}', '${imgSrc}')" title="Use as input">
596
+ ↻ Use as Input
597
+ </button>
598
+ <div class="generation-meta">
599
+ <span class="timestamp">${new Date(generation.timestamp).toLocaleString()}</span>
600
+ <span class="prompt-preview">${generation.prompt}</span>
601
+ </div>
602
+ `;
603
+ historyGrid.appendChild(item);
604
+ });
605
+ }
606
+ });
607
  }
608
 
609
+ // Use image as input
610
+ async function useAsInput(imageId, imageSrc) {
611
  try {
612
+ // Switch to edit mode if in text-to-image mode
 
 
613
  const currentModel = modelSelect.value;
614
  if (currentModel === 'fal-ai/bytedance/seedream/v4/text-to-image') {
 
615
  modelSelect.value = 'fal-ai/bytedance/seedream/v4/edit';
616
  handleModelChange();
617
+ showStatus('Switched to Image Edit mode', 'info');
618
  }
619
 
 
620
  if (uploadedImages.length >= 10) {
621
  showStatus('Maximum 10 images allowed. Please remove some images first.', 'error');
622
  return;
623
  }
624
 
 
625
  uploadedImages.push(imageSrc);
626
 
627
+ // Get dimensions
628
  const imgElement = document.getElementById(imageId);
629
  if (imgElement) {
 
630
  if (!imgElement.complete) {
631
  await new Promise((resolve) => {
632
  imgElement.onload = resolve;
 
634
  });
635
  }
636
 
 
637
  imageDimensions.push({
638
  width: imgElement.naturalWidth || imgElement.width,
639
  height: imgElement.naturalHeight || imgElement.height
640
  });
641
 
 
642
  updateCustomSizeFromLastImage();
643
  } else {
 
644
  const img = new Image();
645
  img.onload = function() {
646
  imageDimensions.push({
 
650
  updateCustomSizeFromLastImage();
651
  };
652
  img.onerror = function() {
653
+ imageDimensions.push({ width: 1280, height: 1280 });
 
 
 
 
654
  updateCustomSizeFromLastImage();
655
  };
656
  img.src = imageSrc;
657
  }
658
 
 
659
  renderImagePreviews();
660
 
 
 
 
 
661
  const totalImages = uploadedImages.length;
662
  showStatus(`Image added as input (${totalImages}/10 slots used)`, 'success');
663
+ addLog(`Added image as input (${totalImages}/10 images)`);
664
 
665
+ // Flash animation
666
  imagePreview.style.animation = 'flash 0.5s';
667
  setTimeout(() => {
668
  imagePreview.style.animation = '';
669
  }, 500);
670
 
671
  } catch (error) {
672
+ console.error('Error using image as input:', error);
673
  showStatus('Failed to add image as input', 'error');
674
  }
675
  }
676
 
677
+ // Clear all input images
678
  function clearAllInputImages() {
679
  uploadedImages = [];
680
  imageDimensions = [];
681
  renderImagePreviews();
682
  showStatus('All input images cleared', 'info');
683
  }
684
+
685
+ // Clear history
686
+ function clearHistory() {
687
+ if (confirm('Are you sure you want to clear all generation history? This cannot be undone.')) {
688
+ generationHistory = [];
689
+ localStorage.removeItem(HISTORY_KEY);
690
+ displayHistory();
691
+ updateHistoryCount();
692
+ showStatus('History cleared', 'info');
693
+ }
694
+ }
695
+
696
+ // Download all history
697
+ function downloadAllHistory() {
698
+ if (generationHistory.length === 0) {
699
+ showStatus('No history to download', 'error');
700
+ return;
701
+ }
702
+
703
+ // Create a zip file or download each image
704
+ generationHistory.forEach((generation, genIndex) => {
705
+ if (generation.results && generation.results.images) {
706
+ generation.results.images.forEach((image, imgIndex) => {
707
+ const imgSrc = image.url || image.file_data || '';
708
+ if (imgSrc) {
709
+ const link = document.createElement('a');
710
+ link.href = imgSrc;
711
+ link.download = `seedream-${generation.id}-${imgIndex}.png`;
712
+ document.body.appendChild(link);
713
+ link.click();
714
+ document.body.removeChild(link);
715
+ }
716
+ });
717
+ }
718
+ });
719
+
720
+ showStatus('Downloading all images...', 'info');
721
+ }
722
+
723
+ // Update history count
724
+ function updateHistoryCount() {
725
+ const countElement = document.getElementById('historyCount');
726
+ if (countElement) {
727
+ let totalImages = 0;
728
+ generationHistory.forEach(gen => {
729
+ if (gen.results && gen.results.images) {
730
+ totalImages += gen.results.images.length;
731
+ }
732
+ });
733
+ countElement.textContent = totalImages;
734
+ }
735
+ }
static/style.css CHANGED
@@ -6,118 +6,304 @@
6
 
7
  body {
8
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
9
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
- min-height: 100vh;
11
- padding: 20px;
12
  color: #333;
 
 
13
  }
14
 
15
- .container {
16
- max-width: 1200px;
17
- margin: 0 auto;
 
 
18
  }
19
 
20
- header {
21
- text-align: center;
22
- margin-bottom: 40px;
 
 
 
 
 
 
 
 
 
 
23
  color: white;
 
 
 
 
 
24
  }
25
 
26
- header h1 {
27
- font-size: 2.5rem;
28
- margin-bottom: 10px;
29
- text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
30
  }
31
 
32
  .subtitle {
33
- font-size: 1.1rem;
34
- opacity: 0.95;
35
  }
36
 
37
- .card {
38
- background: white;
39
- border-radius: 12px;
40
- padding: 25px;
41
- margin-bottom: 20px;
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
  }
89
 
90
- .form-group label {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  display: block;
92
- margin-bottom: 8px;
93
- font-weight: 600;
94
- color: #555;
95
  }
96
 
97
- .help-text {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  display: block;
99
- margin-top: 5px;
100
- font-size: 0.85rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  color: #666;
102
  }
103
 
104
- .help-text a {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  color: #764ba2;
106
- text-decoration: none;
 
107
  }
108
 
109
- .help-text a:hover {
110
- text-decoration: underline;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }
112
 
113
  .form-group input[type="text"],
 
114
  .form-group input[type="number"],
115
  .form-group textarea,
116
  .form-group select {
117
  width: 100%;
118
- padding: 10px 12px;
119
  border: 2px solid #e1e8ed;
120
- border-radius: 8px;
121
  font-size: 14px;
122
  transition: border-color 0.3s;
123
  }
@@ -138,44 +324,49 @@ header h1 {
138
  padding: 8px;
139
  background: #f7f9fc;
140
  border: 2px dashed #d1d9e6;
141
- border-radius: 8px;
142
  cursor: pointer;
143
  width: 100%;
144
  }
145
 
146
- .settings-grid {
147
- display: grid;
148
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
149
- gap: 15px;
 
150
  }
151
 
152
- .checkbox-label {
153
- display: flex;
154
- align-items: center;
155
- cursor: pointer;
156
- user-select: none;
157
  }
158
 
159
- .checkbox-label input[type="checkbox"] {
160
- margin-right: 8px;
161
- width: 18px;
162
- height: 18px;
163
- cursor: pointer;
 
 
 
 
164
  }
165
 
 
166
  .image-preview {
167
  display: grid;
168
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
169
- gap: 15px;
170
- margin-top: 15px;
171
  }
172
 
173
  .image-preview-item {
174
  position: relative;
175
- border-radius: 8px;
176
  overflow: hidden;
177
  background: #f7f9fc;
178
  aspect-ratio: 1;
 
179
  }
180
 
181
  .image-preview-item img {
@@ -184,7 +375,30 @@ header h1 {
184
  object-fit: cover;
185
  }
186
 
187
- /* Loading preview styles */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  .loading-preview {
189
  display: flex;
190
  align-items: center;
@@ -195,57 +409,36 @@ header h1 {
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;
222
- right: 5px;
223
- background: rgba(255, 59, 48, 0.9);
224
- color: white;
225
- border: none;
226
- border-radius: 50%;
227
- width: 24px;
228
- height: 24px;
229
- cursor: pointer;
230
- display: flex;
231
- align-items: center;
232
- justify-content: center;
233
- font-size: 16px;
234
- transition: background 0.3s;
235
- }
236
-
237
- .image-preview-item .remove-btn:hover {
238
- background: rgba(255, 59, 48, 1);
239
  }
240
 
 
241
  .generate-btn {
242
  width: 100%;
243
- padding: 16px;
244
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
245
  color: white;
246
  border: none;
247
  border-radius: 8px;
248
- font-size: 1.1rem;
249
  font-weight: 600;
250
  cursor: pointer;
251
  transition: transform 0.2s, box-shadow 0.2s;
@@ -266,24 +459,30 @@ header h1 {
266
  transform: none;
267
  }
268
 
269
- .spinner {
270
- width: 20px;
271
- height: 20px;
272
- border: 3px solid rgba(255, 255, 255, 0.3);
273
- border-top-color: white;
274
- border-radius: 50%;
275
- animation: spin 1s linear infinite;
 
 
 
276
  }
277
 
278
- @keyframes spin {
279
- to { transform: rotate(360deg); }
 
280
  }
281
 
 
282
  .status-message {
283
- margin-top: 20px;
284
- padding: 15px;
285
- border-radius: 8px;
286
  display: none;
 
287
  }
288
 
289
  .status-message.info {
@@ -307,14 +506,23 @@ header h1 {
307
  display: block;
308
  }
309
 
 
 
 
 
 
 
 
 
310
  .progress-logs {
311
- margin-top: 15px;
312
- padding: 15px;
313
  background: #f5f7fa;
314
- border-radius: 8px;
315
- max-height: 200px;
316
  overflow-y: auto;
317
  display: none;
 
318
  }
319
 
320
  .progress-logs.active {
@@ -322,8 +530,7 @@ header h1 {
322
  }
323
 
324
  .progress-logs .log-entry {
325
- padding: 5px 0;
326
- font-size: 0.9rem;
327
  color: #666;
328
  border-bottom: 1px solid #e1e8ed;
329
  }
@@ -332,127 +539,104 @@ header h1 {
332
  border-bottom: none;
333
  }
334
 
335
- .results {
336
- margin-top: 30px;
337
- }
338
-
339
- .result-images {
340
- display: grid;
341
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
342
- gap: 20px;
343
- margin-top: 20px;
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);
351
- transition: transform 0.3s;
352
- }
353
-
354
- .result-image-item:hover {
355
- transform: scale(1.02);
356
  }
357
 
358
- .result-image-item img {
359
- width: 100%;
360
- height: auto;
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;
400
- background: #f7f9fc;
401
- border-radius: 8px;
402
- font-size: 0.9rem;
403
  }
404
 
405
- .result-info strong {
406
- color: #764ba2;
407
  }
408
 
409
- footer {
410
- margin-top: 50px;
411
- text-align: center;
412
- color: white;
413
- padding: 20px;
414
  }
415
 
416
- footer a {
417
- color: white;
418
- font-weight: 600;
419
  }
420
 
 
421
  .custom-size {
422
  transition: all 0.3s ease;
423
- }
424
-
425
- /* Responsive Design */
426
- @media (max-width: 768px) {
427
- header h1 {
428
- font-size: 2rem;
429
- }
430
-
431
- .settings-grid {
432
- grid-template-columns: 1fr;
433
- }
434
-
435
- .result-images {
436
- grid-template-columns: 1fr;
437
- }
438
- }
439
-
440
- /* Loading state */
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
  }
 
6
 
7
  body {
8
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
9
+ background: #1a1a2e;
 
 
10
  color: #333;
11
+ height: 100vh;
12
+ overflow: hidden;
13
  }
14
 
15
+ /* Main App Container - Two Column Layout */
16
+ .app-container {
17
+ display: flex;
18
+ height: 100vh;
19
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
20
  }
21
 
22
+ /* Left Panel - Controls */
23
+ .left-panel {
24
+ width: 45%;
25
+ min-width: 500px;
26
+ background: #f8f9fa;
27
+ overflow-y: auto;
28
+ box-shadow: 2px 0 10px rgba(0,0,0,0.1);
29
+ display: flex;
30
+ flex-direction: column;
31
+ }
32
+
33
+ .left-panel header {
34
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
35
  color: white;
36
+ padding: 20px;
37
+ text-align: center;
38
+ position: sticky;
39
+ top: 0;
40
+ z-index: 10;
41
  }
42
 
43
+ .left-panel header h1 {
44
+ font-size: 1.8rem;
45
+ margin-bottom: 5px;
 
46
  }
47
 
48
  .subtitle {
49
+ font-size: 0.9rem;
50
+ opacity: 0.9;
51
  }
52
 
53
+ .controls-section {
54
+ padding: 20px;
55
+ flex: 1;
 
 
 
56
  }
57
 
58
+ /* Right Panel - Results & History */
59
+ .right-panel {
60
+ flex: 1;
61
+ background: #2d2d2d;
62
+ color: white;
63
+ display: flex;
64
+ flex-direction: column;
65
+ overflow: hidden;
66
+ }
67
+
68
+ .history-header {
69
+ background: #1a1a2e;
70
+ padding: 15px 20px;
71
  display: flex;
72
  justify-content: space-between;
73
  align-items: center;
74
+ border-bottom: 1px solid #444;
75
  }
76
 
77
+ .history-header h2 {
78
+ font-size: 1.3rem;
79
+ color: white;
 
80
  }
81
 
82
+ .history-controls {
83
+ display: flex;
84
+ gap: 10px;
 
85
  }
86
 
87
+ .history-btn {
88
+ background: rgba(255, 255, 255, 0.1);
 
89
  color: white;
90
+ border: 1px solid rgba(255, 255, 255, 0.2);
91
+ padding: 6px 12px;
92
  border-radius: 6px;
 
 
93
  cursor: pointer;
94
+ font-size: 0.85rem;
95
+ transition: all 0.3s;
96
  }
97
 
98
+ .history-btn:hover {
99
+ background: rgba(255, 255, 255, 0.2);
100
  transform: translateY(-1px);
 
101
  }
102
 
103
+ /* Tabs */
104
+ .history-tabs {
105
+ display: flex;
106
+ background: #242438;
107
+ border-bottom: 1px solid #444;
108
  }
109
 
110
+ .tab-btn {
111
+ flex: 1;
112
+ padding: 12px;
113
+ background: transparent;
114
+ color: #aaa;
115
+ border: none;
116
+ cursor: pointer;
117
+ font-size: 0.95rem;
118
+ transition: all 0.3s;
119
+ border-bottom: 2px solid transparent;
120
  }
121
 
122
+ .tab-btn:hover {
123
+ background: rgba(255, 255, 255, 0.05);
124
+ }
125
+
126
+ .tab-btn.active {
127
+ color: white;
128
+ border-bottom-color: #764ba2;
129
+ background: rgba(118, 75, 162, 0.1);
130
+ }
131
+
132
+ .tab-content {
133
+ display: none;
134
+ flex: 1;
135
+ overflow-y: auto;
136
+ padding: 20px;
137
+ }
138
+
139
+ .tab-content.active {
140
  display: block;
 
 
 
141
  }
142
 
143
+ /* Results Grid */
144
+ .results-grid, .history-grid {
145
+ display: grid;
146
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
147
+ gap: 15px;
148
+ }
149
+
150
+ .generation-item {
151
+ position: relative;
152
+ border-radius: 8px;
153
+ overflow: hidden;
154
+ background: #1a1a2e;
155
+ cursor: pointer;
156
+ transition: all 0.3s;
157
+ border: 2px solid transparent;
158
+ }
159
+
160
+ .generation-item:hover {
161
+ transform: scale(1.03);
162
+ border-color: #764ba2;
163
+ box-shadow: 0 4px 12px rgba(118, 75, 162, 0.3);
164
+ }
165
+
166
+ .generation-item img {
167
+ width: 100%;
168
+ height: auto;
169
  display: block;
170
+ }
171
+
172
+ .generation-meta {
173
+ padding: 8px;
174
+ background: rgba(0, 0, 0, 0.7);
175
+ font-size: 0.75rem;
176
+ color: #ccc;
177
+ }
178
+
179
+ .generation-meta .timestamp {
180
+ display: block;
181
+ margin-bottom: 4px;
182
+ }
183
+
184
+ .generation-meta .prompt-preview {
185
+ display: block;
186
+ white-space: nowrap;
187
+ overflow: hidden;
188
+ text-overflow: ellipsis;
189
+ opacity: 0.8;
190
+ }
191
+
192
+ .use-as-input-btn {
193
+ position: absolute;
194
+ top: 8px;
195
+ right: 8px;
196
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
197
+ color: white;
198
+ border: none;
199
+ padding: 6px 10px;
200
+ border-radius: 4px;
201
+ font-size: 0.75rem;
202
+ font-weight: 600;
203
+ cursor: pointer;
204
+ opacity: 0;
205
+ transition: all 0.3s;
206
+ }
207
+
208
+ .generation-item:hover .use-as-input-btn {
209
+ opacity: 1;
210
+ }
211
+
212
+ .use-as-input-btn:hover {
213
+ transform: scale(1.1);
214
+ }
215
+
216
+ /* Empty State */
217
+ .empty-state {
218
+ text-align: center;
219
+ padding: 40px;
220
  color: #666;
221
  }
222
 
223
+ .empty-state p {
224
+ font-size: 1.1rem;
225
+ margin-bottom: 8px;
226
+ color: #999;
227
+ }
228
+
229
+ .empty-state small {
230
+ font-size: 0.85rem;
231
+ opacity: 0.7;
232
+ }
233
+
234
+ /* Generation Info */
235
+ .generation-info {
236
+ background: #1a1a2e;
237
+ padding: 15px;
238
+ border-radius: 8px;
239
+ margin-top: 15px;
240
+ font-size: 0.85rem;
241
+ color: #ccc;
242
+ }
243
+
244
+ /* Cards */
245
+ .card {
246
+ background: white;
247
+ border-radius: 12px;
248
+ padding: 20px;
249
+ margin-bottom: 15px;
250
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
251
+ }
252
+
253
+ .card-header {
254
+ display: flex;
255
+ justify-content: space-between;
256
+ align-items: center;
257
+ margin-bottom: 15px;
258
+ }
259
+
260
+ .card-header.clickable {
261
+ cursor: pointer;
262
+ user-select: none;
263
+ }
264
+
265
+ .card-header h2 {
266
  color: #764ba2;
267
+ margin: 0;
268
+ font-size: 1.2rem;
269
  }
270
 
271
+ .card.collapsed .settings-content {
272
+ display: none;
273
+ }
274
+
275
+ .card.collapsed .toggle-icon {
276
+ transform: rotate(-90deg);
277
+ }
278
+
279
+ .toggle-icon {
280
+ color: #764ba2;
281
+ transition: transform 0.3s;
282
+ font-size: 0.8rem;
283
+ }
284
+
285
+ /* Form Elements */
286
+ .form-group {
287
+ margin-bottom: 15px;
288
+ }
289
+
290
+ .form-group label {
291
+ display: block;
292
+ margin-bottom: 6px;
293
+ font-weight: 600;
294
+ color: #555;
295
+ font-size: 0.9rem;
296
  }
297
 
298
  .form-group input[type="text"],
299
+ .form-group input[type="password"],
300
  .form-group input[type="number"],
301
  .form-group textarea,
302
  .form-group select {
303
  width: 100%;
304
+ padding: 8px 10px;
305
  border: 2px solid #e1e8ed;
306
+ border-radius: 6px;
307
  font-size: 14px;
308
  transition: border-color 0.3s;
309
  }
 
324
  padding: 8px;
325
  background: #f7f9fc;
326
  border: 2px dashed #d1d9e6;
327
+ border-radius: 6px;
328
  cursor: pointer;
329
  width: 100%;
330
  }
331
 
332
+ .help-text {
333
+ display: block;
334
+ margin-top: 4px;
335
+ font-size: 0.75rem;
336
+ color: #666;
337
  }
338
 
339
+ .help-text a {
340
+ color: #764ba2;
341
+ text-decoration: none;
 
 
342
  }
343
 
344
+ .help-text a:hover {
345
+ text-decoration: underline;
346
+ }
347
+
348
+ /* Settings Grid */
349
+ .settings-grid {
350
+ display: grid;
351
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
352
+ gap: 12px;
353
  }
354
 
355
+ /* Image Preview */
356
  .image-preview {
357
  display: grid;
358
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
359
+ gap: 10px;
360
+ margin-top: 10px;
361
  }
362
 
363
  .image-preview-item {
364
  position: relative;
365
+ border-radius: 6px;
366
  overflow: hidden;
367
  background: #f7f9fc;
368
  aspect-ratio: 1;
369
+ border: 2px solid #e1e8ed;
370
  }
371
 
372
  .image-preview-item img {
 
375
  object-fit: cover;
376
  }
377
 
378
+ .image-preview-item .remove-btn {
379
+ position: absolute;
380
+ top: 4px;
381
+ right: 4px;
382
+ background: rgba(255, 59, 48, 0.9);
383
+ color: white;
384
+ border: none;
385
+ border-radius: 50%;
386
+ width: 20px;
387
+ height: 20px;
388
+ cursor: pointer;
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+ font-size: 14px;
393
+ transition: background 0.3s;
394
+ }
395
+
396
+ .image-preview-item .remove-btn:hover {
397
+ background: rgba(255, 59, 48, 1);
398
+ transform: scale(1.1);
399
+ }
400
+
401
+ /* Loading Preview */
402
  .loading-preview {
403
  display: flex;
404
  align-items: center;
 
409
 
410
  .loading-placeholder {
411
  text-align: center;
412
+ padding: 10px;
413
  }
414
 
415
  .loading-placeholder .spinner {
416
+ width: 24px;
417
+ height: 24px;
418
+ margin: 0 auto 6px;
419
+ border: 2px solid rgba(118, 75, 162, 0.2);
420
  border-top-color: #764ba2;
421
  border-radius: 50%;
422
  animation: spin 0.8s linear infinite;
423
  }
424
 
425
  .loading-placeholder p {
426
+ font-size: 0.7rem;
427
  color: #666;
428
  margin: 0;
429
  word-break: break-all;
430
+ max-width: 90px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  }
432
 
433
+ /* Buttons */
434
  .generate-btn {
435
  width: 100%;
436
+ padding: 14px;
437
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
438
  color: white;
439
  border: none;
440
  border-radius: 8px;
441
+ font-size: 1rem;
442
  font-weight: 600;
443
  cursor: pointer;
444
  transition: transform 0.2s, box-shadow 0.2s;
 
459
  transform: none;
460
  }
461
 
462
+ .clear-all-btn {
463
+ padding: 5px 10px;
464
+ background: #ff4444;
465
+ color: white;
466
+ border: none;
467
+ border-radius: 4px;
468
+ font-size: 0.75rem;
469
+ font-weight: 600;
470
+ cursor: pointer;
471
+ transition: all 0.3s;
472
  }
473
 
474
+ .clear-all-btn:hover {
475
+ background: #cc0000;
476
+ transform: translateY(-1px);
477
  }
478
 
479
+ /* Status Messages */
480
  .status-message {
481
+ margin-top: 15px;
482
+ padding: 12px;
483
+ border-radius: 6px;
484
  display: none;
485
+ font-size: 0.85rem;
486
  }
487
 
488
  .status-message.info {
 
506
  display: block;
507
  }
508
 
509
+ .status-message.warning {
510
+ background: #fff3e0;
511
+ color: #e65100;
512
+ border-left: 4px solid #e65100;
513
+ display: block;
514
+ }
515
+
516
+ /* Progress Logs */
517
  .progress-logs {
518
+ margin-top: 10px;
519
+ padding: 10px;
520
  background: #f5f7fa;
521
+ border-radius: 6px;
522
+ max-height: 150px;
523
  overflow-y: auto;
524
  display: none;
525
+ font-size: 0.75rem;
526
  }
527
 
528
  .progress-logs.active {
 
530
  }
531
 
532
  .progress-logs .log-entry {
533
+ padding: 3px 0;
 
534
  color: #666;
535
  border-bottom: 1px solid #e1e8ed;
536
  }
 
539
  border-bottom: none;
540
  }
541
 
542
+ /* Spinner Animation */
543
+ .spinner {
544
+ width: 18px;
545
+ height: 18px;
546
+ border: 2px solid rgba(255, 255, 255, 0.3);
547
+ border-top-color: white;
548
+ border-radius: 50%;
549
+ animation: spin 1s linear infinite;
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  }
551
 
552
+ @keyframes spin {
553
+ to { transform: rotate(360deg); }
 
 
554
  }
555
 
556
+ /* Flash Animation */
557
+ @keyframes flash {
558
+ 0% {
559
+ background-color: rgba(118, 75, 162, 0.1);
560
+ }
561
+ 50% {
562
+ background-color: rgba(118, 75, 162, 0.2);
563
+ border-color: #764ba2;
564
+ }
565
+ 100% {
566
+ background-color: rgba(118, 75, 162, 0.1);
567
+ }
 
 
 
 
568
  }
569
 
570
+ /* Responsive Design */
571
+ @media (max-width: 1200px) {
572
+ .left-panel {
573
+ width: 50%;
574
+ min-width: 400px;
575
+ }
576
  }
577
 
578
+ @media (max-width: 900px) {
579
+ .app-container {
580
+ flex-direction: column;
581
+ }
582
+
583
+ .left-panel {
584
+ width: 100%;
585
+ min-width: auto;
586
+ height: 50vh;
587
+ border-right: none;
588
+ border-bottom: 2px solid #ddd;
589
+ }
590
+
591
+ .right-panel {
592
+ height: 50vh;
593
+ }
594
+
595
+ .results-grid, .history-grid {
596
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
597
+ }
598
  }
599
 
600
+ @media (max-width: 600px) {
601
+ .settings-grid {
602
+ grid-template-columns: 1fr;
603
+ }
604
+
605
+ .results-grid, .history-grid {
606
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
607
+ }
608
+
609
+ .history-controls {
610
+ flex-direction: column;
611
+ gap: 5px;
612
+ }
613
+
614
+ .history-btn {
615
+ width: 100%;
616
+ text-align: center;
617
+ }
618
  }
619
 
620
+ /* Scrollbar Styling */
621
+ ::-webkit-scrollbar {
622
+ width: 8px;
623
+ height: 8px;
 
 
624
  }
625
 
626
+ ::-webkit-scrollbar-track {
627
+ background: rgba(0, 0, 0, 0.1);
628
  }
629
 
630
+ ::-webkit-scrollbar-thumb {
631
+ background: rgba(118, 75, 162, 0.5);
632
+ border-radius: 4px;
 
 
633
  }
634
 
635
+ ::-webkit-scrollbar-thumb:hover {
636
+ background: rgba(118, 75, 162, 0.7);
 
637
  }
638
 
639
+ /* Custom size fields visibility */
640
  .custom-size {
641
  transition: all 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  }
templates/index.html CHANGED
@@ -3,127 +3,164 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SeedDream v4 Edit - Image Editor</title>
7
  <link rel="stylesheet" href="/static/style.css">
8
  </head>
9
  <body>
10
- <div class="container">
11
- <header>
12
- <h1>🎨 SeedDream v4 Edit</h1>
13
- <p class="subtitle">AI-powered image editing using ByteDance's SeedDream model</p>
14
- </header>
15
-
16
- <main>
17
- <div class="card">
18
- <h2>API Configuration</h2>
19
- <div class="settings-grid">
20
- <div class="form-group">
21
- <label for="apiKey">FAL API Key</label>
22
- <input type="password" id="apiKey" placeholder="Enter your FAL API key" />
23
- <small class="help-text">Get your API key from <a href="https://fal.ai" target="_blank">fal.ai</a></small>
24
- </div>
25
- <div class="form-group">
26
- <label for="modelSelect">Model</label>
27
- <select id="modelSelect">
28
- <option value="fal-ai/bytedance/seedream/v4/edit">Image Edit</option>
29
- <option value="fal-ai/bytedance/seedream/v4/text-to-image">Text to Image</option>
30
- </select>
31
- <small class="help-text">Select the model for generation</small>
32
- </div>
33
- </div>
34
- </div>
35
 
36
- <div class="card">
37
- <h2 id="promptTitle">Edit Instructions</h2>
38
- <div class="form-group">
39
- <label for="prompt" id="promptLabel">Editing Prompt</label>
40
- <textarea id="prompt" rows="3" placeholder="e.g., Dress the model in the clothes and shoes.">Dress the model in the clothes and shoes.</textarea>
41
- </div>
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/*" />
54
- <div id="imagePreview" class="image-preview"></div>
55
- </div>
56
-
57
- <div class="form-group">
58
- <label for="imageUrls">Or Enter Image URLs (one per line)</label>
59
- <textarea id="imageUrls" rows="4" placeholder="https://example.com/image1.jpg&#10;https://example.com/image2.jpg"></textarea>
60
  </div>
61
- </div>
62
 
63
- <div class="card">
64
- <h2>Settings</h2>
65
- <div class="settings-grid">
66
  <div class="form-group">
67
- <label for="imageSize">Image Size</label>
68
- <select id="imageSize">
69
- <option value="custom" selected>Custom Size</option>
70
- <option value="square_hd">Square HD (1024x1024)</option>
71
- <option value="square">Square</option>
72
- <option value="portrait_4_3">Portrait 4:3</option>
73
- <option value="portrait_16_9">Portrait 16:9</option>
74
- <option value="landscape_4_3">Landscape 4:3</option>
75
- <option value="landscape_16_9">Landscape 16:9</option>
76
- </select>
77
- </div>
78
-
79
- <div class="form-group custom-size">
80
- <label>Custom Width</label>
81
- <input type="number" id="customWidth" min="1024" max="4096" value="1280" />
82
  </div>
 
83
 
84
- <div class="form-group custom-size">
85
- <label>Custom Height</label>
86
- <input type="number" id="customHeight" min="1024" max="4096" value="1280" />
 
 
 
87
  </div>
88
-
89
  <div class="form-group">
90
- <label for="numImages">Number of Generations</label>
91
- <input type="number" id="numImages" min="1" max="10" value="1" />
 
92
  </div>
93
-
94
  <div class="form-group">
95
- <label for="maxImages">Max Images per Generation</label>
96
- <input type="number" id="maxImages" min="1" max="10" value="1" />
97
  </div>
 
98
 
99
- <div class="form-group">
100
- <label for="seed">Seed (optional)</label>
101
- <input type="number" id="seed" placeholder="Random" />
 
102
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- <!-- Safety checker is disabled by default and hidden from UI -->
105
- <input type="hidden" id="safetyChecker" value="false" />
 
 
 
 
 
 
 
106
  </div>
 
 
 
 
 
 
 
 
107
  </div>
 
108
 
109
- <button id="generateBtn" class="generate-btn">
110
- <span class="btn-text">Generate Edit</span>
111
- <div class="spinner" style="display: none;"></div>
112
- </button>
 
 
 
 
 
 
 
 
 
113
 
114
- <div id="statusMessage" class="status-message"></div>
115
- <div id="progressLogs" class="progress-logs"></div>
 
 
116
 
117
- <div id="results" class="results" style="display: none;">
118
- <h2>Results</h2>
119
- <div id="resultImages" class="result-images"></div>
120
- <div id="resultInfo" class="result-info"></div>
 
 
 
 
121
  </div>
122
- </main>
123
 
124
- <footer>
125
- <p>Powered by <a href="https://fal.ai" target="_blank">fal.ai</a> and ByteDance SeedDream v4</p>
126
- </footer>
 
 
 
 
 
 
127
  </div>
128
 
129
  <script src="/static/script.js"></script>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SeedDream v4 - AI Image Generator & Editor</title>
7
  <link rel="stylesheet" href="/static/style.css">
8
  </head>
9
  <body>
10
+ <div class="app-container">
11
+ <!-- Left Panel: Controls -->
12
+ <div class="left-panel">
13
+ <header>
14
+ <h1>🎨 SeedDream v4</h1>
15
+ <p class="subtitle">AI-powered image generation & editing</p>
16
+ </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ <div class="controls-section">
19
+ <div class="card">
20
+ <h2>API Configuration</h2>
21
+ <div class="settings-grid">
22
+ <div class="form-group">
23
+ <label for="apiKey">FAL API Key</label>
24
+ <input type="password" id="apiKey" placeholder="Enter your FAL API key" />
25
+ <small class="help-text">Get your API key from <a href="https://fal.ai" target="_blank">fal.ai</a></small>
26
+ </div>
27
+ <div class="form-group">
28
+ <label for="modelSelect">Model</label>
29
+ <select id="modelSelect">
30
+ <option value="fal-ai/bytedance/seedream/v4/edit">Image Edit</option>
31
+ <option value="fal-ai/bytedance/seedream/v4/text-to-image">Text to Image</option>
32
+ </select>
33
+ <small class="help-text">Select the model for generation</small>
34
+ </div>
35
+ </div>
 
 
 
 
 
 
36
  </div>
 
37
 
38
+ <div class="card">
39
+ <h2 id="promptTitle">Edit Instructions</h2>
 
40
  <div class="form-group">
41
+ <label for="prompt" id="promptLabel">Editing Prompt</label>
42
+ <textarea id="prompt" rows="3" placeholder="e.g., Dress the model in the clothes and shoes.">Dress the model in the clothes and shoes.</textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  </div>
44
+ </div>
45
 
46
+ <div class="card" id="imageInputCard">
47
+ <div class="card-header">
48
+ <h2>Input Images</h2>
49
+ <button id="clearAllBtn" class="clear-all-btn" onclick="clearAllInputImages()" title="Clear all input images">
50
+ Clear All
51
+ </button>
52
  </div>
 
53
  <div class="form-group">
54
+ <label>Upload Images (Max 10)</label>
55
+ <input type="file" id="fileInput" multiple accept="image/*" />
56
+ <div id="imagePreview" class="image-preview"></div>
57
  </div>
58
+
59
  <div class="form-group">
60
+ <label for="imageUrls">Or Enter Image URLs (one per line)</label>
61
+ <textarea id="imageUrls" rows="3" placeholder="https://example.com/image1.jpg&#10;https://example.com/image2.jpg"></textarea>
62
  </div>
63
+ </div>
64
 
65
+ <div class="card collapsed" id="settingsCard">
66
+ <div class="card-header clickable" onclick="toggleSettings()">
67
+ <h2>Settings</h2>
68
+ <span class="toggle-icon">▼</span>
69
  </div>
70
+ <div class="settings-content">
71
+ <div class="settings-grid">
72
+ <div class="form-group">
73
+ <label for="imageSize">Image Size</label>
74
+ <select id="imageSize">
75
+ <option value="custom" selected>Custom Size</option>
76
+ <option value="square_hd">Square HD (1024x1024)</option>
77
+ <option value="square">Square</option>
78
+ <option value="portrait_4_3">Portrait 4:3</option>
79
+ <option value="portrait_16_9">Portrait 16:9</option>
80
+ <option value="landscape_4_3">Landscape 4:3</option>
81
+ <option value="landscape_16_9">Landscape 16:9</option>
82
+ </select>
83
+ </div>
84
+
85
+ <div class="form-group custom-size">
86
+ <label>Custom Width</label>
87
+ <input type="number" id="customWidth" min="1024" max="4096" value="1280" />
88
+ </div>
89
+
90
+ <div class="form-group custom-size">
91
+ <label>Custom Height</label>
92
+ <input type="number" id="customHeight" min="1024" max="4096" value="1280" />
93
+ </div>
94
+
95
+ <div class="form-group">
96
+ <label for="numImages">Number of Generations</label>
97
+ <input type="number" id="numImages" min="1" max="10" value="1" />
98
+ </div>
99
+
100
+ <div class="form-group">
101
+ <label for="maxImages">Max Images per Generation</label>
102
+ <input type="number" id="maxImages" min="1" max="10" value="1" />
103
+ </div>
104
 
105
+ <div class="form-group">
106
+ <label for="seed">Seed (optional)</label>
107
+ <input type="number" id="seed" placeholder="Random" />
108
+ </div>
109
+
110
+ <!-- Safety checker is disabled by default and hidden from UI -->
111
+ <input type="hidden" id="safetyChecker" value="false" />
112
+ </div>
113
+ </div>
114
  </div>
115
+
116
+ <button id="generateBtn" class="generate-btn">
117
+ <span class="btn-text">Generate</span>
118
+ <div class="spinner" style="display: none;"></div>
119
+ </button>
120
+
121
+ <div id="statusMessage" class="status-message"></div>
122
+ <div id="progressLogs" class="progress-logs"></div>
123
  </div>
124
+ </div>
125
 
126
+ <!-- Right Panel: Results & History -->
127
+ <div class="right-panel">
128
+ <div class="history-header">
129
+ <h2>Generation History</h2>
130
+ <div class="history-controls">
131
+ <button class="history-btn" onclick="clearHistory()" title="Clear all history">
132
+ 🗑️ Clear History
133
+ </button>
134
+ <button class="history-btn" onclick="downloadAllHistory()" title="Download all images">
135
+ ⬇️ Download All
136
+ </button>
137
+ </div>
138
+ </div>
139
 
140
+ <div class="history-tabs">
141
+ <button class="tab-btn active" onclick="switchTab('current')">Current Generation</button>
142
+ <button class="tab-btn" onclick="switchTab('history')">History (<span id="historyCount">0</span>)</button>
143
+ </div>
144
 
145
+ <div id="currentTab" class="tab-content active">
146
+ <div id="currentResults" class="results-grid">
147
+ <div class="empty-state">
148
+ <p>No current generation</p>
149
+ <small>Generate an image to see results here</small>
150
+ </div>
151
+ </div>
152
+ <div id="currentInfo" class="generation-info"></div>
153
  </div>
 
154
 
155
+ <div id="historyTab" class="tab-content">
156
+ <div id="historyGrid" class="history-grid">
157
+ <div class="empty-state">
158
+ <p>No generation history</p>
159
+ <small>Your generated images will be saved here</small>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
  </div>
165
 
166
  <script src="/static/script.js"></script>