shukdevdatta123 commited on
Commit
5e321e2
·
verified ·
1 Parent(s): 81160e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +452 -25
app.py CHANGED
@@ -8,6 +8,7 @@ import os
8
  from together import Together
9
  import tempfile
10
  import uuid
 
11
 
12
  def encode_image_to_base64(image_path):
13
  """Convert image to base64 encoding"""
@@ -50,6 +51,189 @@ def analyze_single_image(client, img_path):
50
  except Exception as e:
51
  return f"Error analyzing image: {str(e)}"
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def get_recipe_suggestions(api_key, image_paths, num_recipes=3, dietary_restrictions="None", cuisine_preference="Any"):
54
  """Get recipe suggestions based on the uploaded images of ingredients"""
55
  if not api_key:
@@ -82,6 +266,7 @@ def get_recipe_suggestions(api_key, image_paths, num_recipes=3, dietary_restrict
82
  6. Difficulty level (Easy, Medium, Advanced)
83
  7. Nutritional highlights
84
 
 
85
  Consider any dietary restrictions and cuisine preferences mentioned by the user."""
86
 
87
  user_prompt = f"""Based on the following ingredients identified from multiple images, suggest {num_recipes} creative and delicious recipes.
@@ -99,19 +284,195 @@ def get_recipe_suggestions(api_key, image_paths, num_recipes=3, dietary_restrict
99
  {"role": "system", "content": system_prompt},
100
  {"role": "user", "content": user_prompt}
101
  ],
102
- max_tokens=20000, #2048
103
  temperature=0.7
104
  )
105
 
106
- result = "## 📋 Ingredients Identified\n\n"
107
- result += combined_ingredients
108
- result += "\n\n---\n\n"
109
- result += "## 🍽️ Recipe Suggestions\n\n"
110
- result += response.choices[0].message.content
111
 
112
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  except Exception as e:
114
- return f"Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  def update_gallery(files):
117
  """Update the gallery with uploaded image paths"""
@@ -122,7 +483,7 @@ def update_gallery(files):
122
  def process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference):
123
  """Process the recipe request with uploaded files"""
124
  if not files:
125
- return "Please upload at least one image of ingredients."
126
  return get_recipe_suggestions(api_key, files, num_recipes, dietary_restrictions, cuisine_preference)
127
 
128
  custom_css = """
@@ -219,6 +580,27 @@ button.primary-button:hover {
219
  background-color: #E15F52;
220
  box-shadow: var(--hover-shadow);
221
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  .gallery-container {
223
  display: grid;
224
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
@@ -245,15 +627,36 @@ button.primary-button:hover {
245
  overflow-y: auto;
246
  padding-right: 15px;
247
  }
248
- .recipe-output h2 {
249
- color: var(--primary-color);
250
- margin-top: 30px;
251
- font-size: 2em;
 
252
  }
253
- .recipe-output h3 {
 
 
 
 
 
 
 
254
  color: var(--secondary-color);
255
- font-size: 1.5em;
256
- margin-top: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  }
258
  .loading-container {
259
  display: flex;
@@ -358,22 +761,27 @@ html_footer = """
358
  if (submitBtn) {
359
  submitBtn.addEventListener('click', function() {
360
  showLoading();
361
- const output = document.querySelector('.recipe-output');
362
-
363
  // Check every second for output content
364
  const checkInterval = setInterval(function() {
365
- if (output && output.textContent.trim().length > 0) {
 
366
  hideLoading();
367
  clearInterval(checkInterval);
368
  clearTimeout(forceHideTimeout);
 
 
 
 
 
 
369
  }
370
- }, 60000);
371
-
372
  // Force hide after 120 seconds
373
  const forceHideTimeout = setTimeout(function() {
374
  hideLoading();
375
  clearInterval(checkInterval);
376
- }, 120000); // 120000 milliseconds = 120 seconds
377
  });
378
  }
379
  });
@@ -383,6 +791,9 @@ html_footer = """
383
  with gr.Blocks(css=custom_css) as app:
384
  gr.HTML(html_header)
385
 
 
 
 
386
  with gr.Row():
387
  with gr.Column(scale=1):
388
  with gr.Group(elem_classes="input-section"):
@@ -444,16 +855,32 @@ with gr.Blocks(css=custom_css) as app:
444
  with gr.Column(scale=1):
445
  with gr.Group(elem_classes="output-section"):
446
  gr.HTML('<h3 class="section-header">Your Personalized Recipes</h3>')
447
- output = gr.Markdown(elem_classes="recipe-output")
 
448
 
449
  gr.HTML(html_footer)
450
 
 
 
 
 
 
 
 
 
451
  file_upload.change(fn=update_gallery, inputs=file_upload, outputs=image_input)
452
 
453
  submit_button.click(
454
- fn=process_recipe_request,
455
  inputs=[api_key_input, file_upload, num_recipes, dietary_restrictions, cuisine_preference],
456
- outputs=output
 
 
 
 
 
 
 
457
  )
458
 
459
  if __name__ == "__main__":
 
8
  from together import Together
9
  import tempfile
10
  import uuid
11
+ import time
12
 
13
  def encode_image_to_base64(image_path):
14
  """Convert image to base64 encoding"""
 
51
  except Exception as e:
52
  return f"Error analyzing image: {str(e)}"
53
 
54
+ def format_ingredients_html(all_ingredients):
55
+ """Format the identified ingredients in HTML"""
56
+ html_content = """
57
+ <div class="ingredients-identified">
58
+ <h2>📋 Ingredients Identified</h2>
59
+ """
60
+
61
+ for i, ingredients in enumerate(all_ingredients):
62
+ html_content += f"""
63
+ <div class="ingredient-group">
64
+ <h3>Image {i+1} Ingredients</h3>
65
+ <ul class="ingredient-list">
66
+ """
67
+
68
+ # Split ingredients by new line and create list items
69
+ ingredient_lines = ingredients.strip().split('\n')
70
+ for line in ingredient_lines:
71
+ if line.strip():
72
+ html_content += f"<li>{line.strip()}</li>\n"
73
+
74
+ html_content += """
75
+ </ul>
76
+ </div>
77
+ """
78
+
79
+ html_content += "</div>"
80
+ return html_content
81
+
82
+ def format_recipe_to_html(recipe_text):
83
+ """Convert the recipe text to formatted HTML"""
84
+ # Initialize the HTML content with the recipe suggestions header
85
+ html_content = """
86
+ <div class="recipe-suggestions">
87
+ <h2>🍽️ Recipe Suggestions</h2>
88
+ """
89
+
90
+ # Split the text by recipe (assume recipes are separated by a recipe name heading)
91
+ recipe_sections = []
92
+ current_recipe = ""
93
+ lines = recipe_text.split('\n')
94
+
95
+ # Process lines to identify recipe sections
96
+ for line in lines:
97
+ if line.strip().startswith(("Recipe ", "# ", "## ", "### ")):
98
+ # If we've collected some content for a recipe, save it
99
+ if current_recipe:
100
+ recipe_sections.append(current_recipe)
101
+ current_recipe = ""
102
+
103
+ # Add line to current recipe
104
+ current_recipe += line + "\n"
105
+
106
+ # Add the last recipe if exists
107
+ if current_recipe:
108
+ recipe_sections.append(current_recipe)
109
+
110
+ # Process each recipe section into HTML
111
+ for recipe in recipe_sections:
112
+ if not recipe.strip():
113
+ continue
114
+
115
+ html_content += '<div class="recipe-card">'
116
+
117
+ lines = recipe.split('\n')
118
+ in_ingredients = False
119
+ in_instructions = False
120
+
121
+ for line in lines:
122
+ # Handle recipe title
123
+ if any(x in line.lower() for x in ["recipe ", "# recipe"]) or line.strip().startswith(("# ", "## ")):
124
+ title = line.replace("#", "").replace("Recipe:", "").replace("Recipe", "").strip()
125
+ html_content += f'<h3 class="recipe-title">{title}</h3>\n'
126
+ continue
127
+
128
+ # Handle description
129
+ if "description" in line.lower() and ":" in line:
130
+ description = line.split(":", 1)[1].strip()
131
+ html_content += f'<p class="recipe-description">{description}</p>\n'
132
+ continue
133
+
134
+ # Start ingredients section
135
+ if "ingredients" in line.lower() and not in_ingredients:
136
+ in_ingredients = True
137
+ in_instructions = False
138
+ html_content += '<div class="recipe-ingredients">\n'
139
+ html_content += '<h4>Ingredients</h4>\n<ul>\n'
140
+ continue
141
+
142
+ # Start instructions section
143
+ if any(x in line.lower() for x in ["instructions", "directions", "steps", "preparation"]) and not in_instructions:
144
+ if in_ingredients:
145
+ html_content += '</ul>\n</div>\n'
146
+ in_ingredients = False
147
+
148
+ in_instructions = True
149
+ html_content += '<div class="recipe-instructions">\n'
150
+ html_content += '<h4>Instructions</h4>\n<ol>\n'
151
+ continue
152
+
153
+ # Handle cooking time
154
+ if "cooking time" in line.lower() or "prep time" in line.lower() or "time" in line.lower():
155
+ if in_ingredients:
156
+ html_content += '</ul>\n</div>\n'
157
+ in_ingredients = False
158
+ if in_instructions:
159
+ html_content += '</ol>\n</div>\n'
160
+ in_instructions = False
161
+
162
+ time_info = line.strip()
163
+ html_content += f'<p class="recipe-time"><strong>⏱️ {time_info}</strong></p>\n'
164
+ continue
165
+
166
+ # Handle difficulty level
167
+ if "difficulty" in line.lower():
168
+ difficulty = line.strip()
169
+ html_content += f'<p class="recipe-difficulty"><strong>🔍 {difficulty}</strong></p>\n'
170
+ continue
171
+
172
+ # Handle nutritional info
173
+ if "nutritional" in line.lower():
174
+ if in_ingredients:
175
+ html_content += '</ul>\n</div>\n'
176
+ in_ingredients = False
177
+ if in_instructions:
178
+ html_content += '</ol>\n</div>\n'
179
+ in_instructions = False
180
+
181
+ html_content += '<div class="recipe-nutrition">\n'
182
+ html_content += f'<h4>{line.strip()}</h4>\n<ul>\n'
183
+ continue
184
+
185
+ # Process ingredient line
186
+ if in_ingredients and line.strip() and not line.lower().startswith(("ingredients", "instructions")):
187
+ item = line.strip()
188
+ if item.startswith("- "):
189
+ item = item[2:]
190
+ elif item.startswith("* "):
191
+ item = item[2:]
192
+
193
+ if item:
194
+ html_content += f'<li>{item}</li>\n'
195
+ continue
196
+
197
+ # Process instruction line
198
+ if in_instructions and line.strip() and not line.lower().startswith(("ingredients", "instructions")):
199
+ step = line.strip()
200
+ if step.startswith("- "):
201
+ step = step[2:]
202
+ elif step.startswith("* "):
203
+ step = step[2:]
204
+ elif step.startswith(". "):
205
+ step = step[2:]
206
+ elif step[0].isdigit() and step[1] in [".", ")"]:
207
+ step = step[2:].strip()
208
+
209
+ if step:
210
+ html_content += f'<li>{step}</li>\n'
211
+ continue
212
+
213
+ # Process nutrition line
214
+ if "nutritional" in line.lower() and line.strip() and not line.startswith(("ingredients", "instructions")):
215
+ item = line.strip()
216
+ if item.startswith("- "):
217
+ item = item[2:]
218
+ elif item.startswith("* "):
219
+ item = item[2:]
220
+
221
+ if item and not item.lower().startswith("nutritional"):
222
+ html_content += f'<li>{item}</li>\n'
223
+ continue
224
+
225
+ # Close any open sections
226
+ if in_ingredients:
227
+ html_content += '</ul>\n</div>\n'
228
+ if in_instructions:
229
+ html_content += '</ol>\n</div>\n'
230
+
231
+ html_content += '</div>\n' # Close recipe card
232
+
233
+ html_content += '</div>\n' # Close recipe suggestions div
234
+
235
+ return html_content
236
+
237
  def get_recipe_suggestions(api_key, image_paths, num_recipes=3, dietary_restrictions="None", cuisine_preference="Any"):
238
  """Get recipe suggestions based on the uploaded images of ingredients"""
239
  if not api_key:
 
266
  6. Difficulty level (Easy, Medium, Advanced)
267
  7. Nutritional highlights
268
 
269
+ Format each recipe clearly with headings for each section. Make sure to separate recipes clearly.
270
  Consider any dietary restrictions and cuisine preferences mentioned by the user."""
271
 
272
  user_prompt = f"""Based on the following ingredients identified from multiple images, suggest {num_recipes} creative and delicious recipes.
 
284
  {"role": "system", "content": system_prompt},
285
  {"role": "user", "content": user_prompt}
286
  ],
287
+ max_tokens=2048,
288
  temperature=0.7
289
  )
290
 
291
+ recipe_text = response.choices[0].message.content
 
 
 
 
292
 
293
+ # Format ingredients and recipes as HTML
294
+ ingredients_html = format_ingredients_html(all_ingredients)
295
+ recipes_html = format_recipe_to_html(recipe_text)
296
+
297
+ # Combine HTML content
298
+ html_content = ingredients_html + "<hr>" + recipes_html
299
+
300
+ # Create a downloadable HTML file with styling
301
+ full_html = create_downloadable_html(ingredients_html, recipes_html, dietary_restrictions, cuisine_preference)
302
+
303
+ # Generate a unique filename for the downloadable HTML
304
+ timestamp = int(time.time())
305
+ file_name = f"recipes_{timestamp}.html"
306
+ file_path = os.path.join(tempfile.gettempdir(), file_name)
307
+
308
+ with open(file_path, "w", encoding="utf-8") as f:
309
+ f.write(full_html)
310
+
311
+ return [html_content, file_path]
312
  except Exception as e:
313
+ return [f"Error: {str(e)}", None]
314
+
315
+ def create_downloadable_html(ingredients_html, recipes_html, dietary_restrictions, cuisine_preference):
316
+ """Create a complete HTML document with styling for download"""
317
+ html = f"""<!DOCTYPE html>
318
+ <html lang="en">
319
+ <head>
320
+ <meta charset="UTF-8">
321
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
322
+ <title>Your Personalized Recipes</title>
323
+ <style>
324
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
325
+ :root {{
326
+ --primary-color: #FF6F61;
327
+ --secondary-color: #4BB543;
328
+ --accent-color: #F0A500;
329
+ --background-color: #F4F4F9;
330
+ --text-color: #333333;
331
+ --card-background: #FFFFFF;
332
+ --border-radius: 12px;
333
+ --box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 20px;
334
+ }}
335
+ body {{
336
+ font-family: 'Poppins', sans-serif;
337
+ background-color: var(--background-color);
338
+ color: var(--text-color);
339
+ margin: 0;
340
+ padding: 0;
341
+ line-height: 1.6;
342
+ }}
343
+ .container {{
344
+ max-width: 1000px;
345
+ margin: 0 auto;
346
+ padding: 20px;
347
+ }}
348
+ header {{
349
+ background-color: var(--primary-color);
350
+ color: white;
351
+ padding: 40px 20px;
352
+ text-align: center;
353
+ border-radius: 0 0 20px 20px;
354
+ margin-bottom: 30px;
355
+ }}
356
+ h1 {{
357
+ font-size: 2.5em;
358
+ margin-bottom: 10px;
359
+ }}
360
+ .recipe-info {{
361
+ display: flex;
362
+ justify-content: center;
363
+ gap: 20px;
364
+ margin-bottom: 20px;
365
+ flex-wrap: wrap;
366
+ }}
367
+ .info-badge {{
368
+ background-color: rgba(255, 255, 255, 0.2);
369
+ padding: 8px 16px;
370
+ border-radius: 20px;
371
+ font-size: 0.9em;
372
+ }}
373
+ .ingredients-identified, .recipe-suggestions {{
374
+ background-color: var(--card-background);
375
+ border-radius: var(--border-radius);
376
+ padding: 25px;
377
+ margin-bottom: 30px;
378
+ box-shadow: var(--box-shadow);
379
+ }}
380
+ h2 {{
381
+ color: var(--primary-color);
382
+ border-bottom: 2px solid var(--primary-color);
383
+ padding-bottom: 10px;
384
+ margin-top: 0;
385
+ }}
386
+ .ingredient-group {{
387
+ margin-bottom: 20px;
388
+ }}
389
+ h3 {{
390
+ color: var(--accent-color);
391
+ margin-bottom: 10px;
392
+ }}
393
+ .ingredient-list {{
394
+ list-style-type: disc;
395
+ padding-left: 20px;
396
+ }}
397
+ .recipe-card {{
398
+ background-color: #f9f9f9;
399
+ border-radius: var(--border-radius);
400
+ padding: 20px;
401
+ margin-bottom: 30px;
402
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
403
+ }}
404
+ .recipe-title {{
405
+ color: var(--secondary-color);
406
+ font-size: 1.8em;
407
+ margin-top: 0;
408
+ margin-bottom: 15px;
409
+ }}
410
+ .recipe-description {{
411
+ font-style: italic;
412
+ margin-bottom: 20px;
413
+ color: #666;
414
+ }}
415
+ .recipe-ingredients, .recipe-instructions, .recipe-nutrition {{
416
+ margin-bottom: 20px;
417
+ }}
418
+ .recipe-ingredients h4, .recipe-instructions h4, .recipe-nutrition h4 {{
419
+ color: var(--primary-color);
420
+ margin-bottom: 10px;
421
+ }}
422
+ .recipe-ingredients ul {{
423
+ list-style-type: disc;
424
+ }}
425
+ .recipe-instructions ol {{
426
+ padding-left: 20px;
427
+ }}
428
+ .recipe-instructions li {{
429
+ margin-bottom: 10px;
430
+ }}
431
+ .recipe-time, .recipe-difficulty {{
432
+ color: #666;
433
+ margin: 10px 0;
434
+ }}
435
+ hr {{
436
+ border: 0;
437
+ height: 1px;
438
+ background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0));
439
+ margin: 30px 0;
440
+ }}
441
+ footer {{
442
+ text-align: center;
443
+ margin-top: 50px;
444
+ padding: 20px;
445
+ color: #666;
446
+ font-size: 0.9em;
447
+ }}
448
+ </style>
449
+ </head>
450
+ <body>
451
+ <header>
452
+ <h1>🍲 Your Personalized Recipes</h1>
453
+ <div class="recipe-info">
454
+ <span class="info-badge">Dietary: {dietary_restrictions}</span>
455
+ <span class="info-badge">Cuisine: {cuisine_preference}</span>
456
+ <span class="info-badge">Generated: {time.strftime("%Y-%m-%d")}</span>
457
+ </div>
458
+ </header>
459
+
460
+ <div class="container">
461
+ {ingredients_html}
462
+
463
+ <hr>
464
+
465
+ {recipes_html}
466
+
467
+ <footer>
468
+ <p>Generated by Visual Recipe Assistant</p>
469
+ <p>Powered by Meta's Llama-Vision-Free Model & Together AI</p>
470
+ </footer>
471
+ </div>
472
+ </body>
473
+ </html>
474
+ """
475
+ return html
476
 
477
  def update_gallery(files):
478
  """Update the gallery with uploaded image paths"""
 
483
  def process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference):
484
  """Process the recipe request with uploaded files"""
485
  if not files:
486
+ return ["Please upload at least one image of ingredients.", None]
487
  return get_recipe_suggestions(api_key, files, num_recipes, dietary_restrictions, cuisine_preference)
488
 
489
  custom_css = """
 
580
  background-color: #E15F52;
581
  box-shadow: var(--hover-shadow);
582
  }
583
+ button.download-button {
584
+ background-color: var(--secondary-color);
585
+ color: white;
586
+ border: none;
587
+ padding: 12px 24px;
588
+ border-radius: 6px;
589
+ font-size: 1em;
590
+ cursor: pointer;
591
+ transition: all 0.3s ease;
592
+ margin-top: 15px;
593
+ display: flex;
594
+ align-items: center;
595
+ justify-content: center;
596
+ gap: 8px;
597
+ margin-left: auto;
598
+ margin-right: auto;
599
+ }
600
+ button.download-button:hover {
601
+ background-color: #3da037;
602
+ box-shadow: var(--hover-shadow);
603
+ }
604
  .gallery-container {
605
  display: grid;
606
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
 
627
  overflow-y: auto;
628
  padding-right: 15px;
629
  }
630
+ .ingredients-identified, .recipe-suggestions {
631
+ background-color: #f9f9f9;
632
+ border-radius: var(--border-radius);
633
+ padding: 20px;
634
+ margin-bottom: 20px;
635
  }
636
+ .recipe-card {
637
+ background-color: white;
638
+ border-radius: var(--border-radius);
639
+ padding: 20px;
640
+ margin-bottom: 20px;
641
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
642
+ }
643
+ .recipe-title {
644
  color: var(--secondary-color);
645
+ font-size: 1.8em;
646
+ margin-top: 0;
647
+ margin-bottom: 15px;
648
+ }
649
+ .recipe-description {
650
+ font-style: italic;
651
+ margin-bottom: 20px;
652
+ color: #666;
653
+ }
654
+ .recipe-ingredients, .recipe-instructions, .recipe-nutrition {
655
+ margin-bottom: 20px;
656
+ }
657
+ .recipe-ingredients h4, .recipe-instructions h4, .recipe-nutrition h4 {
658
+ color: var(--primary-color);
659
+ margin-bottom: 10px;
660
  }
661
  .loading-container {
662
  display: flex;
 
761
  if (submitBtn) {
762
  submitBtn.addEventListener('click', function() {
763
  showLoading();
 
 
764
  // Check every second for output content
765
  const checkInterval = setInterval(function() {
766
+ const output = document.querySelector('.recipe-output');
767
+ if (output && output.innerHTML.trim().length > 0) {
768
  hideLoading();
769
  clearInterval(checkInterval);
770
  clearTimeout(forceHideTimeout);
771
+
772
+ // Show download button if it exists
773
+ const downloadBtn = document.getElementById('download-button');
774
+ if (downloadBtn) {
775
+ downloadBtn.style.display = 'flex';
776
+ }
777
  }
778
+ }, 1000);
779
+
780
  // Force hide after 120 seconds
781
  const forceHideTimeout = setTimeout(function() {
782
  hideLoading();
783
  clearInterval(checkInterval);
784
+ }, 120000);
785
  });
786
  }
787
  });
 
791
  with gr.Blocks(css=custom_css) as app:
792
  gr.HTML(html_header)
793
 
794
+ # Store the generated html file path for download
795
+ html_file_path = gr.State(None)
796
+
797
  with gr.Row():
798
  with gr.Column(scale=1):
799
  with gr.Group(elem_classes="input-section"):
 
855
  with gr.Column(scale=1):
856
  with gr.Group(elem_classes="output-section"):
857
  gr.HTML('<h3 class="section-header">Your Personalized Recipes</h3>')
858
+ output = gr.HTML(elem_classes="recipe-output")
859
+ download_button = gr.Button("📥 Download Recipes as HTML", elem_classes="download-button", visible=False, elem_id="download-button")
860
 
861
  gr.HTML(html_footer)
862
 
863
+ # Update functions
864
+ def process_and_update_file_path(api_key, files, num_recipes, dietary_restrictions, cuisine_preference):
865
+ """Process recipe request and update file path state"""
866
+ result = process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference)
867
+ html_content = result[0]
868
+ file_path = result[1]
869
+ return html_content, file_path, gr.update(visible=file_path is not None)
870
+
871
  file_upload.change(fn=update_gallery, inputs=file_upload, outputs=image_input)
872
 
873
  submit_button.click(
874
+ fn=process_and_update_file_path,
875
  inputs=[api_key_input, file_upload, num_recipes, dietary_restrictions, cuisine_preference],
876
+ outputs=[output, html_file_path, download_button]
877
+ )
878
+
879
+ # Handle download button click
880
+ download_button.click(
881
+ fn=lambda x: x,
882
+ inputs=html_file_path,
883
+ outputs=gr.File(label="Download Recipe HTML")
884
  )
885
 
886
  if __name__ == "__main__":