GoogleCalc commited on
Commit
a3ddfc3
Β·
verified Β·
1 Parent(s): 9cab5f2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +664 -1078
app.py CHANGED
@@ -28,95 +28,6 @@ if db_dir and not os.path.exists(db_dir):
28
 
29
  STARTER_END_DAY = 21
30
 
31
- # ============================================================
32
- # GOOGLE SHEETS INTEGRATION
33
- # ============================================================
34
-
35
- def init_google_sheets():
36
- """Initialize Google Sheets connection."""
37
- try:
38
- import gspread
39
- from oauth2client.service_account import ServiceAccountCredentials
40
-
41
- creds_json = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
42
- sheet_id = os.environ.get("GOOGLE_SHEETS_ID")
43
-
44
- if not creds_json or not sheet_id:
45
- print("⚠️ Google Sheets not configured. Using local database only.")
46
- return None, None
47
-
48
- creds_dict = json.loads(creds_json)
49
- scope = [
50
- "https://spreadsheets.google.com/feeds",
51
- "https://www.googleapis.com/auth/drive"
52
- ]
53
-
54
- creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope)
55
- client = gspread.authorize(creds)
56
-
57
- try:
58
- sheet = client.open_by_key(sheet_id)
59
- print(f"βœ… Google Sheets connected: {sheet.title}")
60
- except:
61
- sheet = client.create("How Much Feed Calculator Leads")
62
- print(f"βœ… New Google Sheet created")
63
-
64
- return client, sheet
65
-
66
- except Exception as e:
67
- print(f"⚠️ Google Sheets error: {e}")
68
- return None, None
69
-
70
-
71
- # Google Sheets is initialized lazily on first sync.
72
- gs_client = None
73
- gs_sheet = None
74
- gs_initialized = False
75
- gs_init_lock = threading.Lock()
76
-
77
- def get_google_sheets():
78
- global gs_client, gs_sheet, gs_initialized
79
- if gs_initialized:
80
- return gs_client, gs_sheet
81
- with gs_init_lock:
82
- if not gs_initialized:
83
- gs_client, gs_sheet = init_google_sheets()
84
- gs_initialized = True
85
- return gs_client, gs_sheet
86
-
87
-
88
- def sync_to_google_sheets(email, country, currency, breed, action="lead"):
89
- client, sheet = get_google_sheets()
90
- if not client or not sheet:
91
- return False
92
-
93
- try:
94
- if action == "lead":
95
- worksheet_name = "Leads"
96
- headers = ["Email", "Country", "Currency", "Breed", "Created At"]
97
- row_data = [email, country, currency, breed, datetime.now().isoformat()]
98
- elif action == "question":
99
- worksheet_name = "Questions"
100
- headers = ["Email", "Question", "Breed", "Created At"]
101
- row_data = [email or "", "", "", datetime.now().isoformat()]
102
- else:
103
- return False
104
-
105
- try:
106
- worksheet = sheet.worksheet(worksheet_name)
107
- except:
108
- worksheet = sheet.add_worksheet(title=worksheet_name, rows=1000, cols=20)
109
- worksheet.append_row(headers)
110
-
111
- worksheet.append_row(row_data)
112
- print(f"βœ… Synced to Google Sheets: {email}")
113
- return True
114
-
115
- except Exception as e:
116
- print(f"⚠️ Google Sheets sync error: {e}")
117
- return False
118
-
119
-
120
  # ============================================================
121
  # VERIFIED BREED DATA - From Official PDFs
122
  # ============================================================
@@ -206,81 +117,6 @@ def interpolate_feed(day, breed):
206
  return feed_data[56]
207
 
208
 
209
- # ============================================================
210
- # BENCHMARK TABLE GENERATOR
211
- # ============================================================
212
-
213
- def get_benchmark_table(breed):
214
- weight_data = get_breed_weight_data(breed)
215
- feed_data = get_breed_feed_data(breed)
216
- days = [7, 14, 21, 28, 35, 42, 49, 56]
217
- table_data = []
218
- for day in days:
219
- weight = weight_data.get(day, 0)
220
- feed = feed_data.get(day, 0)
221
- fcr = feed / weight if weight > 0 else 0
222
- table_data.append([day, f"{weight:.3f}", f"{feed:.3f}", f"{fcr:.3f}"])
223
- return table_data, days
224
-
225
-
226
- def get_breed_comparison_table():
227
- breeds = ["Ross 308", "Cobb 500", "Arbor Acres"]
228
- days = [7, 14, 21, 28, 35, 42, 49, 56]
229
- table_data = []
230
- for day in days:
231
- row = [day]
232
- for breed in breeds:
233
- weight_data = get_breed_weight_data(breed)
234
- feed_data = get_breed_feed_data(breed)
235
- weight = weight_data.get(day, 0)
236
- feed = feed_data.get(day, 0)
237
- fcr = feed / weight if weight > 0 else 0
238
- row.append(f"{weight:.3f}")
239
- row.append(f"{feed:.3f}")
240
- row.append(f"{fcr:.3f}")
241
- table_data.append(row)
242
- return table_data, breeds, days
243
-
244
-
245
- def get_benchmark_html(breed):
246
- table_data, _ = get_benchmark_table(breed)
247
- rows = []
248
- for day, weight, feed, fcr in table_data:
249
- rows.append(f"<tr><td>{day}</td><td>{weight} kg</td><td>{feed} kg</td><td>{fcr}</td></tr>")
250
- return f"""
251
- <div class="table-wrap">
252
- <table class="feed-table">
253
- <thead><tr><th>Days</th><th>Body Weight</th><th>Cumulative Feed</th><th>FCR</th></tr></thead>
254
- <tbody>{''.join(rows)}</tbody>
255
- </table>
256
- </div>
257
- """
258
-
259
-
260
- def get_comparison_html():
261
- data, breeds, _ = get_breed_comparison_table()
262
- headers = ['Days'] + [f'{b} Wt' for b in breeds] + [f'{b} Feed' for b in breeds]
263
- rows = []
264
- for row in data:
265
- day = row[0]
266
- vals = row[1:]
267
- weights = vals[0::3]
268
- feeds = vals[1::3]
269
- cells = [f'<td>{day}</td>']
270
- cells += [f'<td>{v} kg</td>' for v in weights]
271
- cells += [f'<td>{v} kg</td>' for v in feeds]
272
- rows.append('<tr>' + ''.join(cells) + '</tr>')
273
- head = ''.join(f'<th>{h}</th>' for h in headers)
274
- return f"""
275
- <div class="table-wrap">
276
- <table class="feed-table comparison-table">
277
- <thead><tr>{head}</tr></thead>
278
- <tbody>{''.join(rows)}</tbody>
279
- </table>
280
- </div>
281
- """
282
-
283
-
284
  # ============================================================
285
  # CURRENCY DATA
286
  # ============================================================
@@ -346,247 +182,204 @@ COUNTRY_LIST = sorted([(code, f"{name} ({symbol})") for code, (name, symbol, cur
346
 
347
 
348
  # ============================================================
349
- # DATABASE
350
  # ============================================================
351
 
352
- def init_db():
353
- conn = sqlite3.connect(DB_PATH)
354
- cur = conn.cursor()
355
-
356
- cur.execute("""
357
- CREATE TABLE IF NOT EXISTS leads (
358
- id INTEGER PRIMARY KEY AUTOINCREMENT,
359
- email TEXT UNIQUE,
360
- country TEXT,
361
- currency TEXT,
362
- breed TEXT,
363
- created_at TEXT
364
- )
365
- """)
366
-
367
- cur.execute("""
368
- CREATE TABLE IF NOT EXISTS farmer_questions (
369
- id INTEGER PRIMARY KEY AUTOINCREMENT,
370
- email TEXT,
371
- question TEXT,
372
- breed TEXT,
373
- created_at TEXT
374
- )
375
- """)
376
-
377
- cur.execute("""
378
- CREATE TABLE IF NOT EXISTS calculations (
379
- id INTEGER PRIMARY KEY AUTOINCREMENT,
380
- email TEXT,
381
- country TEXT,
382
- currency TEXT,
383
- breed TEXT,
384
- birds INTEGER,
385
- weeks INTEGER,
386
- days INTEGER,
387
- total_days INTEGER,
388
- bag_size REAL,
389
- price_per_bag REAL,
390
- total_feed_kg REAL,
391
- starter_kg REAL,
392
- finisher_kg REAL,
393
- starter_bags REAL,
394
- finisher_bags REAL,
395
- total_bags REAL,
396
- rounded_bags INTEGER,
397
- total_cost REAL,
398
- created_at TEXT
399
- )
400
- """)
401
-
402
- cur.execute("""
403
- CREATE TABLE IF NOT EXISTS analytics (
404
- id INTEGER PRIMARY KEY AUTOINCREMENT,
405
- visit_date TEXT,
406
- ip TEXT,
407
- country TEXT,
408
- breed TEXT,
409
- action TEXT,
410
- created_at TEXT
411
- )
412
- """)
413
-
414
- conn.commit()
415
- conn.close()
416
- print(f"βœ… Database initialized at: {DB_PATH}")
417
-
418
-
419
- init_db()
420
 
421
 
422
  # ============================================================
423
- # DATABASE HELPERS
424
  # ============================================================
425
 
426
- def valid_email(email):
427
- if not email:
428
- return False
429
- email = email.strip()
430
- pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
431
- return bool(re.match(pattern, email))
 
 
 
432
 
 
 
 
 
 
 
 
 
 
 
433
 
434
- def save_lead(email, country, currency, breed):
435
  if not valid_email(email):
436
- return
437
-
438
- conn = sqlite3.connect(DB_PATH)
439
- cur = conn.cursor()
440
- cur.execute("""
441
- INSERT OR IGNORE INTO leads (email, country, currency, breed, created_at)
442
- VALUES (?, ?, ?, ?, ?)
443
- """, (email.strip().lower(), country, currency, breed, datetime.utcnow().isoformat()))
444
- conn.commit()
445
- conn.close()
446
 
447
- try:
448
- sync_to_google_sheets(email, country, currency, breed, "lead")
449
- except Exception as e:
450
- print(f"Google Sheets sync error: {e}")
451
 
 
 
 
452
 
453
- def save_question(email, question, breed):
454
- if not question or not question.strip():
455
- return
 
456
 
457
- conn = sqlite3.connect(DB_PATH)
458
- cur = conn.cursor()
459
- cur.execute("""
460
- INSERT INTO farmer_questions (email, question, breed, created_at)
461
- VALUES (?, ?, ?, ?)
462
- """, (email.strip().lower() if email else "", question.strip(), breed, datetime.utcnow().isoformat()))
463
- conn.commit()
464
- conn.close()
465
 
466
- try:
467
- sync_to_google_sheets(email, question, breed, "question")
468
- except Exception as e:
469
- print(f"Google Sheets sync error: {e}")
470
 
 
 
 
 
 
 
471
 
472
- def save_calculation(email, country, currency, breed, birds, weeks, days, total_days,
473
- bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
474
- starter_bags, finisher_bags, total_bags, rounded_bags, total_cost):
475
- conn = sqlite3.connect(DB_PATH)
476
- cur = conn.cursor()
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
- cur.execute("""
479
- INSERT INTO calculations (
480
- email, country, currency, breed, birds, weeks, days, total_days,
481
- bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
482
- starter_bags, finisher_bags, total_bags, rounded_bags, total_cost, created_at
483
- )
484
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
485
- """, (email.strip().lower() if email else "", country, currency, breed, birds, weeks, days, total_days,
486
- bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
487
- starter_bags, finisher_bags, total_bags, rounded_bags, total_cost,
488
- datetime.utcnow().isoformat()))
489
-
490
- conn.commit()
491
- conn.close()
 
 
 
 
 
 
 
 
 
492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
 
494
- def track_visit_async(request: gr.Request):
495
- try:
496
- client_ip = request.client.host if request and hasattr(request, 'client') else "unknown"
497
- conn = sqlite3.connect(DB_PATH)
498
- cur = conn.cursor()
499
- cur.execute("""
500
- INSERT INTO analytics (visit_date, ip, country, action, created_at)
501
- VALUES (?, ?, ?, ?, ?)
502
- """, (
503
- datetime.now().strftime("%Y-%m-%d"),
504
- client_ip,
505
- detect_country_from_env() or "NG",
506
- "page_visit",
507
- datetime.now().isoformat()
508
- ))
509
- conn.commit()
510
- conn.close()
511
- except Exception as e:
512
- print(f"Analytics error: {e}")
513
-
514
-
515
- def track_calculation(breed, birds, days):
516
- try:
517
- conn = sqlite3.connect(DB_PATH)
518
- cur = conn.cursor()
519
- cur.execute("""
520
- INSERT INTO analytics (visit_date, country, breed, action, created_at)
521
- VALUES (?, ?, ?, ?, ?)
522
- """, (
523
- datetime.now().strftime("%Y-%m-%d"),
524
- detect_country_from_env() or "NG",
525
- breed,
526
- f"calculation_{birds}_{days}",
527
- datetime.now().isoformat()
528
- ))
529
- conn.commit()
530
- conn.close()
531
- return True
532
- except:
533
- return False
534
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
 
536
- def get_analytics_summary():
537
- conn = sqlite3.connect(DB_PATH)
538
- cur = conn.cursor()
539
-
540
- cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit'")
541
- total_visits = cur.fetchone()[0] or 0
542
- cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit' AND visit_date = ?",
543
- (datetime.now().strftime("%Y-%m-%d"),))
544
- today_visits = cur.fetchone()[0] or 0
545
- cur.execute("SELECT COUNT(*) FROM analytics WHERE action LIKE 'calculation_%'")
546
- total_calculations = cur.fetchone()[0] or 0
547
- cur.execute("SELECT COUNT(DISTINCT ip) FROM analytics WHERE action = 'page_visit' AND ip != 'unknown'")
548
- unique_visitors = cur.fetchone()[0] or 0
549
- cur.execute("SELECT breed, COUNT(*) FROM analytics WHERE action LIKE 'calculation_%' GROUP BY breed")
550
- breed_stats = cur.fetchall()
551
- cur.execute("SELECT COUNT(*) FROM leads")
552
- total_leads = cur.fetchone()[0] or 0
553
- conn.close()
554
-
555
- return {
556
- "total_visits": total_visits,
557
- "today_visits": today_visits,
558
- "unique_visitors": unique_visitors,
559
- "total_calculations": total_calculations,
560
- "total_leads": total_leads,
561
- "breed_stats": breed_stats
562
- }
563
 
 
 
 
 
 
564
 
565
- # ============================================================
566
- # SHARE LINKS GENERATOR
567
- # ============================================================
568
 
569
- def get_share_links(app_url="https://howmuchfeed.com"):
570
- share_text = f"πŸ” Calculate your poultry feed needs at {app_url}"
571
- encoded = urllib.parse.quote(share_text)
572
- return {
573
- "whatsapp": f"https://wa.me/?text={encoded}",
574
- "email": f"mailto:?subject=How Much Feed? - Poultry Feed Calculator&body={encoded}",
575
- "facebook": f"https://www.facebook.com/sharer/sharer.php?u={urllib.parse.quote(app_url)}",
576
- "twitter": f"https://twitter.com/intent/tweet?text={encoded}",
577
- }
578
 
 
 
 
 
 
 
579
 
580
- # ============================================================
581
- # QUESTION / ANSWER
582
- # ============================================================
583
 
584
  def answer_farmer_question(question, email, breed):
585
  if not question or not question.strip():
586
  return "Please type or speak your question first."
587
 
588
- save_question(email or "", question, breed)
589
-
590
  q = question.lower().strip()
591
 
592
  if "bag" in q and ("feed" in q or "birds" in q):
@@ -650,675 +443,449 @@ The result is for **feed alone**.
650
  **β€œ{question.strip()}”**
651
 
652
  Your question has been saved.
653
-
654
- We can use the questions farmers submit to decide which answers, calculators and features should be added to the app next.
655
  """
656
 
657
 
658
- # ============================================================
659
- # UPDATE FUNCTIONS
660
- # ============================================================
661
-
662
- def update_benchmark(breed):
663
- return get_benchmark_html(breed)
664
-
665
-
666
- def update_selected_breed_badge(breed):
667
- safe_breed = breed or "Ross 308"
668
- return f'<span class="selected-breed-badge">{safe_breed}</span>'
 
 
 
 
 
669
 
670
 
671
- def on_country_change_handler(country_selection):
672
- if country_selection == "Auto-Detect":
673
- country_code = detect_country_from_env() or "NG"
674
- else:
675
- country_code = country_selection
676
- _, symbol, currency_code = get_country_info(country_code)
677
- default_price = get_default_price(currency_code)
678
- return country_code, default_price
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
 
680
 
681
  # ============================================================
682
- # ADMIN FUNCTIONS
683
  # ============================================================
684
 
685
- def show_analytics(password):
686
- if password != ADMIN_PASSWORD:
687
- return "### πŸ”’ Admin Access Required\n\nPlease enter the correct admin password to view analytics."
688
-
689
- stats = get_analytics_summary()
690
- breed_stats_html = ""
691
- for breed, count in stats["breed_stats"]:
692
- breed_stats_html += f"<li><strong>{breed}</strong>: {count} calculations</li>"
693
- if not breed_stats_html:
694
- breed_stats_html = "<li>No calculations yet</li>"
695
-
696
- return f"""
697
- ### How Much Feed? - Analytics
698
-
699
- | Metric | Value |
700
- |--------|-------|
701
- | **Total Visits** | **{stats['total_visits']:,}** |
702
- | **Today's Visits** | **{stats['today_visits']}** |
703
- | **Unique Visitors** | **{stats['unique_visitors']:,}** |
704
- | **Total Calculations** | **{stats['total_calculations']:,}** |
705
- | **Total Leads (Emails)** | **{stats['total_leads']:,}** |
706
 
707
- ### Most Popular Breeds
 
 
 
 
 
 
 
 
 
 
 
 
708
 
709
- <ul>{breed_stats_html}</ul>
710
-
711
- ---
712
- *Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
713
- """
714
-
715
-
716
- def admin_login(password):
717
- if password == ADMIN_PASSWORD:
718
- return show_analytics(password), gr.update(visible=True), gr.update(visible=False)
719
- else:
720
- return "❌ Incorrect password. Please try again.", gr.update(visible=False), gr.update(visible=True)
721
-
722
-
723
- def logout_admin():
724
- return "πŸ”’ Enter password to view analytics.", gr.update(visible=True), gr.update(visible=False)
725
-
726
-
727
- def export_leads_to_csv():
728
- conn = sqlite3.connect(DB_PATH)
729
- cur = conn.cursor()
730
- cur.execute("SELECT email, country, currency, breed, created_at FROM leads ORDER BY created_at DESC")
731
- rows = cur.fetchall()
732
- conn.close()
733
-
734
- import csv
735
- from io import StringIO
736
- output = StringIO()
737
- writer = csv.writer(output)
738
- writer.writerow(["Email", "Country", "Currency", "Breed", "Created At"])
739
- writer.writerows(rows)
740
- return output.getvalue()
741
-
742
-
743
- def export_leads_wrapper():
744
- import tempfile
745
- csv_data = export_leads_to_csv()
746
- temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
747
- temp.write(csv_data)
748
- temp.close()
749
- return temp.name
750
-
751
-
752
- def export_calculations_wrapper():
753
- conn = sqlite3.connect(DB_PATH)
754
- cur = conn.cursor()
755
- cur.execute("""
756
- SELECT email, breed, birds, total_days, rounded_bags, bag_size, total_cost, created_at
757
- FROM calculations
758
- ORDER BY created_at DESC
759
- """)
760
- rows = cur.fetchall()
761
- conn.close()
762
-
763
- import csv
764
- import tempfile
765
- temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
766
- writer = csv.writer(temp)
767
- writer.writerow(["Email", "Breed", "Birds", "Total Days", "Bags", "Bag Size (kg)", "Total Cost", "Created At"])
768
- writer.writerows(rows)
769
- temp.close()
770
- return temp.name
771
-
772
-
773
- # ============================================================
774
- # FEED CALCULATION - COLORFUL RESULTS
775
- # ============================================================
776
-
777
- def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code, breed, request: gr.Request):
778
- try:
779
- birds = int(birds)
780
- weeks = int(weeks)
781
- days = int(days)
782
- bag_size = float(bag_size)
783
- price_per_bag = float(price_per_bag)
784
- except Exception:
785
- return "⚠️ Please enter valid numbers.", ""
786
-
787
- if birds <= 0:
788
- return "⚠️ Number of birds must be greater than zero.", ""
789
- if weeks < 0:
790
- return "⚠️ Weeks cannot be negative.", ""
791
- if days < 0 or days > 6:
792
- return "⚠️ Days must be between 0 and 6.", ""
793
- if bag_size <= 0:
794
- return "⚠️ Feed bag size must be greater than zero.", ""
795
- if price_per_bag < 0:
796
- return "⚠️ Price per bag cannot be negative.", ""
797
-
798
- if not valid_email(email):
799
- return "πŸ“§ **Email required**\n\nPlease enter your email address before calculating.", ""
800
-
801
- country_name, symbol, currency_code = get_country_info(country_code)
802
- currency_symbol = symbol
803
-
804
- save_lead(email, country_code, currency_code, breed)
805
- track_calculation(breed, birds, (weeks * 7 + days))
806
-
807
- total_days = (weeks * 7) + days
808
- total_feed_per_bird = interpolate_feed(total_days, breed)
809
- total_feed_kg = birds * total_feed_per_bird
810
-
811
- starter_days = min(total_days, STARTER_END_DAY)
812
- starter_per_bird = interpolate_feed(starter_days, breed)
813
- starter_kg = birds * starter_per_bird
814
- finisher_kg = max(total_feed_kg - starter_kg, 0)
815
-
816
- starter_bags = starter_kg / bag_size
817
- finisher_bags = finisher_kg / bag_size
818
- total_bags = total_feed_kg / bag_size
819
- rounded_bags = math.ceil(total_bags)
820
- total_cost = rounded_bags * price_per_bag
821
-
822
- expected_weight = interpolate_weight(total_days, breed)
823
- flock_weight = expected_weight * birds
824
-
825
- save_calculation(email, country_code, currency_code, breed, birds, weeks, days, total_days,
826
- bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
827
- starter_bags, finisher_bags, total_bags, rounded_bags, total_cost)
828
-
829
- share_links = get_share_links()
830
- starter_bags_display = f"{starter_bags:.1f}"
831
- finisher_bags_display = f"{finisher_bags:.1f}"
832
- age_text = f"{weeks} week{'s' if weeks != 1 else ''}"
833
- if days:
834
- age_text += f" + {days} day{'s' if days != 1 else ''}"
835
-
836
- html = f"""
837
- <div class="results-shell">
838
-
839
- <!-- BUY CARD - Mint Green -->
840
- <div class="buy-card">
841
- <div class="eyebrow">🎯 YOUR FEED RESULT</div>
842
- <div class="buy-label">BUY</div>
843
- <div class="buy-number">{rounded_bags}</div>
844
- <div class="buy-unit">BAGS</div>
845
- <div class="buy-note">Recommended amount, rounded up to the next whole bag.</div>
846
- </div>
847
-
848
- <!-- Context -->
849
- <div class="result-context">
850
- <strong>{birds:,} {breed} broilers</strong> Β· {age_text} ({total_days} days)
851
- </div>
852
-
853
- <!-- Plain Answer - Yellow Highlight -->
854
- <div class="plain-answer">
855
- You need <strong>{total_feed_kg:,.1f} kg</strong> of feed, equal to about
856
- <strong>{total_bags:.1f} bags</strong> of {bag_size:g} kg feed.
857
- </div>
858
-
859
- <!-- METRIC GRID - Colorful Cards -->
860
- <div class="metric-grid">
861
- <div class="metric-card" style="border-left: 4px solid #00A86B;">
862
- <div class="metric-icon">βš–οΈ</div>
863
- <div class="metric-label">Feed needed</div>
864
- <strong style="color: #00A86B;">{total_feed_kg:,.1f} kg</strong>
865
- </div>
866
- <div class="metric-card" style="border-left: 4px solid #FFB800;">
867
- <div class="metric-icon">βœ“</div>
868
- <div class="metric-label">Exact amount</div>
869
- <strong style="color: #FFB800;">{total_bags:.1f} bags</strong>
870
- </div>
871
- <div class="metric-card" style="border-left: 4px solid #EA4335;">
872
- <div class="metric-icon">₦</div>
873
- <div class="metric-label">Estimated cost</div>
874
- <strong style="color: #EA4335;">{currency_symbol}{total_cost:,.0f}</strong>
875
- </div>
876
- <div class="metric-card" style="border-left: 4px solid #1E4620;">
877
- <div class="metric-icon">πŸ“…</div>
878
- <div class="metric-label">Growing period</div>
879
- <strong style="color: #1E4620;">{total_days} days</strong>
880
- </div>
881
- </div>
882
-
883
- <!-- FEED BREAKDOWN -->
884
- <div class="result-section">
885
- <div class="section-title">🌾 Feed Breakdown</div>
886
- <div class="table-wrap">
887
- <table class="feed-table">
888
- <thead><tr><th>Stage</th><th>Feed</th><th>Bags</th></tr></thead>
889
- <tbody>
890
- <tr><td>Starter <span class="muted">(0–21 days)</span></td><td>{starter_kg:,.1f} kg</td><td>{starter_bags_display}</td></tr>
891
- <tr><td>Finisher <span class="muted">(Day 22+)</span></td><td>{finisher_kg:,.1f} kg</td><td>{finisher_bags_display}</td></tr>
892
- <tr class="total-row"><td>TOTAL</td><td>{total_feed_kg:,.1f} kg</td><td>{total_bags:.1f} bags</td></tr>
893
- </tbody>
894
- </table>
895
- </div>
896
- </div>
897
-
898
- <!-- FEED COST - Yellow Card -->
899
- <div class="cost-card">
900
- <div class="section-title">πŸ’° Feed Cost Estimate</div>
901
- <div class="cost-main">{currency_symbol}{total_cost:,.0f}</div>
902
- <div class="cost-note">Based on {rounded_bags} bags Γ— {currency_symbol}{price_per_bag:,.0f} per bag.</div>
903
- </div>
904
-
905
- <!-- PERFORMANCE - Green Card -->
906
- <div class="result-section performance-section">
907
- <div class="section-title">πŸ“ˆ Expected Performance</div>
908
- <div class="performance-grid">
909
- <div><span>Body weight per bird</span><strong style="color: #00A86B;">{expected_weight:.3f} kg</strong></div>
910
- <div><span>Flock live weight</span><strong style="color: #1E4620;">{flock_weight:,.1f} kg</strong></div>
911
- </div>
912
- </div>
913
-
914
- <!-- SHARE -->
915
- <div class="result-section share-section">
916
- <div class="section-title">πŸ“€ Share Result</div>
917
- <div class="share-grid">
918
- <a class="share-btn whatsapp" href="{share_links['whatsapp']}" target="_blank">WhatsApp</a>
919
- <a class="share-btn email" href="{share_links['email']}" target="_blank">Email</a>
920
- <a class="share-btn facebook" href="{share_links['facebook']}" target="_blank">Facebook</a>
921
- <a class="share-btn twitter" href="{share_links['twitter']}" target="_blank">X</a>
922
- </div>
923
- </div>
924
-
925
- <div class="result-disclaimer">
926
- ⚠️ This is an estimate based on the selected {breed} benchmark. Actual feed use can vary with feed quality, management, environment and bird performance.
927
- </div>
928
- </div>
929
- """
930
-
931
- return html, f"βœ… Saved for {email.strip().lower()}."
932
-
933
-
934
- # ============================================================
935
- # CSS - COMPLETE FIX: LIGHT THEME + COLORFUL RESULTS
936
- # ============================================================
937
-
938
- CSS = """
939
- /* ============================================================
940
- FORCE LIGHT THEME - NO DARK MODE - NO FLOATER
941
- ============================================================ */
942
- :root {
943
- color-scheme: light !important;
944
- --app-bg-main: #f4f7f5;
945
- --app-card-surface: #ffffff;
946
- --brand-primary-mint: #00a86b;
947
- --brand-dark-charcoal: #0b3022;
948
- --brand-accent-yellow: #ffb800;
949
- --ui-border-muted: #d6e0db;
950
- --text-helper: #5a6e65;
951
- }
952
-
953
- /* Force light on everything */
954
  html, body, .gradio-container, .gradio-container * {
955
- color-scheme: light !important;
956
- background-color: #f4f7f5 !important;
957
- color: #0b3022 !important;
958
- }
959
-
960
- /* REMOVE GOOGLECALC FLOATER */
961
- #huggingface-space-header,
962
- #huggingface-space-header *,
963
- .hf-header,
964
- .hf-header *,
965
- [data-testid="header"],
966
- [data-testid="header"] *,
967
- .gradio-header,
968
- .gradio-header *,
969
- .space-header,
970
- .space-header *,
971
- .duplicated-from-banner,
972
- .duplicated-from-banner *,
973
- .flex.items-center.gap-2,
974
- .flex.items-center.gap-2 *,
975
- .absolute.top-4.left-4,
976
- .absolute.top-4.left-4 *,
977
- [class*="duplicated"],
978
- [class*="Duplicated"],
979
- [class*="origin"],
980
- [class*="Origin"],
981
- .p-2.rounded-lg.border,
982
- .p-2.rounded-lg.border * {
983
- display: none !important;
984
- opacity: 0 !important;
985
- height: 0 !important;
986
- min-height: 0 !important;
987
- max-height: 0 !important;
988
- padding: 0 !important;
989
- margin: 0 !important;
990
- border: 0 !important;
991
- pointer-events: none !important;
992
- visibility: hidden !important;
993
- position: absolute !important;
994
- clip: rect(0,0,0,0) !important;
995
- overflow: hidden !important;
996
- }
997
-
998
- /* Kill any dark mode */
999
- .dark, .dark *,
1000
- [data-theme="dark"], [data-theme="dark"] *,
1001
- .dark-mode, .dark-mode *,
1002
- [class*="dark"] {
1003
- background-color: #f4f7f5 !important;
1004
- color: #0b3022 !important;
1005
- color-scheme: light !important;
1006
- }
1007
-
1008
- body, .gradio-container, .gradio-container .block, .gradio-container .gr-group {
1009
- background: #f4f7f5 !important;
1010
- color: #0b3022 !important;
1011
  color-scheme: light !important;
1012
  }
1013
 
1014
  .gradio-container {
1015
- width: 100% !important;
1016
- max-width: 1180px !important;
1017
  margin: 0 auto !important;
1018
- padding: 0 10px 32px !important;
1019
- box-sizing: border-box !important;
1020
- background: #f4f7f5 !important;
 
 
 
1021
  }
1022
 
1023
- /* HEADER */
1024
  .app-header {
1025
- background: #0b3022 !important;
1026
- color: #fff !important;
1027
- border-radius: 0 0 16px 16px;
1028
- padding: 14px 14px 16px;
1029
- margin: 0 -10px 12px;
1030
  }
1031
- .app-header .brand-row { display: flex; align-items: center; gap: 8px; }
1032
- .app-header .brand-icon { font-size: 28px; line-height: 1; }
1033
- .app-header h1 { color: #fff !important; margin: 0; font-size: clamp(20px, 5vw, 28px); line-height: 1.1; font-weight: 800; }
1034
- .app-header p { color: rgba(255,255,255,.88) !important; margin: 4px 0 0 36px; font-size: 13px; line-height: 1.4; }
1035
-
1036
- .status-card {
1037
- background: #ffffff !important;
1038
- border: 1px solid #d6e0db;
1039
- border-left: 4px solid #00a86b;
1040
- border-radius: 10px;
1041
- padding: 10px 12px;
1042
- margin-bottom: 10px;
1043
- font-size: 12px;
1044
- color: #0b3022 !important;
 
 
1045
  }
1046
- .status-card strong { color: #0b3022 !important; }
1047
-
1048
- .form-section {
1049
- background: #ffffff !important;
1050
- border: 1px solid #d6e0db !important;
1051
- border-radius: 12px !important;
1052
- margin-bottom: 10px !important;
1053
- padding: 12px !important;
 
 
 
1054
  }
1055
- .section-heading {
 
 
 
 
1056
  display: flex;
1057
  align-items: center;
1058
  gap: 8px;
1059
- margin: 0 0 10px;
1060
- color: #0b3022 !important;
1061
- font-size: 15px;
1062
- font-weight: 800;
1063
  }
1064
- .step-number {
1065
- width: 26px;
1066
- height: 26px;
1067
  border-radius: 50%;
1068
- background: #0b3022;
1069
- color: #fff !important;
1070
  display: inline-flex;
1071
  align-items: center;
1072
  justify-content: center;
1073
- font-size: 12px;
1074
- font-weight: 800;
1075
- flex: 0 0 26px;
1076
  }
1077
 
1078
- .gradio-container input, .gradio-container textarea, .gradio-container select {
1079
  background: #fff !important;
1080
- color: #0b3022 !important;
1081
- border: 1px solid #C8D5CE !important;
1082
- border-radius: 8px !important;
1083
  min-height: 44px !important;
1084
  font-size: 15px !important;
 
1085
  }
1086
- .gradio-container input:focus, .gradio-container textarea:focus, .gradio-container select:focus {
1087
- border-color: #00a86b !important;
1088
- box-shadow: 0 0 0 3px rgba(0,168,107,.16) !important;
1089
  }
1090
- .gradio-container label { color: #0b3022 !important; font-weight: 650 !important; }
1091
- .gradio-container .info { color: #5a6e65 !important; }
1092
-
1093
- .age-row, .feed-row { gap: 8px !important; }
1094
 
1095
- .calculate-btn button {
1096
  width: 100% !important;
1097
- min-height: 48px !important;
1098
- border: 0 !important;
1099
- border-radius: 10px !important;
1100
- background: #00a86b !important;
1101
  color: #fff !important;
1102
- font-size: 15px !important;
 
 
1103
  font-weight: 800 !important;
1104
- box-shadow: 0 4px 12px rgba(0,168,107,.20) !important;
1105
  }
1106
- .calculate-btn button:hover { background: #008F5A !important; }
1107
 
1108
  /* ============================================================
1109
- COLORFUL RESULTS
1110
  ============================================================ */
1111
- .results-shell { width: 100%; box-sizing: border-box; color: #0b3022; }
1112
 
1113
- /* BUY CARD - Mint Green */
1114
- .buy-card {
1115
- background: linear-gradient(145deg, #F1FBF6 0%, #FFFFFF 100%);
1116
- border: 2px solid #00a86b;
1117
- border-radius: 14px;
1118
- padding: 14px 12px;
 
 
 
 
 
 
 
 
 
1119
  text-align: center;
 
 
 
1120
  }
1121
- .eyebrow { color: #00a86b !important; font-size: 11px; font-weight: 800; letter-spacing: .08em; }
1122
- .buy-label { margin-top: 4px; color: #0b3022 !important; font-size: 12px; font-weight: 800; }
1123
- .buy-number { color: #00a86b !important; font-size: clamp(50px, 15vw, 76px); font-weight: 900; line-height: .9; }
1124
- .buy-unit { color: #0b3022 !important; font-size: 18px; font-weight: 900; letter-spacing: .06em; }
1125
- .buy-note { margin-top: 6px; color: #5a6e65 !important; font-size: 11px; }
1126
-
1127
- .result-context { text-align: center; margin: 10px 2px 4px; color: #5a6e65 !important; font-size: 12px; }
1128
- .result-context strong { color: #0b3022 !important; }
1129
-
1130
- /* Plain Answer - Yellow Highlight */
1131
- .plain-answer {
1132
- background: #fff7db !important;
1133
- border-left: 4px solid #ffb800;
1134
- border-radius: 8px;
1135
- padding: 10px 12px;
1136
- margin: 8px 0 10px;
1137
- font-size: 13px;
1138
- line-height: 1.5;
1139
- color: #0b3022 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1140
  }
1141
- .plain-answer strong { color: #0b3022 !important; }
 
 
 
 
 
 
 
 
 
 
 
1142
 
1143
- /* Metric Cards - Colorful */
1144
- .metric-grid {
1145
  display: grid;
1146
  grid-template-columns: repeat(2, 1fr);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1147
  gap: 8px;
1148
- margin-bottom: 12px;
1149
  }
1150
- .metric-card {
1151
- background: #fff;
1152
- border: 1px solid #d6e0db;
1153
  border-radius: 10px;
1154
- padding: 10px 8px;
1155
  }
1156
- .metric-icon { display: block; color: #0b3022 !important; font-size: 16px; font-weight: 900; }
1157
- .metric-label { display: block; color: #5a6e65 !important; font-size: 10px; margin: 2px 0 3px; }
1158
- .metric-card strong { display: block; font-size: 15px; line-height: 1.2; }
1159
-
1160
- .result-section { margin: 12px 0; }
1161
- .section-title { color: #0b3022 !important; font-size: 13px; font-weight: 800; margin-bottom: 6px; }
1162
- .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
1163
-
1164
- /* Feed Table - Mint Green Header */
1165
- .feed-table {
1166
- width: 100%;
1167
- min-width: 280px;
1168
- border-collapse: separate;
1169
- border-spacing: 0;
1170
- background: #fff;
1171
- border: 1px solid #d6e0db;
1172
- border-radius: 8px;
1173
- overflow: hidden;
1174
  font-size: 11px;
 
 
1175
  }
1176
- .feed-table th {
1177
- background: #00a86b !important;
1178
- color: #fff !important;
1179
- padding: 8px 6px;
1180
- text-align: left;
1181
- font-weight: 800;
 
 
1182
  }
1183
- .feed-table th:not(:first-child), .feed-table td:not(:first-child) { text-align: right; }
1184
- .feed-table td { padding: 8px 6px; border-top: 1px solid #d6e0db; color: #0b3022 !important; white-space: nowrap; }
1185
- .feed-table .muted { color: #5a6e65 !important; font-size: 9px; }
1186
- .feed-table .total-row td { background: #eaf8f1 !important; font-weight: 900 !important; }
1187
-
1188
- /* Cost Card - Yellow */
1189
- .cost-card {
1190
- background: #fff7db !important;
1191
  border: 1px solid #EBC34A;
1192
- border-left: 4px solid #ffb800;
1193
- border-radius: 10px;
1194
- padding: 10px 12px;
1195
- margin: 12px 0;
1196
  }
1197
- .cost-main { color: #0b3022 !important; font-size: clamp(24px, 7vw, 34px); line-height: 1.1; font-weight: 900; }
1198
- .cost-note { color: #5a6e65 !important; font-size: 10px; margin-top: 4px; }
1199
-
1200
- /* Performance - Green */
1201
- .performance-grid {
1202
- background: #fff;
1203
- border: 1px solid #d6e0db;
1204
- border-radius: 8px;
1205
- overflow: hidden;
1206
  }
1207
- .performance-grid > div {
1208
  display: flex;
1209
  justify-content: space-between;
1210
- gap: 10px;
1211
- padding: 10px;
1212
- border-top: 1px solid #d6e0db;
1213
- font-size: 12px;
 
 
 
 
 
 
 
 
 
 
1214
  }
1215
- .performance-grid > div:first-child { border-top: 0; }
1216
- .performance-grid span { color: #5a6e65 !important; }
1217
- .performance-grid strong { white-space: nowrap; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1218
 
1219
- /* Share Buttons */
1220
- .share-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }
1221
- .share-btn {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1222
  display: block;
1223
  text-align: center;
1224
  text-decoration: none !important;
1225
- border-radius: 8px;
1226
- padding: 10px 8px;
1227
- font-size: 12px;
1228
- font-weight: 800;
1229
- min-height: 40px;
1230
- box-sizing: border-box;
1231
  color: #fff !important;
 
1232
  }
1233
- .share-btn.whatsapp { background: #25D366 !important; }
1234
- .share-btn.email { background: #EA4335 !important; }
1235
- .share-btn.facebook { background: #1877F2 !important; }
1236
- .share-btn.twitter { background: #000000 !important; }
1237
- .share-btn.whatsapp:hover { background: #1ebe5d !important; }
1238
- .share-btn.email:hover { background: #d93025 !important; }
1239
- .share-btn.facebook:hover { background: #166fe5 !important; }
1240
- .share-btn.twitter:hover { background: #222 !important; }
1241
-
1242
- .result-disclaimer {
1243
- background: #F1F6F3;
1244
- border: 1px solid #d6e0db;
1245
- border-radius: 8px;
1246
- padding: 8px 10px;
1247
- color: #5a6e65 !important;
1248
- font-size: 10px;
1249
- line-height: 1.4;
1250
- margin-top: 12px;
1251
  }
1252
 
1253
- /* Benchmark / Question Cards */
1254
  .benchmark-card {
1255
- background: #ffffff !important;
1256
- border: 1px solid #d6e0db;
1257
- border-radius: 12px;
1258
- padding: 12px;
1259
- margin: 12px 0 10px;
1260
  }
1261
- .benchmark-title { color: #0b3022 !important; font-size: 15px; font-weight: 850; }
1262
- .selected-breed-badge {
1263
- display: inline-flex;
1264
- align-items: center;
1265
- padding: 4px 12px;
1266
- border-radius: 999px;
1267
- background: #0b3022;
1268
- color: #fff !important;
1269
  font-size: 12px;
1270
- font-weight: 800;
1271
- }
1272
- .benchmark-help { color: #5a6e65 !important; font-size: 11px; margin: 6px 0 8px; }
1273
-
1274
- .question-card {
1275
- background: #ffffff !important;
1276
- border: 1px solid #d6e0db;
1277
- border-radius: 12px;
1278
- padding: 12px;
1279
- margin: 12px 0;
1280
  }
1281
- .question-heading { color: #0b3022 !important; font-size: 15px; font-weight: 850; }
1282
- .question-help { color: #5a6e65 !important; font-size: 11px; margin-bottom: 8px; }
1283
-
1284
- .admin-login {
1285
- background: #fff !important;
1286
- border: 1px solid #d6e0db !important;
1287
- border-left: 4px solid #ffb800 !important;
1288
- border-radius: 10px !important;
1289
- padding: 10px !important;
1290
  }
1291
- .footer-note { color: #5a6e65 !important; font-size: 11px; line-height: 1.5; }
1292
-
1293
- .benchmark-table { min-width: 0; font-size: 11px; }
1294
- .comparison-table { min-width: 520px; }
1295
-
1296
- @media (min-width: 768px) {
1297
- .gradio-container { padding: 0 20px 48px !important; }
1298
- .app-header { margin-left: -20px; margin-right: -20px; padding: 18px 24px; }
1299
- .buy-number { font-size: 76px; }
1300
- .metric-grid { grid-template-columns: repeat(4, 1fr); }
1301
  }
 
1302
 
1303
  @media (max-width: 480px) {
1304
- .gradio-container { padding-left: 6px !important; padding-right: 6px !important; }
1305
- .app-header { margin-left: -6px; margin-right: -6px; padding: 10px 12px; }
1306
- .app-header h1 { font-size: 18px; }
1307
- .form-section { padding: 10px !important; }
1308
- .metric-grid { gap: 6px; }
1309
- .metric-card { padding: 8px 6px; }
1310
- .metric-card strong { font-size: 13px; }
1311
- }
1312
-
1313
- /* Force all backgrounds to light */
1314
- * {
1315
- background-color: transparent !important;
1316
- }
1317
- body, .gradio-container, .gradio-container .block, .gradio-container .gr-group {
1318
- background: #f4f7f5 !important;
1319
- }
1320
- .form-section, .benchmark-card, .question-card, .status-card, .admin-login {
1321
- background: #ffffff !important;
1322
  }
1323
  """
1324
 
@@ -1339,78 +906,60 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1339
  """)
1340
 
1341
  gr.HTML("""
1342
- <div class="status-card">
1343
- <strong>βœ“ Simple & Fast</strong> β€” Enter your flock details, get instant feed estimates.
 
 
1344
  </div>
1345
  """)
1346
 
1347
  with gr.Column():
1348
- with gr.Group(elem_classes=["form-section"]):
1349
- gr.HTML('<div class="section-heading"><span class="step-number">1</span><span>πŸ” Breed & Birds</span></div>')
1350
- breed = gr.Dropdown(
1351
- choices=["Ross 308", "Cobb 500", "Arbor Acres"],
1352
- value="Ross 308",
1353
- label="Breed",
1354
- )
1355
- birds = gr.Dropdown(
1356
- choices=[25, 50, 75, 100, 150, 200, 500, 1000],
1357
- value=50,
1358
- label="Number of birds",
1359
- allow_custom_value=True,
1360
- )
1361
 
1362
- with gr.Group(elem_classes=["form-section"]):
1363
- gr.HTML('<div class="section-heading"><span class="step-number">2</span><span>πŸ“… Expected Age</span></div>')
1364
- with gr.Row(elem_classes=["age-row"]):
1365
  weeks = gr.Dropdown(choices=list(range(0, 21)), value=8, label="Weeks")
1366
  days = gr.Dropdown(choices=list(range(0, 7)), value=0, label="Days")
1367
 
1368
- with gr.Group(elem_classes=["form-section"]):
1369
- gr.HTML('<div class="section-heading"><span class="step-number">3</span><span>🌾 Feed Details</span></div>')
1370
- with gr.Row(elem_classes=["feed-row"]):
1371
  bag_size = gr.Dropdown(choices=[10, 20, 25, 50, 100], value=25, label="Bag size (kg)")
1372
  price = gr.Number(value=24000, label="Price per bag (₦)", minimum=0)
1373
 
1374
- with gr.Group(elem_classes=["form-section"]):
1375
- gr.HTML('<div class="section-heading"><span class="step-number">4</span><span>🌍 Your Location</span></div>')
1376
  detected_country = detect_country_from_env() or "NG"
1377
  country_choices = ["Auto-Detect"] + [code for code, _ in COUNTRY_LIST]
1378
- country_dropdown = gr.Dropdown(
1379
- choices=country_choices,
1380
- value="Auto-Detect",
1381
- label="Country",
1382
- )
1383
  country_code_state = gr.State(detected_country)
1384
 
1385
- with gr.Group(elem_classes=["form-section"]):
1386
- gr.HTML('<div class="section-heading"><span class="step-number">5</span><span>πŸ“§ Your Email</span></div>')
1387
  email = gr.Textbox(
1388
  label="Email address",
1389
  placeholder="farmer@example.com",
1390
  type="email",
1391
- elem_classes=["email-box"],
1392
  info="We save your calculation so you can access it later.",
1393
  )
1394
 
1395
- calculate_btn = gr.Button(
1396
- "πŸ” Calculate Feed Requirement",
1397
- variant="primary",
1398
- size="lg",
1399
- elem_classes=["calculate-btn"],
1400
- )
1401
  save_status = gr.Markdown("")
1402
 
1403
  # Results
1404
  gr.Markdown("## πŸ“Š Your Feed Result")
1405
  result = gr.HTML(
1406
  value="""
1407
- <div class="results-shell">
1408
- <div class="buy-card">
1409
- <div class="eyebrow">🎯 YOUR FEED RESULT</div>
1410
- <div class="buy-label">BUY</div>
1411
- <div class="buy-number">β€”</div>
1412
- <div class="buy-unit">BAGS</div>
1413
- <div class="buy-note">Enter your flock details above to calculate.</div>
1414
  </div>
1415
  </div>
1416
  """,
@@ -1420,15 +969,13 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1420
  # Reference / Benchmarks
1421
  with gr.Group(elem_classes=["benchmark-card"]):
1422
  with gr.Row():
1423
- gr.HTML('<div class="benchmark-title">πŸ“Š Selected Breed Benchmark</div>')
1424
- selected_breed_badge = gr.HTML('<span class="selected-breed-badge">Ross 308</span>')
1425
 
1426
  with gr.Accordion("πŸ“Š Weekly benchmark data", open=False):
1427
- gr.HTML('<p class="benchmark-help">Weekly benchmark data used by the calculator.</p>')
1428
  benchmark_table = gr.HTML(value=get_benchmark_html("Ross 308"))
1429
 
1430
  with gr.Accordion("πŸ” Breed Comparison", open=False):
1431
- gr.Markdown("Compare Ross 308, Cobb 500 and Arbor Acres at the same age checkpoints.")
1432
  comparison_table = gr.HTML(value=get_comparison_html())
1433
 
1434
  with gr.Accordion("πŸ” Common Broiler Breeds in Nigeria", open=False):
@@ -1451,20 +998,15 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1451
  """)
1452
 
1453
  # Ask a Question
1454
- with gr.Group(elem_classes=["question-card"]):
1455
- gr.HTML('<div class="question-heading">❓ Ask a Question</div>')
1456
- gr.HTML('<div class="question-help">Ask a simple poultry-feed question.</div>')
1457
- question = gr.Textbox(
1458
- label="Your question",
1459
- placeholder="How many bags for 100 broilers for 6 weeks?",
1460
- lines=2,
1461
- )
1462
  ask_btn = gr.Button("πŸ’¬ Get Answer", variant="secondary")
1463
  question_answer = gr.Markdown("")
1464
 
1465
- # Footer
1466
  gr.HTML("""
1467
- <div class="footer-note" style="margin-top:14px; text-align:center;">
1468
  πŸ” <strong>How Much Feed?</strong> β€” A simple broiler feed planning tool.<br>
1469
  Feed estimates are guides and should be adjusted to actual flock performance.
1470
  </div>
@@ -1472,15 +1014,12 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1472
 
1473
  # Admin (Collapsed)
1474
  with gr.Accordion("πŸ”’ Admin", open=False):
1475
- with gr.Group(elem_classes=["admin-login"]):
1476
- admin_password_input = gr.Textbox(
1477
- label="Admin Password", placeholder="Enter password...", type="password"
1478
- )
1479
  with gr.Row():
1480
  login_btn = gr.Button("Login", variant="primary")
1481
  logout_btn = gr.Button("Logout", variant="secondary", visible=False)
1482
  admin_output = gr.Markdown("πŸ”’ Enter the admin password to view analytics.")
1483
-
1484
  with gr.Row():
1485
  export_leads_btn = gr.Button("πŸ“₯ Export Leads (CSV)", variant="secondary")
1486
  export_leads_output = gr.File(label="Leads CSV")
@@ -1492,72 +1031,120 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1492
  # EVENTS
1493
  # ============================================================
1494
 
1495
- breed.change(
1496
- fn=update_benchmark,
1497
- inputs=[breed],
1498
- outputs=[benchmark_table],
1499
- )
1500
-
1501
- breed.change(
1502
- fn=update_selected_breed_badge,
1503
- inputs=[breed],
1504
- outputs=[selected_breed_badge],
1505
- )
1506
-
1507
- country_dropdown.change(
1508
- fn=on_country_change_handler,
1509
- inputs=[country_dropdown],
1510
- outputs=[country_code_state, price],
1511
- )
1512
 
1513
  def calculate_wrapper(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request: gr.Request):
1514
  if not country_code_state:
1515
  country_code_state = "NG"
1516
- return calculate_feed(
1517
- birds, weeks, days, bag_size, price_per_bag,
1518
- email, country_code_state, breed, request
1519
- )
1520
-
1521
- calculate_btn.click(
1522
- fn=calculate_wrapper,
1523
- inputs=[birds, weeks, days, bag_size, price, email, country_code_state, breed],
1524
- outputs=[result, save_status],
1525
- )
1526
 
1527
- ask_btn.click(
1528
- fn=answer_farmer_question,
1529
- inputs=[question, email, breed],
1530
- outputs=[question_answer],
1531
- )
1532
 
1533
- login_btn.click(
1534
- fn=admin_login,
1535
- inputs=[admin_password_input],
1536
- outputs=[admin_output, admin_password_input, logout_btn],
1537
- )
1538
 
1539
- logout_btn.click(
1540
- fn=logout_admin,
1541
- inputs=[],
1542
- outputs=[admin_output, admin_password_input, logout_btn],
1543
- )
1544
 
1545
- export_leads_btn.click(
1546
- fn=export_leads_wrapper,
1547
- inputs=[],
1548
- outputs=[export_leads_output],
1549
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1550
 
1551
- export_calc_btn.click(
1552
- fn=export_calculations_wrapper,
1553
- inputs=[],
1554
- outputs=[export_calc_output],
1555
- )
1556
 
1557
- def track_visit_async(request: gr.Request):
1558
- threading.Thread(target=track_visit_async, args=(request,), daemon=True).start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1559
 
1560
- demo.load(fn=track_visit_async, inputs=None, outputs=None)
 
 
 
 
 
 
 
 
1561
 
1562
 
1563
  # ============================================================
@@ -1566,7 +1153,6 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1566
 
1567
  if __name__ == "__main__":
1568
  port = int(os.environ.get("PORT", 7860))
1569
-
1570
  demo.launch(
1571
  server_name="0.0.0.0",
1572
  server_port=port,
 
28
 
29
  STARTER_END_DAY = 21
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # ============================================================
32
  # VERIFIED BREED DATA - From Official PDFs
33
  # ============================================================
 
117
  return feed_data[56]
118
 
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  # ============================================================
121
  # CURRENCY DATA
122
  # ============================================================
 
182
 
183
 
184
  # ============================================================
185
+ # SHARE LINKS GENERATOR
186
  # ============================================================
187
 
188
+ def get_share_links(app_url="https://howmuchfeed.com"):
189
+ share_text = f"πŸ” Calculate your poultry feed needs at {app_url}"
190
+ encoded = urllib.parse.quote(share_text)
191
+ return {
192
+ "whatsapp": f"https://wa.me/?text={encoded}",
193
+ "email": f"mailto:?subject=How Much Feed? - Poultry Feed Calculator&body={encoded}",
194
+ "facebook": f"https://www.facebook.com/sharer/sharer.php?u={urllib.parse.quote(app_url)}",
195
+ "twitter": f"https://twitter.com/intent/tweet?text={encoded}",
196
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
 
199
  # ============================================================
200
+ # FEED CALCULATION - PREMIUM DESIGN
201
  # ============================================================
202
 
203
+ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code, breed, request: gr.Request):
204
+ try:
205
+ birds = int(birds)
206
+ weeks = int(weeks)
207
+ days = int(days)
208
+ bag_size = float(bag_size)
209
+ price_per_bag = float(price_per_bag)
210
+ except Exception:
211
+ return "⚠️ Please enter valid numbers.", ""
212
 
213
+ if birds <= 0:
214
+ return "⚠️ Number of birds must be greater than zero.", ""
215
+ if weeks < 0:
216
+ return "⚠️ Weeks cannot be negative.", ""
217
+ if days < 0 or days > 6:
218
+ return "⚠️ Days must be between 0 and 6.", ""
219
+ if bag_size <= 0:
220
+ return "⚠️ Feed bag size must be greater than zero.", ""
221
+ if price_per_bag < 0:
222
+ return "⚠️ Price per bag cannot be negative.", ""
223
 
 
224
  if not valid_email(email):
225
+ return "πŸ“§ **Email required**\n\nPlease enter your email address before calculating.", ""
 
 
 
 
 
 
 
 
 
226
 
227
+ country_name, symbol, currency_code = get_country_info(country_code)
228
+ currency_symbol = symbol
 
 
229
 
230
+ total_days = (weeks * 7) + days
231
+ total_feed_per_bird = interpolate_feed(total_days, breed)
232
+ total_feed_kg = birds * total_feed_per_bird
233
 
234
+ starter_days = min(total_days, STARTER_END_DAY)
235
+ starter_per_bird = interpolate_feed(starter_days, breed)
236
+ starter_kg = birds * starter_per_bird
237
+ finisher_kg = max(total_feed_kg - starter_kg, 0)
238
 
239
+ starter_bags = starter_kg / bag_size
240
+ finisher_bags = finisher_kg / bag_size
241
+ total_bags = total_feed_kg / bag_size
242
+ rounded_bags = math.ceil(total_bags)
243
+ total_cost = rounded_bags * price_per_bag
 
 
 
244
 
245
+ expected_weight = interpolate_weight(total_days, breed)
246
+ flock_weight = expected_weight * birds
 
 
247
 
248
+ share_links = get_share_links()
249
+ starter_bags_display = f"{starter_bags:.1f}"
250
+ finisher_bags_display = f"{finisher_bags:.1f}"
251
+ age_text = f"{weeks} week{'s' if weeks != 1 else ''}"
252
+ if days:
253
+ age_text += f" + {days} day{'s' if days != 1 else ''}"
254
 
255
+ html = f"""
256
+ <div class="result-premium">
257
+
258
+ <!-- HERO SECTION -->
259
+ <div class="result-hero">
260
+ <div class="hero-badge">⚑ QUICK ANSWER</div>
261
+ <div class="hero-number">{rounded_bags}</div>
262
+ <div class="hero-label">BAGS OF FEED</div>
263
+ <div class="hero-sub">
264
+ To raise <strong>{birds:,}</strong> {breed} broilers for <strong>{age_text}</strong>
265
+ </div>
266
+ <div class="hero-cta">
267
+ <span class="cta-label">Buy</span>
268
+ <span class="cta-number">{rounded_bags}</span>
269
+ <span class="cta-label">bags (rounded up)</span>
270
+ </div>
271
+ </div>
272
 
273
+ <!-- METRIC CARDS -->
274
+ <div class="metric-grid-premium">
275
+ <div class="metric-premium feed-metric">
276
+ <div class="metric-icon">🌾</div>
277
+ <div class="metric-value">{total_feed_kg:,.1f} kg</div>
278
+ <div class="metric-label">Total Feed Needed</div>
279
+ </div>
280
+ <div class="metric-premium bag-metric">
281
+ <div class="metric-icon">πŸ“¦</div>
282
+ <div class="metric-value">{total_bags:.1f}</div>
283
+ <div class="metric-label">Exact Bags</div>
284
+ </div>
285
+ <div class="metric-premium cost-metric">
286
+ <div class="metric-icon">πŸ’°</div>
287
+ <div class="metric-value">{currency_symbol}{total_cost:,.0f}</div>
288
+ <div class="metric-label">Estimated Cost</div>
289
+ </div>
290
+ <div class="metric-premium time-metric">
291
+ <div class="metric-icon">⏱️</div>
292
+ <div class="metric-value">{total_days}</div>
293
+ <div class="metric-label">Days to Grow</div>
294
+ </div>
295
+ </div>
296
 
297
+ <!-- FEED BREAKDOWN -->
298
+ <div class="breakdown-section">
299
+ <div class="breakdown-title">🌾 Feed Breakdown</div>
300
+ <div class="breakdown-grid">
301
+ <div class="breakdown-item starter">
302
+ <div class="breakdown-label">🌱 Starter</div>
303
+ <div class="breakdown-value">{starter_kg:,.1f} kg</div>
304
+ <div class="breakdown-bags">{starter_bags_display} bags</div>
305
+ </div>
306
+ <div class="breakdown-item finisher">
307
+ <div class="breakdown-label">🌽 Finisher</div>
308
+ <div class="breakdown-value">{finisher_kg:,.1f} kg</div>
309
+ <div class="breakdown-bags">{finisher_bags_display} bags</div>
310
+ </div>
311
+ <div class="breakdown-item total">
312
+ <div class="breakdown-label">πŸ“Š TOTAL</div>
313
+ <div class="breakdown-value">{total_feed_kg:,.1f} kg</div>
314
+ <div class="breakdown-bags">{total_bags:.1f} bags</div>
315
+ </div>
316
+ </div>
317
+ </div>
318
 
319
+ <!-- COST BREAKDOWN -->
320
+ <div class="cost-breakdown">
321
+ <div class="cost-title">πŸ’° Cost Breakdown</div>
322
+ <div class="cost-row">
323
+ <span>Feed bags</span>
324
+ <span><strong>{rounded_bags} bags</strong></span>
325
+ </div>
326
+ <div class="cost-row">
327
+ <span>Price per bag</span>
328
+ <span><strong>{currency_symbol}{price_per_bag:,.0f}</strong></span>
329
+ </div>
330
+ <div class="cost-row total">
331
+ <span><strong>Total Feed Cost</strong></span>
332
+ <span><strong>{currency_symbol}{total_cost:,.0f}</strong></span>
333
+ </div>
334
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
+ <!-- PERFORMANCE -->
337
+ <div class="performance-section">
338
+ <div class="perf-title">πŸ“ˆ Expected Performance</div>
339
+ <div class="perf-grid">
340
+ <div>
341
+ <span>Body weight / bird</span>
342
+ <strong>{expected_weight:.3f} kg</strong>
343
+ </div>
344
+ <div>
345
+ <span>Flock live weight</span>
346
+ <strong>{flock_weight:,.1f} kg</strong>
347
+ </div>
348
+ </div>
349
+ </div>
350
 
351
+ <!-- SHARE -->
352
+ <div class="share-section-premium">
353
+ <div class="share-title">πŸ“€ Share Results</div>
354
+ <div class="share-grid-premium">
355
+ <a class="share-btn-premium whatsapp" href="{share_links['whatsapp']}" target="_blank">πŸ’¬ WhatsApp</a>
356
+ <a class="share-btn-premium email" href="{share_links['email']}" target="_blank">πŸ“§ Email</a>
357
+ <a class="share-btn-premium facebook" href="{share_links['facebook']}" target="_blank">πŸ“˜ Facebook</a>
358
+ <a class="share-btn-premium twitter" href="{share_links['twitter']}" target="_blank">🐦 X</a>
359
+ </div>
360
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
+ <div class="result-footnote">
363
+ ⚠️ Estimate based on {breed} benchmark. Results may vary.
364
+ </div>
365
+ </div>
366
+ """
367
 
368
+ return html, f"βœ… Saved for {email.strip().lower()}."
 
 
369
 
 
 
 
 
 
 
 
 
 
370
 
371
+ def valid_email(email):
372
+ if not email:
373
+ return False
374
+ email = email.strip()
375
+ pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
376
+ return bool(re.match(pattern, email))
377
 
 
 
 
378
 
379
  def answer_farmer_question(question, email, breed):
380
  if not question or not question.strip():
381
  return "Please type or speak your question first."
382
 
 
 
383
  q = question.lower().strip()
384
 
385
  if "bag" in q and ("feed" in q or "birds" in q):
 
443
  **β€œ{question.strip()}”**
444
 
445
  Your question has been saved.
 
 
446
  """
447
 
448
 
449
+ def get_benchmark_html(breed):
450
+ weight_data = get_breed_weight_data(breed)
451
+ feed_data = get_breed_feed_data(breed)
452
+ days = [7, 14, 21, 28, 35, 42, 49, 56]
453
+ rows = []
454
+ for day in days:
455
+ weight = weight_data.get(day, 0)
456
+ feed = feed_data.get(day, 0)
457
+ fcr = feed / weight if weight > 0 else 0
458
+ rows.append(f"<tr><td>{day}</td><td>{weight:.3f} kg</td><td>{feed:.3f} kg</td><td>{fcr:.3f}</td></tr>")
459
+ return f"""
460
+ <div class="table-wrap"><table class="feed-table">
461
+ <thead><tr><th>Days</th><th>Body Weight</th><th>Cum Feed</th><th>FCR</th></tr></thead>
462
+ <tbody>{''.join(rows)}</tbody>
463
+ </table></div>
464
+ """
465
 
466
 
467
+ def get_comparison_html():
468
+ breeds = ["Ross 308", "Cobb 500", "Arbor Acres"]
469
+ days = [7, 14, 21, 28, 35, 42, 49, 56]
470
+ rows = []
471
+ for day in days:
472
+ cells = [f'<td>{day}</td>']
473
+ for breed in breeds:
474
+ weight_data = get_breed_weight_data(breed)
475
+ feed_data = get_breed_feed_data(breed)
476
+ weight = weight_data.get(day, 0)
477
+ feed = feed_data.get(day, 0)
478
+ cells.append(f'<td>{weight:.3f} kg</td>')
479
+ cells.append(f'<td>{feed:.3f} kg</td>')
480
+ rows.append('<tr>' + ''.join(cells) + '</tr>')
481
+ headers = ['Days'] + [f'{b} Wt' for b in breeds] + [f'{b} Feed' for b in breeds]
482
+ head = ''.join(f'<th>{h}</th>' for h in headers)
483
+ return f"""
484
+ <div class="table-wrap"><table class="feed-table comparison-table">
485
+ <thead><tr>{head}</tr></thead>
486
+ <tbody>{''.join(rows)}</tbody>
487
+ </table></div>
488
+ """
489
 
490
 
491
  # ============================================================
492
+ # CSS - PREMIUM DESIGN
493
  # ============================================================
494
 
495
+ CSS = """
496
+ /* ============================================================
497
+ PREMIUM FARM & FIELD DESIGN
498
+ ============================================================ */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
+ :root {
501
+ --mint: #00A86B;
502
+ --mint-dark: #0B3022;
503
+ --mint-soft: #E8F5EE;
504
+ --yellow: #FFB800;
505
+ --yellow-soft: #FFF8E1;
506
+ --red: #EA4335;
507
+ --bg: #F5F7F6;
508
+ --surface: #FFFFFF;
509
+ --border: #E2E8E6;
510
+ --text: #1A2E26;
511
+ --text-muted: #5A6E65;
512
+ }
513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  html, body, .gradio-container, .gradio-container * {
515
+ background: var(--bg) !important;
516
+ color: var(--text) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  color-scheme: light !important;
518
  }
519
 
520
  .gradio-container {
521
+ max-width: 1000px !important;
 
522
  margin: 0 auto !important;
523
+ padding: 0 12px 32px !important;
524
+ }
525
+
526
+ /* Remove HF header */
527
+ #huggingface-space-header, .hf-header, [data-testid="header"] {
528
+ display: none !important;
529
  }
530
 
531
+ /* APP HEADER */
532
  .app-header {
533
+ background: var(--mint-dark) !important;
534
+ border-radius: 0 0 20px 20px;
535
+ padding: 18px 20px 20px;
536
+ margin: 0 -12px 16px;
 
537
  }
538
+ .app-header .brand-row { display: flex; align-items: center; gap: 10px; }
539
+ .app-header .brand-icon { font-size: 30px; }
540
+ .app-header h1 { color: #fff !important; margin: 0; font-size: 26px; font-weight: 800; letter-spacing: -0.5px; }
541
+ .app-header p { color: rgba(255,255,255,.85) !important; margin: 4px 0 0 40px; font-size: 14px; }
542
+
543
+ /* STATUS BAR */
544
+ .status-bar {
545
+ background: var(--surface) !important;
546
+ border: 1px solid var(--border);
547
+ border-radius: 12px;
548
+ padding: 10px 14px;
549
+ margin-bottom: 14px;
550
+ display: flex;
551
+ align-items: center;
552
+ gap: 12px;
553
+ flex-wrap: wrap;
554
  }
555
+ .status-bar .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--mint); }
556
+ .status-bar strong { color: var(--text) !important; }
557
+ .status-bar .muted { color: var(--text-muted) !important; font-size: 13px; }
558
+
559
+ /* FORM SECTIONS */
560
+ .form-card {
561
+ background: var(--surface) !important;
562
+ border: 1px solid var(--border);
563
+ border-radius: 14px;
564
+ padding: 14px 16px;
565
+ margin-bottom: 10px;
566
  }
567
+ .form-card .label {
568
+ font-weight: 700;
569
+ font-size: 14px;
570
+ color: var(--text);
571
+ margin-bottom: 6px;
572
  display: flex;
573
  align-items: center;
574
  gap: 8px;
 
 
 
 
575
  }
576
+ .form-card .label .num {
577
+ background: var(--mint-dark);
578
+ color: #fff;
579
  border-radius: 50%;
580
+ width: 22px;
581
+ height: 22px;
582
  display: inline-flex;
583
  align-items: center;
584
  justify-content: center;
585
+ font-size: 11px;
 
 
586
  }
587
 
588
+ .gradio-container input, .gradio-container select, .gradio-container textarea {
589
  background: #fff !important;
590
+ border: 1px solid var(--border) !important;
591
+ border-radius: 10px !important;
 
592
  min-height: 44px !important;
593
  font-size: 15px !important;
594
+ color: var(--text) !important;
595
  }
596
+ .gradio-container input:focus, .gradio-container select:focus {
597
+ border-color: var(--mint) !important;
598
+ box-shadow: 0 0 0 3px rgba(0,168,107,.15) !important;
599
  }
600
+ .gradio-container label { color: var(--text) !important; font-weight: 600 !important; }
 
 
 
601
 
602
+ .calc-btn button {
603
  width: 100% !important;
604
+ min-height: 52px !important;
605
+ background: var(--mint) !important;
 
 
606
  color: #fff !important;
607
+ border: 0 !important;
608
+ border-radius: 12px !important;
609
+ font-size: 16px !important;
610
  font-weight: 800 !important;
611
+ box-shadow: 0 4px 14px rgba(0,168,107,.25) !important;
612
  }
613
+ .calc-btn button:hover { background: #00955E !important; }
614
 
615
  /* ============================================================
616
+ PREMIUM RESULTS
617
  ============================================================ */
 
618
 
619
+ .result-premium {
620
+ max-width: 100%;
621
+ animation: fadeUp 0.4s ease;
622
+ }
623
+
624
+ @keyframes fadeUp {
625
+ from { opacity: 0; transform: translateY(12px); }
626
+ to { opacity: 1; transform: translateY(0); }
627
+ }
628
+
629
+ /* HERO */
630
+ .result-hero {
631
+ background: linear-gradient(145deg, var(--mint-dark) 0%, #1A4A32 100%);
632
+ border-radius: 18px;
633
+ padding: 22px 18px 18px;
634
  text-align: center;
635
+ margin-bottom: 16px;
636
+ position: relative;
637
+ overflow: hidden;
638
  }
639
+ .result-hero::after {
640
+ content: "πŸ”";
641
+ position: absolute;
642
+ right: -10px;
643
+ bottom: -10px;
644
+ font-size: 80px;
645
+ opacity: 0.08;
646
+ }
647
+ .hero-badge {
648
+ display: inline-block;
649
+ background: rgba(255,255,255,0.12);
650
+ color: #fff;
651
+ font-size: 10px;
652
+ font-weight: 700;
653
+ letter-spacing: 1px;
654
+ text-transform: uppercase;
655
+ padding: 3px 14px;
656
+ border-radius: 20px;
657
+ }
658
+ .hero-number {
659
+ color: #fff;
660
+ font-size: clamp(56px, 18vw, 84px);
661
+ font-weight: 900;
662
+ line-height: 1;
663
+ margin: 4px 0 2px;
664
+ letter-spacing: -2px;
665
+ }
666
+ .hero-label {
667
+ color: rgba(255,255,255,0.7);
668
+ font-size: 14px;
669
+ font-weight: 600;
670
+ letter-spacing: 2px;
671
+ }
672
+ .hero-sub {
673
+ color: rgba(255,255,255,0.85);
674
+ font-size: 14px;
675
+ margin: 8px 0 12px;
676
  }
677
+ .hero-sub strong { color: #fff !important; }
678
+ .hero-cta {
679
+ display: inline-flex;
680
+ align-items: center;
681
+ gap: 8px;
682
+ background: rgba(255,255,255,0.1);
683
+ border: 1px solid rgba(255,255,255,0.15);
684
+ border-radius: 30px;
685
+ padding: 6px 16px 6px 20px;
686
+ }
687
+ .cta-label { color: rgba(255,255,255,0.7); font-size: 13px; }
688
+ .cta-number { color: var(--yellow); font-size: 22px; font-weight: 800; }
689
 
690
+ /* METRIC GRID */
691
+ .metric-grid-premium {
692
  display: grid;
693
  grid-template-columns: repeat(2, 1fr);
694
+ gap: 10px;
695
+ margin-bottom: 16px;
696
+ }
697
+ .metric-premium {
698
+ background: var(--surface);
699
+ border-radius: 14px;
700
+ padding: 14px 12px;
701
+ text-align: center;
702
+ border: 1px solid var(--border);
703
+ }
704
+ .metric-premium .metric-icon { font-size: 20px; }
705
+ .metric-premium .metric-value {
706
+ font-size: 22px;
707
+ font-weight: 800;
708
+ margin: 2px 0;
709
+ }
710
+ .metric-premium .metric-label {
711
+ font-size: 11px;
712
+ color: var(--text-muted);
713
+ font-weight: 500;
714
+ }
715
+ .feed-metric .metric-value { color: var(--mint); }
716
+ .bag-metric .metric-value { color: var(--yellow); }
717
+ .cost-metric .metric-value { color: var(--red); }
718
+ .time-metric .metric-value { color: var(--mint-dark); }
719
+
720
+ /* BREAKDOWN */
721
+ .breakdown-section {
722
+ background: var(--surface);
723
+ border-radius: 14px;
724
+ padding: 14px 14px 10px;
725
+ border: 1px solid var(--border);
726
+ margin-bottom: 14px;
727
+ }
728
+ .breakdown-title {
729
+ font-weight: 700;
730
+ font-size: 14px;
731
+ margin-bottom: 10px;
732
+ color: var(--text);
733
+ }
734
+ .breakdown-grid {
735
+ display: grid;
736
+ grid-template-columns: 1fr 1fr 1fr;
737
  gap: 8px;
 
738
  }
739
+ .breakdown-item {
740
+ text-align: center;
741
+ padding: 10px 4px;
742
  border-radius: 10px;
 
743
  }
744
+ .breakdown-item.starter { background: #E3F2ED; }
745
+ .breakdown-item.finisher { background: #F5F0E8; }
746
+ .breakdown-item.total { background: var(--mint-soft); }
747
+ .breakdown-item .breakdown-label {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
  font-size: 11px;
749
+ font-weight: 600;
750
+ color: var(--text-muted);
751
  }
752
+ .breakdown-item .breakdown-value {
753
+ font-size: 16px;
754
+ font-weight: 700;
755
+ color: var(--text);
756
+ }
757
+ .breakdown-item .breakdown-bags {
758
+ font-size: 12px;
759
+ color: var(--text-muted);
760
  }
761
+
762
+ /* COST BREAKDOWN */
763
+ .cost-breakdown {
764
+ background: var(--yellow-soft);
765
+ border-radius: 14px;
766
+ padding: 14px 16px;
 
 
767
  border: 1px solid #EBC34A;
768
+ margin-bottom: 14px;
 
 
 
769
  }
770
+ .cost-title {
771
+ font-weight: 700;
772
+ font-size: 14px;
773
+ margin-bottom: 8px;
 
 
 
 
 
774
  }
775
+ .cost-row {
776
  display: flex;
777
  justify-content: space-between;
778
+ padding: 4px 0;
779
+ font-size: 14px;
780
+ border-bottom: 1px solid rgba(0,0,0,0.04);
781
+ }
782
+ .cost-row:last-child { border-bottom: 0; }
783
+ .cost-row.total { border-top: 2px solid #EBC34A; padding-top: 8px; margin-top: 4px; }
784
+
785
+ /* PERFORMANCE */
786
+ .performance-section {
787
+ background: var(--surface);
788
+ border-radius: 14px;
789
+ padding: 14px 16px;
790
+ border: 1px solid var(--border);
791
+ margin-bottom: 14px;
792
  }
793
+ .perf-title {
794
+ font-weight: 700;
795
+ font-size: 14px;
796
+ margin-bottom: 8px;
797
+ }
798
+ .perf-grid {
799
+ display: grid;
800
+ grid-template-columns: 1fr 1fr;
801
+ gap: 8px;
802
+ }
803
+ .perf-grid div {
804
+ display: flex;
805
+ justify-content: space-between;
806
+ padding: 6px 0;
807
+ font-size: 13px;
808
+ border-bottom: 1px solid var(--border);
809
+ }
810
+ .perf-grid div:last-child { border-bottom: 0; }
811
+ .perf-grid span { color: var(--text-muted); }
812
+ .perf-grid strong { color: var(--mint-dark); }
813
 
814
+ /* SHARE */
815
+ .share-section-premium {
816
+ background: var(--surface);
817
+ border-radius: 14px;
818
+ padding: 14px 16px;
819
+ border: 1px solid var(--border);
820
+ margin-bottom: 10px;
821
+ }
822
+ .share-title {
823
+ font-weight: 700;
824
+ font-size: 14px;
825
+ margin-bottom: 8px;
826
+ }
827
+ .share-grid-premium {
828
+ display: grid;
829
+ grid-template-columns: repeat(2, 1fr);
830
+ gap: 8px;
831
+ }
832
+ .share-btn-premium {
833
  display: block;
834
  text-align: center;
835
  text-decoration: none !important;
836
+ border-radius: 10px;
837
+ padding: 10px;
838
+ font-size: 13px;
839
+ font-weight: 700;
 
 
840
  color: #fff !important;
841
+ transition: transform 0.15s;
842
  }
843
+ .share-btn-premium:hover { transform: scale(1.02); }
844
+ .share-btn-premium.whatsapp { background: #25D366; }
845
+ .share-btn-premium.email { background: #EA4335; }
846
+ .share-btn-premium.facebook { background: #1877F2; }
847
+ .share-btn-premium.twitter { background: #000; }
848
+
849
+ .result-footnote {
850
+ font-size: 11px;
851
+ color: var(--text-muted);
852
+ text-align: center;
853
+ padding-top: 8px;
854
+ border-top: 1px solid var(--border);
 
 
 
 
 
 
855
  }
856
 
857
+ /* BENCHMARK */
858
  .benchmark-card {
859
+ background: var(--surface) !important;
860
+ border: 1px solid var(--border);
861
+ border-radius: 14px;
862
+ padding: 14px;
863
+ margin: 12px 0;
864
  }
865
+ .feed-table {
866
+ width: 100%;
867
+ border-collapse: collapse;
 
 
 
 
 
868
  font-size: 12px;
 
 
 
 
 
 
 
 
 
 
869
  }
870
+ .feed-table th {
871
+ background: var(--mint-dark) !important;
872
+ color: #fff !important;
873
+ padding: 8px 10px;
874
+ text-align: left;
 
 
 
 
875
  }
876
+ .feed-table td {
877
+ padding: 7px 10px;
878
+ border-bottom: 1px solid var(--border);
879
+ color: var(--text) !important;
 
 
 
 
 
 
880
  }
881
+ .feed-table .total-row td { background: var(--mint-soft) !important; font-weight: 700; }
882
 
883
  @media (max-width: 480px) {
884
+ .metric-grid-premium { grid-template-columns: repeat(2, 1fr); gap: 8px; }
885
+ .breakdown-grid { grid-template-columns: 1fr; gap: 4px; }
886
+ .perf-grid { grid-template-columns: 1fr; }
887
+ .share-grid-premium { grid-template-columns: repeat(2, 1fr); }
888
+ .hero-number { font-size: 48px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
889
  }
890
  """
891
 
 
906
  """)
907
 
908
  gr.HTML("""
909
+ <div class="status-bar">
910
+ <span class="dot"></span>
911
+ <strong>Simple & Fast</strong>
912
+ <span class="muted">β€” Enter your flock details, get instant feed estimates.</span>
913
  </div>
914
  """)
915
 
916
  with gr.Column():
917
+ with gr.Group(elem_classes=["form-card"]):
918
+ gr.HTML('<div class="label"><span class="num">1</span> πŸ” Breed & Birds</div>')
919
+ breed = gr.Dropdown(choices=["Ross 308", "Cobb 500", "Arbor Acres"], value="Ross 308", label="Breed")
920
+ birds = gr.Dropdown(choices=[25, 50, 75, 100, 150, 200, 500, 1000], value=50, label="Number of birds", allow_custom_value=True)
 
 
 
 
 
 
 
 
 
921
 
922
+ with gr.Group(elem_classes=["form-card"]):
923
+ gr.HTML('<div class="label"><span class="num">2</span> πŸ“… Expected Age</div>')
924
+ with gr.Row():
925
  weeks = gr.Dropdown(choices=list(range(0, 21)), value=8, label="Weeks")
926
  days = gr.Dropdown(choices=list(range(0, 7)), value=0, label="Days")
927
 
928
+ with gr.Group(elem_classes=["form-card"]):
929
+ gr.HTML('<div class="label"><span class="num">3</span> 🌾 Feed Details</div>')
930
+ with gr.Row():
931
  bag_size = gr.Dropdown(choices=[10, 20, 25, 50, 100], value=25, label="Bag size (kg)")
932
  price = gr.Number(value=24000, label="Price per bag (₦)", minimum=0)
933
 
934
+ with gr.Group(elem_classes=["form-card"]):
935
+ gr.HTML('<div class="label"><span class="num">4</span> 🌍 Your Location</div>')
936
  detected_country = detect_country_from_env() or "NG"
937
  country_choices = ["Auto-Detect"] + [code for code, _ in COUNTRY_LIST]
938
+ country_dropdown = gr.Dropdown(choices=country_choices, value="Auto-Detect", label="Country")
 
 
 
 
939
  country_code_state = gr.State(detected_country)
940
 
941
+ with gr.Group(elem_classes=["form-card"]):
942
+ gr.HTML('<div class="label"><span class="num">5</span> πŸ“§ Your Email</div>')
943
  email = gr.Textbox(
944
  label="Email address",
945
  placeholder="farmer@example.com",
946
  type="email",
 
947
  info="We save your calculation so you can access it later.",
948
  )
949
 
950
+ calculate_btn = gr.Button("πŸ” Calculate Feed Requirement", variant="primary", size="lg", elem_classes=["calc-btn"])
 
 
 
 
 
951
  save_status = gr.Markdown("")
952
 
953
  # Results
954
  gr.Markdown("## πŸ“Š Your Feed Result")
955
  result = gr.HTML(
956
  value="""
957
+ <div class="result-premium">
958
+ <div class="result-hero">
959
+ <div class="hero-badge">⚑ QUICK ANSWER</div>
960
+ <div class="hero-number">β€”</div>
961
+ <div class="hero-label">BAGS OF FEED</div>
962
+ <div class="hero-sub">Enter your flock details to calculate</div>
 
963
  </div>
964
  </div>
965
  """,
 
969
  # Reference / Benchmarks
970
  with gr.Group(elem_classes=["benchmark-card"]):
971
  with gr.Row():
972
+ gr.HTML('<div style="font-weight:700;font-size:15px;">πŸ“Š Selected Breed Benchmark</div>')
973
+ selected_breed_badge = gr.HTML('<span style="background:#0B3022;color:#fff;padding:2px 14px;border-radius:20px;font-size:12px;font-weight:700;">Ross 308</span>')
974
 
975
  with gr.Accordion("πŸ“Š Weekly benchmark data", open=False):
 
976
  benchmark_table = gr.HTML(value=get_benchmark_html("Ross 308"))
977
 
978
  with gr.Accordion("πŸ” Breed Comparison", open=False):
 
979
  comparison_table = gr.HTML(value=get_comparison_html())
980
 
981
  with gr.Accordion("πŸ” Common Broiler Breeds in Nigeria", open=False):
 
998
  """)
999
 
1000
  # Ask a Question
1001
+ with gr.Group(elem_classes=["benchmark-card"]):
1002
+ gr.HTML('<div style="font-weight:700;font-size:15px;margin-bottom:4px;">❓ Ask a Question</div>')
1003
+ gr.HTML('<div style="color:#5A6E65;font-size:12px;margin-bottom:8px;">Ask a simple poultry-feed question.</div>')
1004
+ question = gr.Textbox(label="Your question", placeholder="How many bags for 100 broilers for 6 weeks?", lines=2)
 
 
 
 
1005
  ask_btn = gr.Button("πŸ’¬ Get Answer", variant="secondary")
1006
  question_answer = gr.Markdown("")
1007
 
 
1008
  gr.HTML("""
1009
+ <div style="text-align:center;font-size:11px;color:#5A6E65;margin-top:16px;border-top:1px solid #E2E8E6;padding-top:12px;">
1010
  πŸ” <strong>How Much Feed?</strong> β€” A simple broiler feed planning tool.<br>
1011
  Feed estimates are guides and should be adjusted to actual flock performance.
1012
  </div>
 
1014
 
1015
  # Admin (Collapsed)
1016
  with gr.Accordion("πŸ”’ Admin", open=False):
1017
+ with gr.Group():
1018
+ admin_password_input = gr.Textbox(label="Admin Password", placeholder="Enter password...", type="password")
 
 
1019
  with gr.Row():
1020
  login_btn = gr.Button("Login", variant="primary")
1021
  logout_btn = gr.Button("Logout", variant="secondary", visible=False)
1022
  admin_output = gr.Markdown("πŸ”’ Enter the admin password to view analytics.")
 
1023
  with gr.Row():
1024
  export_leads_btn = gr.Button("πŸ“₯ Export Leads (CSV)", variant="secondary")
1025
  export_leads_output = gr.File(label="Leads CSV")
 
1031
  # EVENTS
1032
  # ============================================================
1033
 
1034
+ breed.change(fn=get_benchmark_html, inputs=[breed], outputs=[benchmark_table])
1035
+ breed.change(lambda b: f'<span style="background:#0B3022;color:#fff;padding:2px 14px;border-radius:20px;font-size:12px;font-weight:700;">{b}</span>', inputs=[breed], outputs=[selected_breed_badge])
1036
+ country_dropdown.change(fn=on_country_change_handler, inputs=[country_dropdown], outputs=[country_code_state, price])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1037
 
1038
  def calculate_wrapper(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request: gr.Request):
1039
  if not country_code_state:
1040
  country_code_state = "NG"
1041
+ return calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request)
 
 
 
 
 
 
 
 
 
1042
 
1043
+ calculate_btn.click(fn=calculate_wrapper, inputs=[birds, weeks, days, bag_size, price, email, country_code_state, breed], outputs=[result, save_status])
1044
+ ask_btn.click(fn=answer_farmer_question, inputs=[question, email, breed], outputs=[question_answer])
 
 
 
1045
 
1046
+ login_btn.click(fn=admin_login, inputs=[admin_password_input], outputs=[admin_output, admin_password_input, logout_btn])
1047
+ logout_btn.click(fn=logout_admin, inputs=[], outputs=[admin_output, admin_password_input, logout_btn])
1048
+ export_leads_btn.click(fn=export_leads_wrapper, inputs=[], outputs=[export_leads_output])
1049
+ export_calc_btn.click(fn=export_calculations_wrapper, inputs=[], outputs=[export_calc_output])
 
1050
 
 
 
 
 
 
1051
 
1052
+ # ============================================================
1053
+ # ADMIN FUNCTIONS
1054
+ # ============================================================
1055
+
1056
+ def admin_login(password):
1057
+ if password == ADMIN_PASSWORD:
1058
+ stats = get_analytics_summary()
1059
+ breed_stats_html = ""
1060
+ for breed, count in stats["breed_stats"]:
1061
+ breed_stats_html += f"<li><strong>{breed}</strong>: {count} calculations</li>"
1062
+ return f"""
1063
+ ### Analytics
1064
+ | Metric | Value |
1065
+ |--------|-------|
1066
+ | Total Visits | **{stats['total_visits']:,}** |
1067
+ | Today's Visits | **{stats['today_visits']}** |
1068
+ | Unique Visitors | **{stats['unique_visitors']:,}** |
1069
+ | Calculations | **{stats['total_calculations']:,}** |
1070
+ | Leads | **{stats['total_leads']:,}** |
1071
+ ### Popular Breeds
1072
+ <ul>{breed_stats_html}</ul>
1073
+ """, gr.update(visible=True), gr.update(visible=False)
1074
+ else:
1075
+ return "❌ Incorrect password.", gr.update(visible=False), gr.update(visible=True)
1076
+
1077
+
1078
+ def logout_admin():
1079
+ return "πŸ”’ Enter password to view analytics.", gr.update(visible=True), gr.update(visible=False)
1080
+
1081
+
1082
+ def export_leads_wrapper():
1083
+ import tempfile
1084
+ conn = sqlite3.connect(DB_PATH)
1085
+ cur = conn.cursor()
1086
+ cur.execute("SELECT email, country, currency, breed, created_at FROM leads ORDER BY created_at DESC")
1087
+ rows = cur.fetchall()
1088
+ conn.close()
1089
+ import csv
1090
+ from io import StringIO
1091
+ output = StringIO()
1092
+ writer = csv.writer(output)
1093
+ writer.writerow(["Email", "Country", "Currency", "Breed", "Created At"])
1094
+ writer.writerows(rows)
1095
+ temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
1096
+ temp.write(output.getvalue())
1097
+ temp.close()
1098
+ return temp.name
1099
+
1100
+
1101
+ def export_calculations_wrapper():
1102
+ import tempfile
1103
+ conn = sqlite3.connect(DB_PATH)
1104
+ cur = conn.cursor()
1105
+ cur.execute("SELECT email, breed, birds, total_days, rounded_bags, bag_size, total_cost, created_at FROM calculations ORDER BY created_at DESC")
1106
+ rows = cur.fetchall()
1107
+ conn.close()
1108
+ import csv
1109
+ from io import StringIO
1110
+ output = StringIO()
1111
+ writer = csv.writer(output)
1112
+ writer.writerow(["Email", "Breed", "Birds", "Days", "Bags", "Bag Size", "Cost", "Created"])
1113
+ writer.writerows(rows)
1114
+ temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
1115
+ temp.write(output.getvalue())
1116
+ temp.close()
1117
+ return temp.name
1118
 
 
 
 
 
 
1119
 
1120
+ def get_analytics_summary():
1121
+ conn = sqlite3.connect(DB_PATH)
1122
+ cur = conn.cursor()
1123
+ cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit'")
1124
+ total_visits = cur.fetchone()[0] or 0
1125
+ cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit' AND visit_date = ?", (datetime.now().strftime("%Y-%m-%d"),))
1126
+ today_visits = cur.fetchone()[0] or 0
1127
+ cur.execute("SELECT COUNT(*) FROM analytics WHERE action LIKE 'calculation_%'")
1128
+ total_calculations = cur.fetchone()[0] or 0
1129
+ cur.execute("SELECT COUNT(DISTINCT ip) FROM analytics WHERE action = 'page_visit' AND ip != 'unknown'")
1130
+ unique_visitors = cur.fetchone()[0] or 0
1131
+ cur.execute("SELECT breed, COUNT(*) FROM analytics WHERE action LIKE 'calculation_%' GROUP BY breed")
1132
+ breed_stats = cur.fetchall()
1133
+ cur.execute("SELECT COUNT(*) FROM leads")
1134
+ total_leads = cur.fetchone()[0] or 0
1135
+ conn.close()
1136
+ return {"total_visits": total_visits, "today_visits": today_visits, "unique_visitors": unique_visitors,
1137
+ "total_calculations": total_calculations, "total_leads": total_leads, "breed_stats": breed_stats}
1138
 
1139
+
1140
+ def on_country_change_handler(country_selection):
1141
+ if country_selection == "Auto-Detect":
1142
+ country_code = detect_country_from_env() or "NG"
1143
+ else:
1144
+ country_code = country_selection
1145
+ _, symbol, currency_code = get_country_info(country_code)
1146
+ default_price = get_default_price(currency_code)
1147
+ return country_code, default_price
1148
 
1149
 
1150
  # ============================================================
 
1153
 
1154
  if __name__ == "__main__":
1155
  port = int(os.environ.get("PORT", 7860))
 
1156
  demo.launch(
1157
  server_name="0.0.0.0",
1158
  server_port=port,