shukdevdatta123 commited on
Commit
3a148b5
Β·
verified Β·
1 Parent(s): ca8ad1c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +473 -117
app.py CHANGED
@@ -44,35 +44,40 @@ def analyze_single_image(client, img_path):
44
 
45
  user_prompt = "Please identify all the food ingredients visible in this image. List each ingredient on a new line."
46
 
47
- # Create message with the image
48
- content = [
49
- {"type": "text", "text": user_prompt},
50
- {
51
- "type": "image_url",
52
- "image_url": {
53
- "url": f"file://{img_path}"
54
- }
55
- }
56
- ]
57
-
58
  try:
59
- response = client.chat.completions.create(
60
- model="meta-llama/Llama-Vision-Free",
61
- messages=[
 
 
 
 
62
  {
63
- "role": "system",
64
- "content": system_prompt
65
- },
66
- {
67
- "role": "user",
68
- "content": content
69
  }
70
- ],
71
- max_tokens=500,
72
- temperature=0.2
73
- )
74
-
75
- return response.choices[0].message.content
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  except Exception as e:
77
  return f"Error analyzing image: {str(e)}"
78
 
@@ -183,16 +188,20 @@ def get_recipe_suggestions(api_key, images, num_recipes=3, dietary_restrictions=
183
  pass
184
  return f"Error: {str(e)}"
185
 
186
- # Custom CSS for a more appealing interface
187
  custom_css = """
 
 
188
  :root {
189
  --primary-color: #FF6B6B;
 
190
  --secondary-color: #4ECDC4;
191
  --accent-color: #FFD166;
192
  --background-color: #f8f9fa;
193
  --text-color: #212529;
194
- --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
195
- --border-radius: 10px;
 
196
  --font-family: 'Poppins', sans-serif;
197
  }
198
 
@@ -200,6 +209,8 @@ body {
200
  font-family: var(--font-family);
201
  background-color: var(--background-color);
202
  color: var(--text-color);
 
 
203
  }
204
 
205
  .container {
@@ -210,129 +221,248 @@ body {
210
 
211
  .app-header {
212
  text-align: center;
213
- margin-bottom: 30px;
214
- padding: 30px 0;
215
  background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
216
  border-radius: var(--border-radius);
217
  color: white;
218
  box-shadow: var(--card-shadow);
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  }
220
 
221
  .app-title {
222
- font-size: 3em;
223
- margin-bottom: 10px;
224
- font-weight: bold;
 
 
225
  }
226
 
227
  .app-subtitle {
228
- font-size: 1.2em;
229
  opacity: 0.9;
230
  max-width: 700px;
231
  margin: 0 auto;
 
 
 
232
  }
233
 
234
  .input-section, .output-section {
235
  background-color: white;
236
  border-radius: var(--border-radius);
237
- padding: 25px;
238
  box-shadow: var(--card-shadow);
239
- margin-bottom: 20px;
 
 
 
 
 
 
 
240
  }
241
 
242
- .input-section h3, .output-section h3 {
243
  color: var(--primary-color);
244
  margin-top: 0;
245
- font-size: 1.5em;
246
  border-bottom: 2px solid var(--secondary-color);
247
- padding-bottom: 10px;
248
- margin-bottom: 20px;
 
 
 
 
 
 
 
 
249
  }
250
 
251
  .image-upload-container {
252
- border: 2px dashed var(--secondary-color);
253
  border-radius: var(--border-radius);
254
- padding: 20px;
255
  text-align: center;
256
- margin-bottom: 20px;
257
  transition: all 0.3s ease;
 
 
258
  }
259
 
260
  .image-upload-container:hover {
261
  border-color: var(--primary-color);
262
  background-color: rgba(255, 107, 107, 0.05);
 
 
 
 
 
 
 
 
 
 
 
 
263
  }
264
 
265
  button.primary-button {
266
  background: linear-gradient(135deg, var(--primary-color) 0%, #FF8E8E 100%);
267
  color: white;
268
  border: none;
269
- padding: 12px 25px;
270
- border-radius: 30px;
271
- font-size: 1.1em;
272
  cursor: pointer;
273
  transition: all 0.3s ease;
274
- box-shadow: 0 2px 5px rgba(255, 107, 107, 0.3);
275
- font-weight: bold;
276
  display: block;
277
  width: 100%;
278
- margin-top: 20px;
 
 
279
  }
280
 
281
  button.primary-button:hover {
282
- transform: translateY(-2px);
283
- box-shadow: 0 4px 8px rgba(255, 107, 107, 0.4);
284
- background: linear-gradient(135deg, #FF8E8E 0%, var(--primary-color) 100%);
285
  }
286
 
287
- .gradio-slider.svelte-17l1npl {
288
- margin-bottom: 20px;
 
289
  }
290
 
291
- .tab-nav {
292
- background-color: var(--secondary-color);
293
- border-radius: var(--border-radius) var(--border-radius) 0 0;
 
 
 
 
 
 
 
 
 
294
  }
295
 
296
- .tab-nav button {
297
- color: white;
298
- font-weight: bold;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  }
300
 
301
  .recipe-card {
302
  border-left: 5px solid var(--accent-color);
303
- padding: 15px;
304
  background-color: #f9f9f9;
305
- margin-bottom: 15px;
306
  border-radius: 0 var(--border-radius) var(--border-radius) 0;
 
 
 
 
 
 
307
  }
308
 
309
  .recipe-title {
310
  color: var(--primary-color);
311
- font-size: 1.3em;
312
- margin-bottom: 5px;
 
313
  }
314
 
315
  .footer {
316
  text-align: center;
317
- margin-top: 40px;
 
318
  color: #6c757d;
319
- font-size: 0.9em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  }
321
 
322
  .icon {
323
  color: var(--primary-color);
324
- margin-right: 5px;
 
325
  }
326
 
327
  .input-group {
328
- margin-bottom: 20px;
329
  }
330
 
331
  .input-group label {
332
  display: block;
333
- margin-bottom: 8px;
334
  font-weight: 600;
335
  color: var(--text-color);
 
 
 
 
 
 
 
 
336
  }
337
 
338
  .gallery-item {
@@ -340,74 +470,241 @@ button.primary-button:hover {
340
  overflow: hidden;
341
  box-shadow: var(--card-shadow);
342
  transition: transform 0.3s ease;
 
 
343
  }
344
 
345
  .gallery-item:hover {
346
- transform: scale(1.02);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  }
348
 
349
  .loading-spinner {
350
- text-align: center;
351
- padding: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  }
353
 
354
  .recipe-output {
355
  max-height: 800px;
356
  overflow-y: auto;
357
- padding-right: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  }
359
 
360
  .recipe-output h2 {
361
  color: var(--primary-color);
362
- border-bottom: 1px solid var(--secondary-color);
363
- padding-bottom: 5px;
 
 
364
  }
365
 
366
  .recipe-output h3 {
367
  color: var(--secondary-color);
 
 
368
  }
369
 
370
- /* Responsive styles */
371
- @media (max-width: 768px) {
372
- .app-title {
373
- font-size: 2em;
374
- }
375
-
376
- .input-section, .output-section {
377
- padding: 15px;
378
- }
379
- }
380
-
381
- /* Custom styling for the API key input */
382
- input[type="password"] {
383
  border: 2px solid #e9ecef;
384
  border-radius: var(--border-radius);
385
- padding: 10px 15px;
386
  font-size: 1em;
387
  width: 100%;
388
- transition: border-color 0.3s ease;
 
389
  }
390
 
391
- input[type="password"]:focus {
392
  border-color: var(--secondary-color);
393
  outline: none;
 
394
  }
395
 
396
  /* Custom dropdown styling */
397
  select {
398
  appearance: none;
399
- background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FF6B6B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E") no-repeat right 10px center;
400
  border: 2px solid #e9ecef;
401
  border-radius: var(--border-radius);
402
- padding: 10px 40px 10px 15px;
403
  font-size: 1em;
404
  width: 100%;
405
- transition: border-color 0.3s ease;
 
406
  }
407
 
408
  select:focus {
409
  border-color: var(--secondary-color);
410
  outline: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  }
412
 
413
  /* Remove Gradio branding */
@@ -415,25 +712,84 @@ select:focus {
415
  max-width: 100% !important;
416
  }
417
 
418
- .footer-logo, .footer-links {
419
  display: none !important;
420
  }
421
  """
422
 
423
- # Custom HTML header
424
  html_header = """
425
  <div class="app-header">
426
  <div class="app-title">🍲 Visual Recipe Assistant</div>
427
  <div class="app-subtitle">Upload images of ingredients you have on hand and get personalized recipe suggestions powered by AI</div>
428
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  """
430
 
431
- # Custom HTML footer
432
  html_footer = """
433
  <div class="footer">
434
- <p>πŸ§ͺ Powered by Meta's Llama-Vision-Free Model & Together AI</p>
435
- <p>πŸ“Έ Upload multiple ingredient images for more creative recipe combinations</p>
 
 
 
 
 
 
 
 
436
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  """
438
 
439
  # Create the Gradio interface with improved design
@@ -443,25 +799,16 @@ with gr.Blocks(css=custom_css) as app:
443
  with gr.Row():
444
  with gr.Column(scale=1):
445
  with gr.Group(elem_classes="input-section"):
446
- gr.HTML("<h3>πŸ”‘ API Configuration</h3>")
447
  api_key_input = gr.Textbox(
448
  label="Together API Key",
449
  placeholder="Enter your Together API key here...",
450
  type="password",
451
- elem_classes="input-group"
452
- )
453
-
454
- gr.HTML("<h3>πŸ“· Upload Ingredients</h3>")
455
- image_input = gr.Gallery(
456
- label="",
457
- elem_id="ingredient-gallery",
458
- elem_classes="gallery-container",
459
- columns=3,
460
- rows=2,
461
- height="auto",
462
- object_fit="contain"
463
  )
464
 
 
465
  # Use File component to handle multiple image uploads
466
  file_upload = gr.File(
467
  label="Upload images of ingredients",
@@ -470,7 +817,16 @@ with gr.Blocks(css=custom_css) as app:
470
  elem_classes="image-upload-container"
471
  )
472
 
473
- gr.HTML("<h3>βš™οΈ Recipe Preferences</h3>")
 
 
 
 
 
 
 
 
 
474
  with gr.Row():
475
  num_recipes = gr.Slider(
476
  minimum=1,
@@ -502,7 +858,7 @@ with gr.Blocks(css=custom_css) as app:
502
 
503
  with gr.Column(scale=1):
504
  with gr.Group(elem_classes="output-section"):
505
- gr.HTML("<h3>🍽️ Your Personalized Recipes</h3>")
506
  output = gr.Markdown(elem_classes="recipe-output")
507
 
508
  gr.HTML(html_footer)
@@ -510,8 +866,8 @@ with gr.Blocks(css=custom_css) as app:
510
  # Handle file uploads to display in gallery
511
  def update_gallery(files):
512
  if not files:
513
- return None
514
- return [file.name for file in files]
515
 
516
  file_upload.change(fn=update_gallery, inputs=file_upload, outputs=image_input)
517
 
 
44
 
45
  user_prompt = "Please identify all the food ingredients visible in this image. List each ingredient on a new line."
46
 
47
+ # First, convert the image to base64
 
 
 
 
 
 
 
 
 
 
48
  try:
49
+ with open(img_path, "rb") as image_file:
50
+ # Read the binary data and encode as base64
51
+ base64_image = base64.b64encode(image_file.read()).decode('utf-8')
52
+
53
+ # Create message with the image properly formatted
54
+ content = [
55
+ {"type": "text", "text": user_prompt},
56
  {
57
+ "type": "image_url",
58
+ "image_url": {
59
+ "url": f"data:image/jpeg;base64,{base64_image}"
60
+ }
 
 
61
  }
62
+ ]
63
+
64
+ response = client.chat.completions.create(
65
+ model="meta-llama/Llama-Vision-Free",
66
+ messages=[
67
+ {
68
+ "role": "system",
69
+ "content": system_prompt
70
+ },
71
+ {
72
+ "role": "user",
73
+ "content": content
74
+ }
75
+ ],
76
+ max_tokens=500,
77
+ temperature=0.2
78
+ )
79
+
80
+ return response.choices[0].message.content
81
  except Exception as e:
82
  return f"Error analyzing image: {str(e)}"
83
 
 
188
  pass
189
  return f"Error: {str(e)}"
190
 
191
+ # Enhanced Custom CSS for a more appealing interface
192
  custom_css = """
193
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
194
+
195
  :root {
196
  --primary-color: #FF6B6B;
197
+ --primary-dark: #e55858;
198
  --secondary-color: #4ECDC4;
199
  --accent-color: #FFD166;
200
  --background-color: #f8f9fa;
201
  --text-color: #212529;
202
+ --card-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
203
+ --hover-shadow: 0 12px 28px rgba(0, 0, 0, 0.15);
204
+ --border-radius: 12px;
205
  --font-family: 'Poppins', sans-serif;
206
  }
207
 
 
209
  font-family: var(--font-family);
210
  background-color: var(--background-color);
211
  color: var(--text-color);
212
+ margin: 0;
213
+ padding: 0;
214
  }
215
 
216
  .container {
 
221
 
222
  .app-header {
223
  text-align: center;
224
+ margin-bottom: 40px;
225
+ padding: 50px 20px;
226
  background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
227
  border-radius: var(--border-radius);
228
  color: white;
229
  box-shadow: var(--card-shadow);
230
+ position: relative;
231
+ overflow: hidden;
232
+ }
233
+
234
+ .app-header::before {
235
+ content: '';
236
+ position: absolute;
237
+ top: 0;
238
+ left: 0;
239
+ right: 0;
240
+ bottom: 0;
241
+ background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path fill="rgba(255,255,255,0.05)" d="M30,20 L70,20 L50,50 Z"></path></svg>') repeat;
242
+ opacity: 0.3;
243
  }
244
 
245
  .app-title {
246
+ font-size: 3.5em;
247
+ margin-bottom: 15px;
248
+ font-weight: 700;
249
+ text-shadow: 0 2px 4px rgba(0,0,0,0.1);
250
+ position: relative;
251
  }
252
 
253
  .app-subtitle {
254
+ font-size: 1.3em;
255
  opacity: 0.9;
256
  max-width: 700px;
257
  margin: 0 auto;
258
+ line-height: 1.6;
259
+ font-weight: 300;
260
+ position: relative;
261
  }
262
 
263
  .input-section, .output-section {
264
  background-color: white;
265
  border-radius: var(--border-radius);
266
+ padding: 30px;
267
  box-shadow: var(--card-shadow);
268
+ margin-bottom: 30px;
269
+ transition: all 0.3s ease;
270
+ border: 1px solid rgba(0,0,0,0.05);
271
+ }
272
+
273
+ .input-section:hover, .output-section:hover {
274
+ box-shadow: var(--hover-shadow);
275
+ transform: translateY(-5px);
276
  }
277
 
278
+ .section-header {
279
  color: var(--primary-color);
280
  margin-top: 0;
281
+ font-size: 1.7em;
282
  border-bottom: 2px solid var(--secondary-color);
283
+ padding-bottom: 15px;
284
+ margin-bottom: 25px;
285
+ font-weight: 600;
286
+ display: flex;
287
+ align-items: center;
288
+ }
289
+
290
+ .section-header i {
291
+ margin-right: 10px;
292
+ font-size: 1.2em;
293
  }
294
 
295
  .image-upload-container {
296
+ border: 3px dashed var(--secondary-color);
297
  border-radius: var(--border-radius);
298
+ padding: 30px;
299
  text-align: center;
300
+ margin-bottom: 25px;
301
  transition: all 0.3s ease;
302
+ background-color: rgba(78, 205, 196, 0.05);
303
+ position: relative;
304
  }
305
 
306
  .image-upload-container:hover {
307
  border-color: var(--primary-color);
308
  background-color: rgba(255, 107, 107, 0.05);
309
+ transform: translateY(-3px);
310
+ }
311
+
312
+ .image-upload-icon {
313
+ font-size: 3em;
314
+ color: var(--secondary-color);
315
+ margin-bottom: 15px;
316
+ }
317
+
318
+ .image-upload-text {
319
+ color: #666;
320
+ font-weight: 500;
321
  }
322
 
323
  button.primary-button {
324
  background: linear-gradient(135deg, var(--primary-color) 0%, #FF8E8E 100%);
325
  color: white;
326
  border: none;
327
+ padding: 15px 30px;
328
+ border-radius: 50px;
329
+ font-size: 1.2em;
330
  cursor: pointer;
331
  transition: all 0.3s ease;
332
+ box-shadow: 0 4px 10px rgba(255, 107, 107, 0.3);
333
+ font-weight: 600;
334
  display: block;
335
  width: 100%;
336
+ margin-top: 30px;
337
+ position: relative;
338
+ overflow: hidden;
339
  }
340
 
341
  button.primary-button:hover {
342
+ transform: translateY(-3px);
343
+ box-shadow: 0 8px 15px rgba(255, 107, 107, 0.4);
344
+ background: linear-gradient(135deg, #FF8E8E 0%, var(--primary-dark) 100%);
345
  }
346
 
347
+ button.primary-button:active {
348
+ transform: translateY(1px);
349
+ box-shadow: 0 2px 5px rgba(255, 107, 107, 0.4);
350
  }
351
 
352
+ button.primary-button::after {
353
+ content: '';
354
+ position: absolute;
355
+ top: 50%;
356
+ left: 50%;
357
+ width: 5px;
358
+ height: 5px;
359
+ background: rgba(255, 255, 255, 0.5);
360
+ opacity: 0;
361
+ border-radius: 100%;
362
+ transform: scale(1, 1) translate(-50%, -50%);
363
+ transform-origin: 50% 50%;
364
  }
365
 
366
+ button.primary-button:focus:not(:active)::after {
367
+ animation: ripple 1s ease-out;
368
+ }
369
+
370
+ @keyframes ripple {
371
+ 0% {
372
+ transform: scale(0, 0);
373
+ opacity: 0.5;
374
+ }
375
+ 100% {
376
+ transform: scale(100, 100);
377
+ opacity: 0;
378
+ }
379
+ }
380
+
381
+ .gradio-slider.svelte-17l1npl {
382
+ margin-bottom: 25px;
383
  }
384
 
385
  .recipe-card {
386
  border-left: 5px solid var(--accent-color);
387
+ padding: 20px;
388
  background-color: #f9f9f9;
389
+ margin-bottom: 20px;
390
  border-radius: 0 var(--border-radius) var(--border-radius) 0;
391
+ transition: all 0.3s ease;
392
+ }
393
+
394
+ .recipe-card:hover {
395
+ transform: translateX(5px);
396
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
397
  }
398
 
399
  .recipe-title {
400
  color: var(--primary-color);
401
+ font-size: 1.5em;
402
+ margin-bottom: 10px;
403
+ font-weight: 600;
404
  }
405
 
406
  .footer {
407
  text-align: center;
408
+ margin-top: 60px;
409
+ padding: 30px 0;
410
  color: #6c757d;
411
+ font-size: 1em;
412
+ background-color: #f1f3f5;
413
+ border-radius: var(--border-radius);
414
+ box-shadow: inset 0 2px 10px rgba(0,0,0,0.05);
415
+ }
416
+
417
+ .footer-content {
418
+ max-width: 700px;
419
+ margin: 0 auto;
420
+ }
421
+
422
+ .footer-brand {
423
+ font-weight: 600;
424
+ color: var(--primary-color);
425
+ }
426
+
427
+ .footer-links {
428
+ margin-top: 20px;
429
+ }
430
+
431
+ .footer-links a {
432
+ color: var(--secondary-color);
433
+ margin: 0 10px;
434
+ text-decoration: none;
435
+ transition: color 0.3s ease;
436
+ }
437
+
438
+ .footer-links a:hover {
439
+ color: var(--primary-color);
440
+ text-decoration: underline;
441
  }
442
 
443
  .icon {
444
  color: var(--primary-color);
445
+ margin-right: 10px;
446
+ font-size: 1.2em;
447
  }
448
 
449
  .input-group {
450
+ margin-bottom: 25px;
451
  }
452
 
453
  .input-group label {
454
  display: block;
455
+ margin-bottom: 10px;
456
  font-weight: 600;
457
  color: var(--text-color);
458
+ font-size: 1.1em;
459
+ }
460
+
461
+ .gallery-container {
462
+ display: grid;
463
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
464
+ gap: 20px;
465
+ margin-top: 20px;
466
  }
467
 
468
  .gallery-item {
 
470
  overflow: hidden;
471
  box-shadow: var(--card-shadow);
472
  transition: transform 0.3s ease;
473
+ aspect-ratio: 1 / 1;
474
+ object-fit: cover;
475
  }
476
 
477
  .gallery-item:hover {
478
+ transform: scale(1.05);
479
+ box-shadow: var(--hover-shadow);
480
+ }
481
+
482
+ /* Loading Spinner */
483
+ .loading-container {
484
+ position: fixed;
485
+ top: 0;
486
+ left: 0;
487
+ width: 100%;
488
+ height: 100%;
489
+ background-color: rgba(0, 0, 0, 0.7);
490
+ display: flex;
491
+ justify-content: center;
492
+ align-items: center;
493
+ z-index: 1000;
494
+ opacity: 0;
495
+ visibility: hidden;
496
+ transition: opacity 0.3s ease, visibility 0.3s ease;
497
+ }
498
+
499
+ .loading-container.visible {
500
+ opacity: 1;
501
+ visibility: visible;
502
  }
503
 
504
  .loading-spinner {
505
+ width: 80px;
506
+ height: 80px;
507
+ border-radius: 50%;
508
+ position: relative;
509
+ animation: spin 1.2s linear infinite;
510
+ }
511
+
512
+ .loading-spinner::before,
513
+ .loading-spinner::after {
514
+ content: '';
515
+ position: absolute;
516
+ border-radius: 50%;
517
+ }
518
+
519
+ .loading-spinner::before {
520
+ width: 100%;
521
+ height: 100%;
522
+ background: linear-gradient(to right, var(--primary-color) 0%, transparent 100%);
523
+ animation: spin 2s linear infinite;
524
+ }
525
+
526
+ .loading-spinner::after {
527
+ width: 75%;
528
+ height: 75%;
529
+ background-color: rgba(0, 0, 0, 0.7);
530
+ top: 12.5%;
531
+ left: 12.5%;
532
+ }
533
+
534
+ .loading-text {
535
+ position: absolute;
536
+ bottom: -40px;
537
+ color: white;
538
+ font-size: 1.2em;
539
+ font-weight: 500;
540
+ }
541
+
542
+ @keyframes spin {
543
+ 0% {
544
+ transform: rotate(0deg);
545
+ }
546
+ 100% {
547
+ transform: rotate(360deg);
548
+ }
549
  }
550
 
551
  .recipe-output {
552
  max-height: 800px;
553
  overflow-y: auto;
554
+ padding-right: 15px;
555
+ line-height: 1.6;
556
+ }
557
+
558
+ .recipe-output::-webkit-scrollbar {
559
+ width: 8px;
560
+ }
561
+
562
+ .recipe-output::-webkit-scrollbar-track {
563
+ background: #f1f1f1;
564
+ border-radius: 10px;
565
+ }
566
+
567
+ .recipe-output::-webkit-scrollbar-thumb {
568
+ background: var(--secondary-color);
569
+ border-radius: 10px;
570
+ }
571
+
572
+ .recipe-output::-webkit-scrollbar-thumb:hover {
573
+ background: var(--primary-color);
574
  }
575
 
576
  .recipe-output h2 {
577
  color: var(--primary-color);
578
+ border-bottom: 2px solid var(--secondary-color);
579
+ padding-bottom: 10px;
580
+ font-size: 1.8em;
581
+ margin-top: 30px;
582
  }
583
 
584
  .recipe-output h3 {
585
  color: var(--secondary-color);
586
+ font-size: 1.4em;
587
+ margin-top: 25px;
588
  }
589
 
590
+ /* Form inputs styling */
591
+ input[type="password"], input[type="text"] {
 
 
 
 
 
 
 
 
 
 
 
592
  border: 2px solid #e9ecef;
593
  border-radius: var(--border-radius);
594
+ padding: 15px;
595
  font-size: 1em;
596
  width: 100%;
597
+ transition: all 0.3s ease;
598
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
599
  }
600
 
601
+ input[type="password"]:focus, input[type="text"]:focus {
602
  border-color: var(--secondary-color);
603
  outline: none;
604
+ box-shadow: 0 0 0 3px rgba(78, 205, 196, 0.2);
605
  }
606
 
607
  /* Custom dropdown styling */
608
  select {
609
  appearance: none;
610
+ background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FF6B6B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E") no-repeat right 15px center;
611
  border: 2px solid #e9ecef;
612
  border-radius: var(--border-radius);
613
+ padding: 15px 45px 15px 15px;
614
  font-size: 1em;
615
  width: 100%;
616
+ transition: all 0.3s ease;
617
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
618
  }
619
 
620
  select:focus {
621
  border-color: var(--secondary-color);
622
  outline: none;
623
+ box-shadow: 0 0 0 3px rgba(78, 205, 196, 0.2);
624
+ }
625
+
626
+ /* Tooltip styling */
627
+ .tooltip {
628
+ position: relative;
629
+ display: inline-block;
630
+ cursor: pointer;
631
+ }
632
+
633
+ .tooltip .tooltiptext {
634
+ visibility: hidden;
635
+ width: 250px;
636
+ background-color: #333;
637
+ color: #fff;
638
+ text-align: center;
639
+ border-radius: 6px;
640
+ padding: 10px;
641
+ position: absolute;
642
+ z-index: 1;
643
+ bottom: 125%;
644
+ left: 50%;
645
+ margin-left: -125px;
646
+ opacity: 0;
647
+ transition: opacity 0.3s;
648
+ font-size: 0.9em;
649
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
650
+ }
651
+
652
+ .tooltip .tooltiptext::after {
653
+ content: "";
654
+ position: absolute;
655
+ top: 100%;
656
+ left: 50%;
657
+ margin-left: -5px;
658
+ border-width: 5px;
659
+ border-style: solid;
660
+ border-color: #333 transparent transparent transparent;
661
+ }
662
+
663
+ .tooltip:hover .tooltiptext {
664
+ visibility: visible;
665
+ opacity: 1;
666
+ }
667
+
668
+ /* Media queries for responsive design */
669
+ @media (max-width: 768px) {
670
+ .app-title {
671
+ font-size: 2.5em;
672
+ }
673
+
674
+ .app-subtitle {
675
+ font-size: 1.1em;
676
+ }
677
+
678
+ .input-section, .output-section {
679
+ padding: 20px;
680
+ }
681
+
682
+ .section-header {
683
+ font-size: 1.5em;
684
+ }
685
+
686
+ button.primary-button {
687
+ padding: 12px 25px;
688
+ font-size: 1.1em;
689
+ }
690
+ }
691
+
692
+ @media (max-width: 480px) {
693
+ .app-title {
694
+ font-size: 2em;
695
+ }
696
+
697
+ .app-subtitle {
698
+ font-size: 1em;
699
+ }
700
+
701
+ .input-section, .output-section {
702
+ padding: 15px;
703
+ }
704
+
705
+ .section-header {
706
+ font-size: 1.3em;
707
+ }
708
  }
709
 
710
  /* Remove Gradio branding */
 
712
  max-width: 100% !important;
713
  }
714
 
715
+ footer.footer-links {
716
  display: none !important;
717
  }
718
  """
719
 
720
+ # Custom HTML header with icons and improved design
721
  html_header = """
722
  <div class="app-header">
723
  <div class="app-title">🍲 Visual Recipe Assistant</div>
724
  <div class="app-subtitle">Upload images of ingredients you have on hand and get personalized recipe suggestions powered by AI</div>
725
  </div>
726
+
727
+ <div id="loading-overlay" class="loading-container">
728
+ <div class="loading-spinner">
729
+ <div class="loading-text">Generating your recipes...</div>
730
+ </div>
731
+ </div>
732
+
733
+ <script>
734
+ // JavaScript to handle loading state
735
+ function showLoading() {
736
+ document.getElementById('loading-overlay').classList.add('visible');
737
+ }
738
+
739
+ function hideLoading() {
740
+ document.getElementById('loading-overlay').classList.remove('visible');
741
+ }
742
+ </script>
743
  """
744
 
745
+ # Custom HTML footer with improved design
746
  html_footer = """
747
  <div class="footer">
748
+ <div class="footer-content">
749
+ <p><span class="footer-brand">🍲 Visual Recipe Assistant</span></p>
750
+ <p>Powered by Meta's Llama-Vision-Free Model & Together AI</p>
751
+ <p>Upload multiple ingredient images for more creative recipe combinations</p>
752
+ <div class="footer-links">
753
+ <a href="#" target="_blank">How It Works</a>
754
+ <a href="#" target="_blank">Privacy Policy</a>
755
+ <a href="#" target="_blank">Contact Us</a>
756
+ </div>
757
+ </div>
758
  </div>
759
+
760
+ <script>
761
+ // Add event listener to the submit button
762
+ document.addEventListener('DOMContentLoaded', function() {
763
+ const submitBtn = document.querySelector('button.primary-button');
764
+ if (submitBtn) {
765
+ submitBtn.addEventListener('click', function() {
766
+ showLoading();
767
+
768
+ // Hide loading after the response is received (this is approximate)
769
+ setTimeout(function() {
770
+ const output = document.querySelector('.recipe-output');
771
+ if (output && output.textContent.trim().length > 0) {
772
+ hideLoading();
773
+ } else {
774
+ // Check every second until content appears
775
+ const checkInterval = setInterval(function() {
776
+ if (output && output.textContent.trim().length > 0) {
777
+ hideLoading();
778
+ clearInterval(checkInterval);
779
+ }
780
+ }, 1000);
781
+
782
+ // Fallback: hide after 30 seconds regardless
783
+ setTimeout(function() {
784
+ hideLoading();
785
+ clearInterval(checkInterval);
786
+ }, 30000);
787
+ }
788
+ }, 3000);
789
+ });
790
+ }
791
+ });
792
+ </script>
793
  """
794
 
795
  # Create the Gradio interface with improved design
 
799
  with gr.Row():
800
  with gr.Column(scale=1):
801
  with gr.Group(elem_classes="input-section"):
802
+ gr.HTML('<h3 class="section-header"><i class="icon">πŸ”‘</i> API Configuration</h3>')
803
  api_key_input = gr.Textbox(
804
  label="Together API Key",
805
  placeholder="Enter your Together API key here...",
806
  type="password",
807
+ elem_classes="input-group",
808
+ info="Your API key will remain private and is used only for this session."
 
 
 
 
 
 
 
 
 
 
809
  )
810
 
811
+ gr.HTML('<h3 class="section-header"><i class="icon">πŸ“·</i> Upload Ingredients</h3>')
812
  # Use File component to handle multiple image uploads
813
  file_upload = gr.File(
814
  label="Upload images of ingredients",
 
817
  elem_classes="image-upload-container"
818
  )
819
 
820
+ image_input = gr.Gallery(
821
+ label="Uploaded Ingredients",
822
+ elem_id="ingredient-gallery",
823
+ columns=3,
824
+ rows=2,
825
+ height="auto",
826
+ visible=False
827
+ )
828
+
829
+ gr.HTML('<h3 class="section-header"><i class="icon">βš™οΈ</i> Recipe Preferences</h3>')
830
  with gr.Row():
831
  num_recipes = gr.Slider(
832
  minimum=1,
 
858
 
859
  with gr.Column(scale=1):
860
  with gr.Group(elem_classes="output-section"):
861
+ gr.HTML('<h3 class="section-header"><i class="icon">🍽️</i> Your Personalized Recipes</h3>')
862
  output = gr.Markdown(elem_classes="recipe-output")
863
 
864
  gr.HTML(html_footer)
 
866
  # Handle file uploads to display in gallery
867
  def update_gallery(files):
868
  if not files:
869
+ return gr.Gallery.update(visible=False)
870
+ return gr.Gallery.update(value=[file.name for file in files], visible=True)
871
 
872
  file_upload.change(fn=update_gallery, inputs=file_upload, outputs=image_input)
873