GoogleCalc commited on
Commit
67da504
Β·
verified Β·
1 Parent(s): 1639927

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +694 -572
app.py CHANGED
@@ -18,7 +18,7 @@ APP_NAME = "πŸ” How Much Feed?"
18
  DB_PATH = os.environ.get("DATABASE_PATH", "/data/howmuchfeed.db")
19
 
20
  # Admin password - set this in Hugging Face Secrets
21
- ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin123")
22
 
23
  # Create directory if it doesn't exist
24
  db_dir = os.path.dirname(DB_PATH)
@@ -84,7 +84,7 @@ def sync_to_google_sheets(email, country, currency, breed, action="lead"):
84
  elif action == "question":
85
  worksheet_name = "Questions"
86
  headers = ["Email", "Question", "Breed", "Created At"]
87
- row_data = [email, "Question", breed, datetime.now().isoformat()]
88
  else:
89
  return False
90
 
@@ -645,7 +645,7 @@ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_c
645
  # Generate share links
646
  share_links = get_share_links()
647
 
648
- # Output HTML - MINT GREEN & EGG YELLOW DESIGN
649
  starter_bags_display = f"{starter_bags:.1f}"
650
  finisher_bags_display = f"{finisher_bags:.1f}"
651
 
@@ -653,247 +653,105 @@ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_c
653
  if days:
654
  age_text += f" + {days} day{'s' if days != 1 else ''}"
655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  html = f"""
657
- <div style="
658
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
659
- font-size: 14px;
660
- line-height: 1.5;
661
- color: #0B3022;
662
- background: #FFFFFF;
663
- border-radius: 12px;
664
- padding: 14px 16px;
665
- border: 1px solid #D6E0DB;
666
- width: 100%;
667
- max-width: 100%;
668
- box-sizing: border-box;
669
- overflow-wrap: break-word;
670
- ">
671
-
672
- <!-- RESULTS CARD -->
673
- <div style="
674
- background: #F4F7F5;
675
- border: 2px solid #00A86B;
676
- border-radius: 10px;
677
- padding: 14px 16px;
678
- margin-bottom: 14px;
679
- ">
680
- <div style="
681
- font-size: 12px;
682
- font-weight: 700;
683
- color: #00A86B;
684
- text-transform: uppercase;
685
- letter-spacing: 0.5px;
686
- margin-bottom: 6px;
687
- ">πŸ“Š Your Feed Estimate</div>
688
-
689
- <!-- Total Feed - Dual Column -->
690
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 6px 0;">
691
- <div style="
692
- background: #FFFFFF;
693
- border-radius: 8px;
694
- padding: 10px 12px;
695
- border: 1px solid #D6E0DB;
696
- ">
697
- <div style="font-size: 11px; color: #5A6E65; font-weight: 600;">Total Feed</div>
698
- <div style="font-size: 26px; font-weight: 800; color: #00A86B;">
699
- {total_feed_kg:,.1f} kg
700
- </div>
701
- </div>
702
- <div style="
703
- background: #FFFFFF;
704
- border-radius: 8px;
705
- padding: 10px 12px;
706
- border: 1px solid #D6E0DB;
707
- ">
708
- <div style="font-size: 11px; color: #5A6E65; font-weight: 600;">Exact Bags</div>
709
- <div style="font-size: 26px; font-weight: 800; color: #0B3022;">
710
- {total_bags:.1f}
711
- </div>
712
- </div>
713
  </div>
714
-
715
- <!-- Birds Info -->
716
- <div style="font-size: 13px; color: #0B3022; margin-bottom: 8px;">
717
- For <strong>{birds:,}</strong> {breed} broilers Β· <strong>{age_text}</strong>
718
  </div>
719
-
720
- <!-- Buy Recommendation - Egg Yellow Frame -->
721
- <div style="
722
- background: #FFF8E1;
723
- border: 2px solid #FFB800;
724
- border-radius: 8px;
725
- padding: 10px 14px;
726
- margin-top: 4px;
727
- ">
728
- <div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
729
- <span style="font-size: 13px; font-weight: 600; color: #0B3022;">Buy</span>
730
- <span style="
731
- display: inline-block;
732
- background: #FFB800;
733
- color: #0B3022;
734
- border-radius: 6px;
735
- padding: 2px 14px;
736
- font-size: 20px;
737
- font-weight: 800;
738
- ">{rounded_bags}</span>
739
- <span style="font-size: 13px; color: #5A6E65;">bags (rounded up for safety)</span>
740
- </div>
741
- <div style="font-size: 12px; color: #5A6E65; margin-top: 2px;">
742
- This ensures enough feed for the full growing period.
743
- </div>
744
  </div>
745
- </div>
 
 
 
 
 
746
 
747
- <!-- FEED BREAKDOWN TABLE -->
748
- <div style="margin-bottom: 12px;">
749
- <div style="font-size: 12px; font-weight: 700; color: #00A86B; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px;">🌾 Feed Breakdown</div>
750
- <table style="
751
- width: 100%;
752
- border-collapse: collapse;
753
- font-size: 13px;
754
- background: #FFFFFF;
755
- border: 1px solid #D6E0DB;
756
- border-radius: 8px;
757
- overflow: hidden;
758
- ">
759
  <thead>
760
- <tr style="background: #F4F7F5; border-bottom: 2px solid #00A86B;">
761
- <th style="padding: 8px 10px; text-align: left; font-weight: 600; color: #0B3022; font-size: 12px;">Stage</th>
762
- <th style="padding: 8px 10px; text-align: right; font-weight: 600; color: #0B3022; font-size: 12px;">Feed (kg)</th>
763
- <th style="padding: 8px 10px; text-align: right; font-weight: 600; color: #0B3022; font-size: 12px;">Bags</th>
764
- </tr>
765
  </thead>
766
  <tbody>
767
- <tr style="border-bottom: 1px solid #D6E0DB;">
768
- <td style="padding: 8px 10px; color: #0B3022; font-size: 12px;"><strong>🌱 Starter (0-4 wks)</strong></td>
769
- <td style="padding: 8px 10px; text-align: right; color: #0B3022; font-size: 12px;">{starter_kg:,.1f}</td>
770
- <td style="padding: 8px 10px; text-align: right; font-weight: 700; color: #0B3022; font-size: 12px;">{starter_bags_display}</td>
771
- </tr>
772
- <tr style="border-bottom: 1px solid #D6E0DB;">
773
- <td style="padding: 8px 10px; color: #0B3022; font-size: 12px;"><strong>🌽 Finisher (4-8 wks)</strong></td>
774
- <td style="padding: 8px 10px; text-align: right; color: #0B3022; font-size: 12px;">{finisher_kg:,.1f}</td>
775
- <td style="padding: 8px 10px; text-align: right; font-weight: 700; color: #0B3022; font-size: 12px;">{finisher_bags_display}</td>
776
- </tr>
777
- <tr style="background: #F4F7F5; font-weight: 700;">
778
- <td style="padding: 8px 10px; color: #0B3022; font-size: 13px;">TOTAL</td>
779
- <td style="padding: 8px 10px; text-align: right; color: #0B3022; font-size: 13px;">{total_feed_kg:,.1f}</td>
780
- <td style="padding: 8px 10px; text-align: right; font-weight: 800; font-size: 15px; color: #00A86B;">{total_bags:.1f}</td>
781
- </tr>
782
  </tbody>
783
- </table>
784
- </div>
785
-
786
- <!-- FEED COST - EGG YELLOW CARD -->
787
- <div style="
788
- background: #FFF8E1;
789
- border: 2px solid #FFB800;
790
- border-radius: 8px;
791
- padding: 12px 16px;
792
- margin-bottom: 12px;
793
- ">
794
- <div style="font-size: 11px; font-weight: 700; color: #5A6E65; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">πŸ’° Feed Cost</div>
795
- <div style="display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; font-size: 14px;">
796
- <span style="color: #0B3022;">~</span>
797
- <span style="
798
- display: inline-block;
799
- background: #FFB800;
800
- color: #0B3022;
801
- border-radius: 6px;
802
- padding: 2px 12px;
803
- font-size: 18px;
804
- font-weight: 700;
805
- ">{rounded_bags}</span>
806
- <span style="color: #0B3022;">Γ— {currency_symbol}{price_per_bag:,.0f}</span>
807
  </div>
808
- <div style="font-size: 24px; font-weight: 800; color: #0B3022; margin-top: 2px;">
809
- {currency_symbol}{total_cost:,.0f}
810
- </div>
811
- </div>
812
-
813
- <!-- EXPECTED PERFORMANCE -->
814
- <div style="
815
- border-top: 1px solid #D6E0DB;
816
- padding-top: 10px;
817
- margin-bottom: 10px;
818
- ">
819
- <div style="font-size: 11px; font-weight: 700; color: #00A86B; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">πŸ“ˆ Expected Performance</div>
820
- <div style="display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px;">
821
- <div>
822
- Body: <span style="font-weight: 700; color: #0B3022;">{expected_weight:.3f} kg</span>
823
- </div>
824
- <div>
825
- Flock: <span style="font-weight: 700; color: #0B3022;">{flock_weight:,.1f} kg</span>
826
- </div>
827
  </div>
828
- </div>
829
-
830
- <!-- SHARE BUTTONS -->
831
- <div style="
832
- border-top: 1px solid #D6E0DB;
833
- padding-top: 10px;
834
- ">
835
- <div style="font-size: 11px; font-weight: 700; color: #00A86B; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">πŸ“€ Share Results</div>
836
- <div style="display: flex; gap: 6px; flex-wrap: wrap;">
837
- <a href="{share_links['whatsapp']}" target="_blank" style="text-decoration: none;">
838
- <span style="
839
- display: inline-block;
840
- background: #00A86B;
841
- color: #FFFFFF;
842
- padding: 5px 12px;
843
- border-radius: 6px;
844
- font-size: 12px;
845
- font-weight: 600;
846
- cursor: pointer;
847
- ">WhatsApp</span>
848
- </a>
849
- <a href="{share_links['email']}" target="_blank" style="text-decoration: none;">
850
- <span style="
851
- display: inline-block;
852
- background: #00A86B;
853
- color: #FFFFFF;
854
- padding: 5px 12px;
855
- border-radius: 6px;
856
- font-size: 12px;
857
- font-weight: 600;
858
- cursor: pointer;
859
- ">Email</span>
860
- </a>
861
- <a href="{share_links['facebook']}" target="_blank" style="text-decoration: none;">
862
- <span style="
863
- display: inline-block;
864
- background: #00A86B;
865
- color: #FFFFFF;
866
- padding: 5px 12px;
867
- border-radius: 6px;
868
- font-size: 12px;
869
- font-weight: 600;
870
- cursor: pointer;
871
- ">Facebook</span>
872
- </a>
873
- <a href="{share_links['twitter']}" target="_blank" style="text-decoration: none;">
874
- <span style="
875
- display: inline-block;
876
- background: #00A86B;
877
- color: #FFFFFF;
878
- padding: 5px 12px;
879
- border-radius: 6px;
880
- font-size: 12px;
881
- font-weight: 600;
882
- cursor: pointer;
883
- ">Twitter</span>
884
- </a>
885
  </div>
886
- </div>
887
 
888
- <div style="
889
- margin-top: 8px;
890
- font-size: 10px;
891
- color: #5A6E65;
892
- border-top: 1px solid #D6E0DB;
893
- padding-top: 6px;
894
- ">
895
- ⚠️ Based on {breed} benchmark. Actual results may vary.
896
- </div>
897
  </div>
898
  """
899
 
@@ -991,7 +849,7 @@ def update_benchmark(breed):
991
 
992
 
993
  def on_country_change_handler(country_selection):
994
- """Update price and currency when country changes."""
995
  if country_selection == "Auto-Detect":
996
  country_code = detect_country_from_env() or "NG"
997
  else:
@@ -999,7 +857,7 @@ def on_country_change_handler(country_selection):
999
 
1000
  _, symbol, currency_code = get_country_info(country_code)
1001
  default_price = get_default_price(currency_code)
1002
- return country_code, default_price, symbol
1003
 
1004
 
1005
  # ============================================================
@@ -1115,132 +973,438 @@ def export_calculations_wrapper():
1115
  # ============================================================
1116
 
1117
  CSS = """
1118
- /* ============================================================
1119
- MINT GREEN & EGG YELLOW DESIGN SYSTEM
1120
- ============================================================ */
1121
-
1122
  :root {
1123
- --app-bg-main: #F4F7F5;
1124
- --app-card-surface: #FFFFFF;
1125
- --brand-primary-mint: #00A86B;
1126
- --brand-dark-charcoal: #0B3022;
1127
- --brand-accent-yellow: #FFB800;
1128
- --ui-border-muted: #D6E0DB;
1129
- --text-helper: #5A6E65;
 
 
 
1130
  }
1131
 
1132
- /* GLOBAL */
1133
- body, .gradio-container, .gradio-row, .gradio-column, .gradio-group {
1134
- background-color: var(--app-bg-main) !important;
1135
- color: var(--brand-dark-charcoal) !important;
 
 
 
 
 
 
1136
  }
1137
 
1138
- .gradio-container {
1139
- max-width: 100% !important;
1140
- padding: 4px 6px !important;
 
 
 
 
 
 
1141
  }
1142
 
1143
- /* MAIN TITLE */
1144
- .main-title {
1145
- text-align: center;
1146
- padding: 6px 4px 4px 4px;
1147
- }
1148
- .main-title h1 {
1149
- font-size: 22px;
1150
- margin-bottom: 2px;
1151
- color: var(--brand-dark-charcoal) !important;
1152
  }
1153
- .main-title p {
1154
- font-size: 13px;
1155
- color: var(--text-helper) !important;
 
 
 
 
 
 
 
1156
  }
1157
 
1158
- /* ANSWER BOX */
1159
- .answer-box {
1160
- background: var(--app-card-surface) !important;
1161
- border-radius: 10px !important;
1162
- padding: 6px 8px !important;
1163
- border: 1px solid var(--ui-border-muted) !important;
1164
  }
1165
 
1166
- /* EMAIL BOX */
1167
- .email-box {
1168
- border: 2px solid var(--brand-primary-mint) !important;
1169
- border-radius: 8px !important;
1170
- padding: 6px !important;
1171
- background: #F4F7F5 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1172
  }
1173
 
1174
- /* BUTTONS */
1175
- button {
1176
- min-height: 40px !important;
1177
- background-color: var(--brand-primary-mint) !important;
1178
- color: #FFFFFF !important;
1179
- font-weight: 600 !important;
1180
- border: none !important;
1181
- border-radius: 6px !important;
1182
- font-size: 14px !important;
1183
- padding: 8px 16px !important;
1184
- width: 100% !important;
1185
- transition: all 0.2s ease !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1186
  }
1187
 
1188
- button:hover {
1189
- background-color: #008F5A !important;
1190
- transform: scale(1.02) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1191
  }
1192
 
1193
- /* ADMIN LOGIN */
1194
- .admin-login {
1195
- border: 2px solid var(--brand-accent-yellow) !important;
1196
- border-radius: 8px !important;
1197
- padding: 10px !important;
1198
- background: #FFF8E1 !important;
 
 
1199
  }
1200
 
1201
- /* DROPDOWNS */
1202
- select, input, textarea {
1203
- background: #FFFFFF !important;
1204
- color: var(--brand-dark-charcoal) !important;
1205
- border: 1px solid var(--ui-border-muted) !important;
1206
- border-radius: 6px !important;
1207
  }
1208
 
1209
- select:focus, input:focus, textarea:focus {
1210
- border-color: var(--brand-primary-mint) !important;
1211
- box-shadow: 0 0 0 3px rgba(0, 168, 107, 0.2) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1212
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1213
 
1214
- /* TABLES */
1215
- table {
1216
- font-size: 12px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1217
  }
1218
 
1219
- th, td {
1220
- padding: 4px 6px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
1221
  }
1222
 
1223
- /* DESKTOP */
1224
- @media screen and (min-width: 769px) {
1225
- .gradio-container {
1226
- padding: 8px 16px !important;
1227
- max-width: 1200px !important;
1228
- margin: 0 auto !important;
1229
- }
1230
-
1231
- .main-title h1 {
1232
- font-size: 28px !important;
1233
- }
1234
-
1235
- button {
1236
- width: auto !important;
1237
- }
 
 
 
 
 
 
 
1238
  }
1239
 
1240
- /* DARK MODE OVERRIDE */
1241
- .dark, .dark * {
1242
- background-color: var(--app-bg-main) !important;
1243
- color: var(--brand-dark-charcoal) !important;
 
 
 
 
1244
  }
1245
  """
1246
 
@@ -1248,281 +1412,216 @@ th, td {
1248
  # UI
1249
  # ============================================================
1250
 
1251
- with gr.Blocks(title=APP_NAME, css=CSS, theme=gr.themes.Default()) as demo:
1252
 
1253
  gr.HTML("""
1254
- <div class="main-title">
1255
- <h1>πŸ” How Much Feed?</h1>
1256
- <p>Broiler Feed Calculator β€” Know exactly how much feed you need for your birds.</p>
1257
- </div>
 
 
 
 
 
1258
  """)
1259
 
1260
- # Mobile-first: Stack vertically on mobile, side by side on desktop
1261
- with gr.Row(equal_height=True, variant="default"):
1262
-
1263
- # LEFT COLUMN: INPUTS
1264
- with gr.Column(scale=1, min_width=280):
 
1265
 
1266
- with gr.Group():
1267
- gr.Markdown("## πŸ” Breed & Birds")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1268
 
1269
- breed = gr.Dropdown(
1270
- choices=["Ross 308", "Cobb 500", "Arbor Acres"],
1271
- value="Ross 308",
1272
- label="Breed",
1273
- info="Select your broiler breed for accurate estimates",
1274
  )
1275
-
1276
- birds = gr.Dropdown(
1277
- choices=[25, 50, 75, 100, 150, 200, 500, 1000],
1278
- value=50,
1279
- label="Number of birds",
1280
- allow_custom_value=True,
1281
  )
1282
 
1283
- gr.Markdown("## πŸ“… Expected Age")
1284
-
1285
- with gr.Row():
1286
- weeks = gr.Dropdown(
1287
- choices=list(range(0, 21)),
1288
- value=8,
1289
- label="Weeks",
1290
- scale=1,
1291
- )
1292
- days = gr.Dropdown(
1293
- choices=list(range(0, 7)),
1294
- value=0,
1295
- label="Days",
1296
- scale=1,
1297
- )
1298
-
1299
- gr.Markdown("## 🌾 Feed Details")
1300
-
1301
- with gr.Row():
1302
- bag_size = gr.Dropdown(
1303
- choices=[10, 20, 25, 50, 100],
1304
- value=25,
1305
- label="Bag size (kg)",
1306
- scale=1,
1307
- )
1308
- price = gr.Number(
1309
- value=24000,
1310
- label="Price per bag (₦)",
1311
- minimum=0,
1312
- scale=1,
1313
- )
1314
-
1315
- gr.Markdown("## 🌍 Your Location")
1316
-
1317
- detected_country = detect_country_from_env() or "NG"
1318
- country_choices = ["Auto-Detect"] + [code for code, _ in COUNTRY_LIST]
1319
-
1320
- country_dropdown = gr.Dropdown(
1321
- choices=country_choices,
1322
- value="Auto-Detect",
1323
- label="Country (Auto-Detect uses your IP)",
1324
  )
1325
 
1326
- country_code_state = gr.State(detected_country)
1327
-
1328
- gr.Markdown("## πŸ“§ Your Email (Required)")
 
 
 
 
 
 
 
 
1329
 
 
 
1330
  email = gr.Textbox(
1331
- label="",
1332
  placeholder="farmer@example.com",
1333
  type="email",
1334
  elem_classes=["email-box"],
1335
- info="We never share your email. It is used only to save your results.",
1336
- )
1337
-
1338
- calculate_btn = gr.Button(
1339
- "πŸ” Calculate Feed Requirement",
1340
- variant="primary",
1341
- size="lg",
1342
  )
1343
 
1344
- gr.Markdown("""
1345
- <div style="font-size: 11px; color: #5A6E65; margin-top: 4px;">
1346
- ⚠️ Why email is required? We send your calculation to your email so you can access it anytime.
1347
- </div>
1348
- """)
1349
-
1350
- save_status = gr.Markdown("")
1351
-
1352
- # RIGHT COLUMN: RESULTS
1353
- with gr.Column(scale=1, min_width=280):
1354
- gr.Markdown("## πŸ“Š Your Quick Answer")
1355
- result = gr.HTML(
1356
- value="""
1357
- <div style="font-size: 13px; color: #5A6E65; text-align: center; padding: 20px 10px; background: #FFFFFF; border-radius: 10px; border: 1px solid #D6E0DB;">
1358
- <div style="font-size: 36px; margin-bottom: 8px;">πŸ”</div>
1359
- <div>Enter your details and click <strong>Calculate</strong></div>
1360
- <div style="font-size: 12px; margin-top: 4px; color: #5A6E65;">Your feed estimate appears here</div>
1361
- </div>
1362
- """,
1363
- elem_classes=["answer-box"],
1364
- )
1365
 
1366
  # ============================================================
1367
- # BENCHMARK SECTION
1368
  # ============================================================
1369
-
1370
- gr.Markdown("---")
1371
-
1372
- with gr.Tabs():
1373
- with gr.TabItem("πŸ“Š Selected Breed Benchmark"):
1374
- gr.Markdown("### Weekly Benchmark for Selected Breed")
1375
- initial_table, _ = get_benchmark_table("Ross 308")
1376
-
1377
- benchmark_table = gr.Dataframe(
1378
- value=initial_table,
1379
- headers=["Days", "Body Weight (kg)", "Cumulative Feed (kg)", "FCR"],
1380
- interactive=False,
1381
- wrap=True,
1382
- )
1383
-
1384
- with gr.TabItem("πŸ“ˆ Breed Comparison"):
1385
- gr.Markdown("### Breed Comparison (As-Hatched Performance)")
1386
- comp_data, breeds_list, days_list = get_breed_comparison_table()
1387
-
1388
- headers = ["Days"]
1389
- for b in breeds_list:
1390
- headers.append(f"{b} Weight (kg)")
1391
- headers.append(f"{b} Feed (kg)")
1392
- headers.append(f"{b} FCR")
1393
-
1394
- comparison_table = gr.Dataframe(
1395
- value=comp_data,
1396
- headers=headers,
1397
- interactive=False,
1398
- wrap=True,
1399
- )
1400
-
1401
- with gr.TabItem("πŸ”’ Admin"):
1402
- gr.Markdown("### πŸ”’ Admin Dashboard")
1403
-
1404
- with gr.Group(elem_classes="admin-login"):
1405
- gr.Markdown("Enter admin password to view analytics and download data.")
1406
-
1407
- with gr.Row():
1408
- admin_password_input = gr.Textbox(
1409
- label="Admin Password",
1410
- placeholder="Enter password...",
1411
- type="password",
1412
- scale=2,
1413
- )
1414
- login_btn = gr.Button("Login", variant="primary", scale=1)
1415
- logout_btn = gr.Button("Logout", variant="secondary", scale=1)
1416
-
1417
- admin_output = gr.Markdown("πŸ”’ Enter password to view analytics.")
1418
-
1419
- with gr.Row():
1420
- export_leads_btn = gr.Button("πŸ“₯ Export Leads (CSV)", variant="secondary")
1421
- export_leads_output = gr.File(label="Download Leads CSV")
1422
-
1423
- with gr.Row():
1424
- export_calc_btn = gr.Button("πŸ“₯ Export Calculations (CSV)", variant="secondary")
1425
- export_calc_output = gr.File(label="Download Calculations CSV")
1426
-
1427
- login_btn.click(
1428
- fn=admin_login,
1429
- inputs=[admin_password_input],
1430
- outputs=[admin_output, admin_password_input, logout_btn],
1431
- )
1432
-
1433
- logout_btn.click(
1434
- fn=logout_admin,
1435
- inputs=[],
1436
- outputs=[admin_output, admin_password_input, logout_btn],
1437
- )
1438
-
1439
- export_leads_btn.click(
1440
- fn=export_leads_wrapper,
1441
- inputs=[],
1442
- outputs=[export_leads_output],
1443
- )
1444
-
1445
- export_calc_btn.click(
1446
- fn=export_calculations_wrapper,
1447
- inputs=[],
1448
- outputs=[export_calc_output],
1449
- )
1450
 
1451
  # ============================================================
1452
- # BREED REFERENCE
1453
  # ============================================================
 
 
 
 
 
 
 
 
 
 
1454
 
1455
- gr.Markdown("---")
1456
-
1457
- gr.Markdown("""
1458
- ### πŸ” Common Broiler Breeds in Nigeria
1459
-
1460
- | Breed | Characteristics | Available At |
1461
- |-------|-----------------|--------------|
1462
- | **Ross 308** | Big chest, thick legs, very fast growth | Agrited, Sayed, Valentine |
1463
- | **Cobb 500** | Good early growth, strong legs | Chikun, Zartech |
1464
- | **Arbor Acres** | Balanced growth, hardy, adaptable | Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila |
 
 
 
1465
 
1466
- πŸ’‘ **Tip:** Before buying chicks, ask about the breed and hatchery. Not all broilers are the same!
1467
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1468
 
1469
  # ============================================================
1470
- # HOW IT WORKS
1471
  # ============================================================
1472
-
1473
- gr.Markdown("---")
1474
-
1475
- with gr.Row():
1476
- with gr.Column(scale=1):
1477
- gr.Markdown("""
1478
- ### πŸ“Œ How it works
1479
-
1480
- - **Weeks + Days** β†’ total days
1481
- - **Starter** β†’ Day 0–21
1482
- - **Finisher** β†’ Day 22+
1483
- - **Bags** β†’ rounded up to next whole bag
1484
- - **Breed-specific benchmarks** used for estimates
1485
-
1486
- ⚠️ Actual results may vary with management, feed quality, and other conditions.
1487
- """)
1488
-
1489
- with gr.Column(scale=1):
1490
- gr.Markdown("""
1491
- ### πŸ“Š Data Sources
1492
-
1493
- - **Ross 308** β€” Aviagen (2022)
1494
- - **Cobb 500** β€” Cobb-Vantress
1495
- - **Arbor Acres** β€” Aviagen (2022)
1496
-
1497
- *All data from official breed performance objectives.*
1498
- """)
1499
 
1500
  # ============================================================
1501
- # QUESTION SECTION
1502
  # ============================================================
1503
-
1504
- gr.Markdown("---")
1505
-
1506
- with gr.Row():
1507
- with gr.Column(scale=2):
1508
- gr.Markdown("""
1509
- # ❓ Ask a question
1510
- ### What would you like to know about raising your birds?
1511
- """)
1512
-
1513
- question = gr.Textbox(
1514
- label="",
1515
- placeholder="e.g. How many bags of feed for 100 broilers for 6 weeks?",
1516
- lines=3,
1517
  )
 
 
 
 
1518
 
1519
- ask_btn = gr.Button("πŸ’¬ Get Answer", variant="secondary", size="lg")
1520
- question_answer = gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
1521
 
1522
  # ============================================================
1523
  # EVENTS
1524
  # ============================================================
1525
-
1526
  breed.change(
1527
  fn=update_benchmark,
1528
  inputs=[breed],
@@ -1532,13 +1631,16 @@ with gr.Blocks(title=APP_NAME, css=CSS, theme=gr.themes.Default()) as demo:
1532
  country_dropdown.change(
1533
  fn=on_country_change_handler,
1534
  inputs=[country_dropdown],
1535
- outputs=[country_code_state, price, price],
1536
  )
1537
 
1538
  def calculate_wrapper(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request: gr.Request):
1539
  if not country_code_state:
1540
  country_code_state = "NG"
1541
- return calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request)
 
 
 
1542
 
1543
  calculate_btn.click(
1544
  fn=calculate_wrapper,
@@ -1552,12 +1654,32 @@ with gr.Blocks(title=APP_NAME, css=CSS, theme=gr.themes.Default()) as demo:
1552
  outputs=[question_answer],
1553
  )
1554
 
1555
- demo.load(
1556
- fn=track_visit,
1557
- inputs=None,
1558
- outputs=None,
1559
  )
1560
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1561
 
1562
  # ============================================================
1563
  # START APP
 
18
  DB_PATH = os.environ.get("DATABASE_PATH", "/data/howmuchfeed.db")
19
 
20
  # Admin password - set this in Hugging Face Secrets
21
+ ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "")
22
 
23
  # Create directory if it doesn't exist
24
  db_dir = os.path.dirname(DB_PATH)
 
84
  elif action == "question":
85
  worksheet_name = "Questions"
86
  headers = ["Email", "Question", "Breed", "Created At"]
87
+ row_data = [email or "", country or "", currency or "", datetime.now().isoformat()]
88
  else:
89
  return False
90
 
 
645
  # Generate share links
646
  share_links = get_share_links()
647
 
648
+ # Output HTML - mobile-first farmer-friendly result
649
  starter_bags_display = f"{starter_bags:.1f}"
650
  finisher_bags_display = f"{finisher_bags:.1f}"
651
 
 
653
  if days:
654
  age_text += f" + {days} day{'s' if days != 1 else ''}"
655
 
656
+ share_text = (
657
+ f"How Much Feed?\n"
658
+ f"{breed} broilers: {birds:,} birds for {age_text}.\n"
659
+ f"Feed needed: {total_feed_kg:,.1f} kg ({total_bags:.1f} bags).\n"
660
+ f"Buy {rounded_bags} bags."
661
+ )
662
+ share_encoded = urllib.parse.quote(share_text)
663
+ share_links = {
664
+ "whatsapp": f"https://wa.me/?text={share_encoded}",
665
+ "email": f"mailto:?subject=My How Much Feed result&body={share_encoded}",
666
+ "facebook": f"https://www.facebook.com/sharer/sharer.php?u={urllib.parse.quote('https://howmuchfeed.com')}",
667
+ "twitter": f"https://twitter.com/intent/tweet?text={share_encoded}",
668
+ }
669
+
670
  html = f"""
671
+ <div class="results-shell">
672
+ <section class="buy-card">
673
+ <div class="eyebrow">YOUR FEED RESULT</div>
674
+ <div class="buy-label">BUY</div>
675
+ <div class="buy-number">{rounded_bags}</div>
676
+ <div class="buy-unit">BAGS</div>
677
+ <div class="buy-note">Recommended amount, rounded up to the next whole bag.</div>
678
+ </section>
679
+
680
+ <div class="result-context">
681
+ <strong>{birds:,} {breed} broilers</strong> Β· {age_text} ({total_days} days)
682
+ </div>
683
+
684
+ <p class="plain-answer">
685
+ You need <strong>{total_feed_kg:,.1f} kg</strong> of feed, equal to about
686
+ <strong>{total_bags:.1f} bags</strong> of {bag_size:g} kg feed.
687
+ </p>
688
+
689
+ <div class="metric-grid">
690
+ <div class="metric-card">
691
+ <span class="metric-icon">βš–οΈ</span>
692
+ <span class="metric-label">Feed needed</span>
693
+ <strong>{total_feed_kg:,.1f} kg</strong>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  </div>
695
+ <div class="metric-card">
696
+ <span class="metric-icon">βœ“</span>
697
+ <span class="metric-label">Exact amount</span>
698
+ <strong>{total_bags:.1f} bags</strong>
699
  </div>
700
+ <div class="metric-card">
701
+ <span class="metric-icon">₦</span>
702
+ <span class="metric-label">Estimated cost</span>
703
+ <strong>{currency_symbol}{total_cost:,.0f}</strong>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  </div>
705
+ <div class="metric-card">
706
+ <span class="metric-icon">πŸ“…</span>
707
+ <span class="metric-label">Growing period</span>
708
+ <strong>{total_days} days</strong>
709
+ </div>
710
+ </div>
711
 
712
+ <section class="result-section">
713
+ <div class="section-title">🌾 Feed Breakdown</div>
714
+ <div class="table-wrap">
715
+ <table class="feed-table">
 
 
 
 
 
 
 
 
716
  <thead>
717
+ <tr><th>Stage</th><th>Feed</th><th>Bags</th></tr>
 
 
 
 
718
  </thead>
719
  <tbody>
720
+ <tr><td>Starter <span class="muted">(0–21 days)</span></td><td>{starter_kg:,.1f} kg</td><td>{starter_bags_display}</td></tr>
721
+ <tr><td>Finisher <span class="muted">(Day 22+)</span></td><td>{finisher_kg:,.1f} kg</td><td>{finisher_bags_display}</td></tr>
722
+ <tr class="total-row"><td>TOTAL</td><td>{total_feed_kg:,.1f} kg</td><td>{total_bags:.1f} bags</td></tr>
 
 
 
 
 
 
 
 
 
 
 
 
723
  </tbody>
724
+ </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  </div>
726
+ </section>
727
+
728
+ <section class="cost-card">
729
+ <div class="section-title">πŸ’° Feed Cost Estimate</div>
730
+ <div class="cost-main">{currency_symbol}{total_cost:,.0f}</div>
731
+ <div class="cost-note">Based on {rounded_bags} bags Γ— {currency_symbol}{price_per_bag:,.0f} per bag.</div>
732
+ </section>
733
+
734
+ <section class="result-section performance-section">
735
+ <div class="section-title">πŸ“ˆ Expected Performance</div>
736
+ <div class="performance-grid">
737
+ <div><span>Body weight per bird</span><strong>{expected_weight:.3f} kg</strong></div>
738
+ <div><span>Flock live weight</span><strong>{flock_weight:,.1f} kg</strong></div>
 
 
 
 
 
 
739
  </div>
740
+ </section>
741
+
742
+ <section class="result-section share-section">
743
+ <div class="section-title">πŸ“€ Share Result</div>
744
+ <div class="share-grid">
745
+ <a class="share-btn" href="{share_links['whatsapp']}" target="_blank" rel="noopener">WhatsApp</a>
746
+ <a class="share-btn" href="{share_links['email']}" target="_blank" rel="noopener">Email</a>
747
+ <a class="share-btn" href="{share_links['facebook']}" target="_blank" rel="noopener">Facebook</a>
748
+ <a class="share-btn" href="{share_links['twitter']}" target="_blank" rel="noopener">X</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
  </div>
750
+ </section>
751
 
752
+ <div class="result-disclaimer">
753
+ ⚠️ This is an estimate based on the selected {breed} benchmark. Actual feed use can vary with feed quality, management, environment and bird performance.
754
+ </div>
 
 
 
 
 
 
755
  </div>
756
  """
757
 
 
849
 
850
 
851
  def on_country_change_handler(country_selection):
852
+ """Update country state and default feed price when country changes."""
853
  if country_selection == "Auto-Detect":
854
  country_code = detect_country_from_env() or "NG"
855
  else:
 
857
 
858
  _, symbol, currency_code = get_country_info(country_code)
859
  default_price = get_default_price(currency_code)
860
+ return country_code, default_price
861
 
862
 
863
  # ============================================================
 
973
  # ============================================================
974
 
975
  CSS = """
 
 
 
 
976
  :root {
977
+ --mint: #00A86B;
978
+ --mint-dark: #0B3022;
979
+ --mint-soft: #EAF8F1;
980
+ --yellow: #FFB800;
981
+ --yellow-soft: #FFF7DB;
982
+ --surface: #FFFFFF;
983
+ --page: #F7FAF8;
984
+ --border: #D9E4DE;
985
+ --muted: #5D6B65;
986
+ --danger: #B42318;
987
  }
988
 
989
+ /* =========================
990
+ GLOBAL MOBILE-FIRST UI
991
+ ========================= */
992
+ html, body {
993
+ margin: 0 !important;
994
+ padding: 0 !important;
995
+ background: var(--page) !important;
996
+ color: var(--mint-dark) !important;
997
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif !important;
998
+ -webkit-text-size-adjust: 100%;
999
  }
1000
 
1001
+ body,
1002
+ .gradio-container,
1003
+ .gradio-container > .main,
1004
+ .gradio-container .block,
1005
+ .gradio-container .gr-group,
1006
+ .gradio-container .gr-box,
1007
+ .gradio-container .gr-panel {
1008
+ background: var(--page) !important;
1009
+ color: var(--mint-dark) !important;
1010
  }
1011
 
1012
+ .gradio-container {
1013
+ width: 100% !important;
1014
+ max-width: 1180px !important;
1015
+ margin: 0 auto !important;
1016
+ padding: 0 12px 48px !important;
1017
+ box-sizing: border-box !important;
 
 
 
1018
  }
1019
+
1020
+ /* Never force a dark theme onto the mobile browser. */
1021
+ .dark body,
1022
+ .dark .gradio-container,
1023
+ .dark .gradio-container .block,
1024
+ .dark .gradio-container .gr-group,
1025
+ .dark .gradio-container .gr-box,
1026
+ .dark .gradio-container .gr-panel {
1027
+ background: var(--page) !important;
1028
+ color: var(--mint-dark) !important;
1029
  }
1030
 
1031
+ h1, h2, h3, h4, p, label, span, div {
1032
+ color: inherit;
 
 
 
 
1033
  }
1034
 
1035
+ /* =========================
1036
+ HEADER
1037
+ ========================= */
1038
+ .app-header {
1039
+ background: var(--mint-dark);
1040
+ color: #fff;
1041
+ border-radius: 0 0 18px 18px;
1042
+ padding: 18px 16px 20px;
1043
+ margin: 0 -12px 16px;
1044
+ }
1045
+ .app-header .brand-row {
1046
+ display: flex;
1047
+ align-items: center;
1048
+ gap: 10px;
1049
+ }
1050
+ .app-header .brand-icon { font-size: 30px; line-height: 1; }
1051
+ .app-header h1 {
1052
+ color: #fff !important;
1053
+ margin: 0;
1054
+ font-size: clamp(23px, 6vw, 32px);
1055
+ line-height: 1.1;
1056
+ font-weight: 800;
1057
+ }
1058
+ .app-header p {
1059
+ color: rgba(255,255,255,.88) !important;
1060
+ margin: 6px 0 0 40px;
1061
+ font-size: 14px;
1062
+ line-height: 1.45;
1063
  }
1064
 
1065
+ /* =========================
1066
+ STATUS / FORM SECTIONS
1067
+ ========================= */
1068
+ .status-card {
1069
+ background: var(--surface);
1070
+ border: 1px solid var(--border);
1071
+ border-left: 4px solid var(--mint);
1072
+ border-radius: 12px;
1073
+ padding: 12px 14px;
1074
+ margin-bottom: 12px;
1075
+ font-size: 13px;
1076
+ }
1077
+ .status-card strong { color: var(--mint-dark) !important; }
1078
+ .status-card a { color: var(--mint) !important; font-weight: 700; }
1079
+
1080
+ .form-section {
1081
+ background: var(--surface) !important;
1082
+ border: 1px solid var(--border) !important;
1083
+ border-radius: 14px !important;
1084
+ margin-bottom: 12px !important;
1085
+ overflow: visible !important;
1086
+ padding: 14px !important;
1087
+ box-shadow: 0 2px 8px rgba(11,48,34,.04) !important;
1088
+ }
1089
+ .section-heading {
1090
+ display: flex;
1091
+ align-items: center;
1092
+ gap: 9px;
1093
+ margin: 0 0 12px;
1094
+ color: var(--mint-dark) !important;
1095
+ font-size: 17px;
1096
+ font-weight: 800;
1097
+ }
1098
+ .step-number {
1099
+ width: 28px;
1100
+ height: 28px;
1101
+ border-radius: 50%;
1102
+ background: var(--mint-dark);
1103
+ color: #fff !important;
1104
+ display: inline-flex;
1105
+ align-items: center;
1106
+ justify-content: center;
1107
+ font-size: 13px;
1108
+ font-weight: 800;
1109
+ flex: 0 0 28px;
1110
  }
1111
 
1112
+ /* Gradio field controls */
1113
+ .gradio-container input,
1114
+ .gradio-container textarea,
1115
+ .gradio-container select,
1116
+ .gradio-container [role="combobox"] {
1117
+ background: #fff !important;
1118
+ color: var(--mint-dark) !important;
1119
+ border: 1px solid #C8D5CE !important;
1120
+ border-radius: 10px !important;
1121
+ min-height: 48px !important;
1122
+ box-sizing: border-box !important;
1123
+ font-size: 16px !important;
1124
+ }
1125
+ .gradio-container input:focus,
1126
+ .gradio-container textarea:focus,
1127
+ .gradio-container select:focus,
1128
+ .gradio-container [role="combobox"]:focus-within {
1129
+ border-color: var(--mint) !important;
1130
+ box-shadow: 0 0 0 3px rgba(0,168,107,.16) !important;
1131
+ }
1132
+ .gradio-container label {
1133
+ color: var(--mint-dark) !important;
1134
+ font-weight: 650 !important;
1135
+ }
1136
+ .gradio-container .info,
1137
+ .gradio-container small {
1138
+ color: var(--muted) !important;
1139
  }
1140
 
1141
+ /* Give Weeks and Days a real touch gap on mobile. */
1142
+ .age-row,
1143
+ .feed-row {
1144
+ gap: 10px !important;
1145
+ }
1146
+ .age-row > *,
1147
+ .feed-row > * {
1148
+ min-width: 0 !important;
1149
  }
1150
 
1151
+ .email-box textarea,
1152
+ .email-box input {
1153
+ background: #fff !important;
1154
+ border: 2px solid var(--mint) !important;
 
 
1155
  }
1156
 
1157
+ /* =========================
1158
+ PRIMARY ACTION
1159
+ ========================= */
1160
+ .calculate-btn button {
1161
+ width: 100% !important;
1162
+ min-height: 52px !important;
1163
+ border: 0 !important;
1164
+ border-radius: 12px !important;
1165
+ background: var(--mint) !important;
1166
+ color: #fff !important;
1167
+ font-size: 16px !important;
1168
+ font-weight: 800 !important;
1169
+ box-shadow: 0 5px 14px rgba(0,168,107,.20) !important;
1170
+ }
1171
+ .calculate-btn button:hover { background: #008F5A !important; }
1172
+
1173
+ /* =========================
1174
+ RESULT HTML
1175
+ ========================= */
1176
+ .results-shell {
1177
+ width: 100%;
1178
+ box-sizing: border-box;
1179
+ color: var(--mint-dark);
1180
+ }
1181
+ .buy-card {
1182
+ background: linear-gradient(145deg, #F1FBF6 0%, #FFFFFF 100%);
1183
+ border: 2px solid var(--mint);
1184
+ border-radius: 16px;
1185
+ padding: 18px 16px;
1186
+ text-align: center;
1187
+ box-shadow: 0 5px 16px rgba(11,48,34,.08);
1188
+ }
1189
+ .eyebrow {
1190
+ color: var(--mint) !important;
1191
+ font-size: 12px;
1192
+ font-weight: 800;
1193
+ letter-spacing: .09em;
1194
+ }
1195
+ .buy-label {
1196
+ margin-top: 7px;
1197
+ color: var(--mint-dark) !important;
1198
+ font-size: 13px;
1199
+ font-weight: 800;
1200
+ }
1201
+ .buy-number {
1202
+ color: var(--mint) !important;
1203
+ font-size: clamp(62px, 18vw, 92px);
1204
+ font-weight: 900;
1205
+ line-height: .9;
1206
+ margin-top: 2px;
1207
+ }
1208
+ .buy-unit {
1209
+ color: var(--mint-dark) !important;
1210
+ font-size: 21px;
1211
+ font-weight: 900;
1212
+ letter-spacing: .08em;
1213
+ }
1214
+ .buy-note {
1215
+ margin-top: 8px;
1216
+ color: var(--muted) !important;
1217
+ font-size: 12px;
1218
  }
1219
+ .result-context {
1220
+ text-align: center;
1221
+ margin: 12px 2px 6px;
1222
+ color: var(--muted) !important;
1223
+ font-size: 13px;
1224
+ }
1225
+ .result-context strong { color: var(--mint-dark) !important; }
1226
+ .plain-answer {
1227
+ background: #fff;
1228
+ border-left: 4px solid var(--yellow);
1229
+ border-radius: 10px;
1230
+ padding: 11px 12px;
1231
+ margin: 10px 0 12px;
1232
+ font-size: 14px;
1233
+ line-height: 1.5;
1234
+ color: var(--mint-dark) !important;
1235
+ }
1236
+ .plain-answer strong { color: var(--mint-dark) !important; }
1237
 
1238
+ .metric-grid {
1239
+ display: grid;
1240
+ grid-template-columns: repeat(2, minmax(0,1fr));
1241
+ gap: 9px;
1242
+ margin-bottom: 14px;
1243
+ }
1244
+ .metric-card {
1245
+ background: #fff;
1246
+ border: 1px solid var(--border);
1247
+ border-radius: 12px;
1248
+ padding: 11px 9px;
1249
+ min-width: 0;
1250
+ }
1251
+ .metric-icon {
1252
+ display: block;
1253
+ color: var(--mint) !important;
1254
+ font-size: 18px;
1255
+ font-weight: 900;
1256
+ }
1257
+ .metric-label {
1258
+ display: block;
1259
+ color: var(--muted) !important;
1260
+ font-size: 11px;
1261
+ margin: 2px 0 3px;
1262
+ }
1263
+ .metric-card strong {
1264
+ display: block;
1265
+ color: var(--mint-dark) !important;
1266
+ font-size: 17px;
1267
+ line-height: 1.2;
1268
+ overflow-wrap: anywhere;
1269
+ }
1270
+ .result-section { margin: 14px 0; }
1271
+ .section-title {
1272
+ color: var(--mint-dark) !important;
1273
+ font-size: 14px;
1274
+ font-weight: 850;
1275
+ margin-bottom: 8px;
1276
+ }
1277
+ .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
1278
+ .feed-table {
1279
+ width: 100%;
1280
+ min-width: 330px;
1281
+ border-collapse: separate;
1282
+ border-spacing: 0;
1283
+ background: #fff;
1284
+ border: 1px solid var(--border);
1285
+ border-radius: 11px;
1286
+ overflow: hidden;
1287
+ font-size: 12px;
1288
+ }
1289
+ .feed-table th {
1290
+ background: var(--mint) !important;
1291
+ color: #fff !important;
1292
+ padding: 9px 8px;
1293
+ text-align: left;
1294
+ font-weight: 800;
1295
+ }
1296
+ .feed-table th:not(:first-child),
1297
+ .feed-table td:not(:first-child) { text-align: right; }
1298
+ .feed-table td {
1299
+ padding: 9px 8px;
1300
+ border-top: 1px solid var(--border);
1301
+ color: var(--mint-dark) !important;
1302
+ white-space: nowrap;
1303
+ }
1304
+ .feed-table .muted { color: var(--muted) !important; font-size: 10px; }
1305
+ .feed-table .total-row td {
1306
+ background: var(--mint-soft) !important;
1307
+ font-weight: 900 !important;
1308
+ }
1309
+ .feed-table .total-row td:last-child { font-size: 14px; }
1310
+
1311
+ .cost-card {
1312
+ background: var(--yellow-soft);
1313
+ border: 1px solid #EBC34A;
1314
+ border-left: 4px solid var(--yellow);
1315
+ border-radius: 12px;
1316
+ padding: 13px 14px;
1317
+ margin: 14px 0;
1318
+ }
1319
+ .cost-main {
1320
+ color: var(--mint-dark) !important;
1321
+ font-size: clamp(27px, 8vw, 38px);
1322
+ line-height: 1.1;
1323
+ font-weight: 900;
1324
+ }
1325
+ .cost-note { color: var(--muted) !important; font-size: 11px; margin-top: 5px; }
1326
+
1327
+ .performance-grid { display: grid; gap: 0; background: #fff; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
1328
+ .performance-grid > div { display: flex; justify-content: space-between; gap: 12px; padding: 11px; border-top: 1px solid var(--border); font-size: 13px; }
1329
+ .performance-grid > div:first-child { border-top: 0; }
1330
+ .performance-grid span { color: var(--muted) !important; }
1331
+ .performance-grid strong { color: var(--mint) !important; white-space: nowrap; }
1332
+
1333
+ .share-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 8px; }
1334
+ .share-btn {
1335
+ display: block;
1336
+ text-align: center;
1337
+ text-decoration: none !important;
1338
+ background: var(--mint);
1339
+ color: #fff !important;
1340
+ border-radius: 9px;
1341
+ padding: 11px 8px;
1342
+ font-size: 12px;
1343
+ font-weight: 800;
1344
+ min-height: 44px;
1345
+ box-sizing: border-box;
1346
+ }
1347
+ .share-btn:hover { background: #008F5A; }
1348
+ .result-disclaimer {
1349
+ background: #F1F6F3;
1350
+ border: 1px solid var(--border);
1351
+ border-radius: 10px;
1352
+ padding: 10px 11px;
1353
+ color: var(--muted) !important;
1354
+ font-size: 10px;
1355
+ line-height: 1.45;
1356
+ margin-top: 14px;
1357
  }
1358
 
1359
+ /* =========================
1360
+ COLLAPSIBLE LOWER CONTENT
1361
+ ========================= */
1362
+ .gradio-container details,
1363
+ .gradio-container .gr-accordion {
1364
+ background: #fff !important;
1365
+ border: 1px solid var(--border) !important;
1366
+ border-radius: 12px !important;
1367
+ margin-bottom: 10px !important;
1368
+ overflow: hidden !important;
1369
+ }
1370
+ .gradio-container summary,
1371
+ .gradio-container .gr-accordion > button {
1372
+ color: var(--mint-dark) !important;
1373
+ font-weight: 800 !important;
1374
  }
1375
 
1376
+ /* Dataframes: allow horizontal scrolling on phones. */
1377
+ .benchmark-wrap { overflow-x: auto !important; -webkit-overflow-scrolling: touch !important; }
1378
+ .benchmark-wrap .table-wrap { overflow-x: auto !important; }
1379
+
1380
+ /* =========================
1381
+ ADMIN / FOOTER
1382
+ ========================= */
1383
+ .admin-login {
1384
+ background: #fff !important;
1385
+ border: 1px solid var(--border) !important;
1386
+ border-left: 4px solid var(--yellow) !important;
1387
+ border-radius: 12px !important;
1388
+ padding: 12px !important;
1389
+ }
1390
+ .footer-note { color: var(--muted) !important; font-size: 11px; line-height: 1.5; }
1391
+
1392
+ /* Desktop gets breathing room; mobile stays single-column. */
1393
+ @media (min-width: 900px) {
1394
+ .gradio-container { padding: 0 22px 60px !important; }
1395
+ .app-header { margin-left: -22px; margin-right: -22px; padding-left: 28px; padding-right: 28px; }
1396
+ .buy-number { font-size: 84px; }
1397
+ .metric-grid { grid-template-columns: repeat(4,minmax(0,1fr)); }
1398
  }
1399
 
1400
+ @media (max-width: 480px) {
1401
+ .gradio-container { padding-left: 10px !important; padding-right: 10px !important; }
1402
+ .app-header { margin-left: -10px; margin-right: -10px; }
1403
+ .age-row, .feed-row { gap: 12px !important; }
1404
+ .metric-grid { gap: 7px; }
1405
+ .metric-card { padding: 10px 8px; }
1406
+ .metric-card strong { font-size: 15px; }
1407
+ .performance-grid > div { font-size: 12px; }
1408
  }
1409
  """
1410
 
 
1412
  # UI
1413
  # ============================================================
1414
 
1415
+ with gr.Blocks(title=APP_NAME, css=CSS, theme=gr.themes.Soft()) as demo:
1416
 
1417
  gr.HTML("""
1418
+ <header class="app-header">
1419
+ <div class="brand-row">
1420
+ <div class="brand-icon">πŸ”</div>
1421
+ <div>
1422
+ <h1>How Much Feed?</h1>
1423
+ </div>
1424
+ </div>
1425
+ <p>Broiler feed calculator β€” find out how much feed to buy for your birds.</p>
1426
+ </header>
1427
  """)
1428
 
1429
+ gr.HTML("""
1430
+ <div class="status-card">
1431
+ <strong>βœ“ Simple farmer estimate</strong><br>
1432
+ Enter your breed, flock size, age and feed price. The calculator shows the feed you need and the number of bags to buy.
1433
+ </div>
1434
+ """)
1435
 
1436
+ # ============================================================
1437
+ # MOBILE-FIRST INPUT FORM
1438
+ # ============================================================
1439
+ with gr.Column(elem_classes=["mobile-form"]):
1440
+ with gr.Group(elem_classes=["form-section"]):
1441
+ gr.HTML('<div class="section-heading"><span class="step-number">1</span><span>πŸ” Breed & Birds</span></div>')
1442
+ breed = gr.Dropdown(
1443
+ choices=["Ross 308", "Cobb 500", "Arbor Acres"],
1444
+ value="Ross 308",
1445
+ label="Breed",
1446
+ info="Choose the breed of your broilers.",
1447
+ )
1448
+ birds = gr.Dropdown(
1449
+ choices=[25, 50, 75, 100, 150, 200, 500, 1000],
1450
+ value=50,
1451
+ label="Number of birds",
1452
+ allow_custom_value=True,
1453
+ )
1454
 
1455
+ with gr.Group(elem_classes=["form-section"]):
1456
+ gr.HTML('<div class="section-heading"><span class="step-number">2</span><span>πŸ“… Expected Age</span></div>')
1457
+ with gr.Row(elem_classes=["age-row"]):
1458
+ weeks = gr.Dropdown(
1459
+ choices=list(range(0, 21)), value=8, label="Weeks", scale=1
1460
  )
1461
+ days = gr.Dropdown(
1462
+ choices=list(range(0, 7)), value=0, label="Days", scale=1
 
 
 
 
1463
  )
1464
 
1465
+ with gr.Group(elem_classes=["form-section"]):
1466
+ gr.HTML('<div class="section-heading"><span class="step-number">3</span><span>🌾 Feed Details</span></div>')
1467
+ with gr.Row(elem_classes=["feed-row"]):
1468
+ bag_size = gr.Dropdown(
1469
+ choices=[10, 20, 25, 50, 100], value=25, label="Bag size (kg)", scale=1
1470
+ )
1471
+ price = gr.Number(
1472
+ value=24000, label="Price per bag (₦)", minimum=0, scale=1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1473
  )
1474
 
1475
+ with gr.Group(elem_classes=["form-section"]):
1476
+ gr.HTML('<div class="section-heading"><span class="step-number">4</span><span>🌍 Your Location</span></div>')
1477
+ detected_country = detect_country_from_env() or "NG"
1478
+ country_choices = ["Auto-Detect"] + [code for code, _ in COUNTRY_LIST]
1479
+ country_dropdown = gr.Dropdown(
1480
+ choices=country_choices,
1481
+ value="Auto-Detect",
1482
+ label="Country",
1483
+ info="Auto-Detect uses your connection location when available.",
1484
+ )
1485
+ country_code_state = gr.State(detected_country)
1486
 
1487
+ with gr.Group(elem_classes=["form-section"]):
1488
+ gr.HTML('<div class="section-heading"><span class="step-number">5</span><span>πŸ“§ Email for your result</span></div>')
1489
  email = gr.Textbox(
1490
+ label="Email address",
1491
  placeholder="farmer@example.com",
1492
  type="email",
1493
  elem_classes=["email-box"],
1494
+ info="We'll save your calculation so you can access it later.",
 
 
 
 
 
 
1495
  )
1496
 
1497
+ calculate_btn = gr.Button(
1498
+ "πŸ” Calculate Feed Requirement",
1499
+ variant="primary",
1500
+ size="lg",
1501
+ elem_classes=["calculate-btn"],
1502
+ )
1503
+ save_status = gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1504
 
1505
  # ============================================================
1506
+ # RESULTS
1507
  # ============================================================
1508
+ gr.Markdown("## πŸ“Š Your Feed Result")
1509
+ result = gr.HTML(
1510
+ value="""
1511
+ <div class="results-shell">
1512
+ <section class="buy-card">
1513
+ <div class="eyebrow">YOUR FEED RESULT</div>
1514
+ <div class="buy-label">BUY</div>
1515
+ <div class="buy-number">β€”</div>
1516
+ <div class="buy-unit">BAGS</div>
1517
+ <div class="buy-note">Enter your flock details above to calculate.</div>
1518
+ </section>
1519
+ </div>
1520
+ """,
1521
+ elem_classes=["answer-box"],
1522
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1523
 
1524
  # ============================================================
1525
+ # REFERENCE / BENCHMARKS
1526
  # ============================================================
1527
+ with gr.Accordion("πŸ“Š Selected Breed Benchmark", open=False):
1528
+ gr.Markdown("Weekly breed benchmark data used by the calculator.")
1529
+ initial_table, _ = get_benchmark_table("Ross 308")
1530
+ benchmark_table = gr.Dataframe(
1531
+ value=initial_table,
1532
+ headers=["Days", "Body Weight (kg)", "Cumulative Feed (kg)", "FCR"],
1533
+ interactive=False,
1534
+ wrap=True,
1535
+ elem_classes=["benchmark-wrap"],
1536
+ )
1537
 
1538
+ with gr.Accordion("πŸ” Breed Comparison", open=False):
1539
+ gr.Markdown("Compare Ross 308, Cobb 500 and Arbor Acres at the same age checkpoints.")
1540
+ comp_data, breeds_list, days_list = get_breed_comparison_table()
1541
+ headers = ["Days"]
1542
+ for b in breeds_list:
1543
+ headers.extend([f"{b} Weight (kg)", f"{b} Feed (kg)", f"{b} FCR"])
1544
+ comparison_table = gr.Dataframe(
1545
+ value=comp_data,
1546
+ headers=headers,
1547
+ interactive=False,
1548
+ wrap=True,
1549
+ elem_classes=["benchmark-wrap"],
1550
+ )
1551
 
1552
+ with gr.Accordion("πŸ” Common Broiler Breeds in Nigeria", open=False):
1553
+ gr.Markdown("""
1554
+ | Breed | Characteristics | Available At |
1555
+ |---|---|---|
1556
+ | **Ross 308** | Big chest, thick legs, very fast growth. | Agrited, Sayed, Valentine |
1557
+ | **Cobb 500** | Good early growth, strong legs, solid feed conversion. | Chikun, Zartech |
1558
+ | **Arbor Acres** | Balanced growth, hardy and adaptable. | Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila |
1559
+
1560
+ **Tip:** Ask the hatchery which breed you are buying before using the benchmark.
1561
+ """)
1562
+
1563
+ with gr.Accordion("πŸ“Œ How It Works & Data Sources", open=False):
1564
+ gr.Markdown(f"""
1565
+ **How the estimate works**
1566
+ - Your weeks + days are converted to total days.
1567
+ - Breed-specific cumulative feed benchmarks estimate total feed per bird.
1568
+ - Starter feed is estimated through Day {STARTER_END_DAY}; feed after that is treated as finisher.
1569
+ - Exact bags = total feed Γ· bag size.
1570
+ - Bags to buy = exact bags rounded up to the next whole bag.
1571
+ - Feed cost = bags to buy Γ— your current price per bag.
1572
+
1573
+ **Data sources**
1574
+ - Ross 308 β€” Aviagen (2022)
1575
+ - Cobb 500 β€” Cobb-Vantress
1576
+ - Arbor Acres β€” Aviagen (2022)
1577
+
1578
+ **Important:** This is a benchmark estimate. Actual feed use varies with management, feed quality, environment and bird performance.
1579
+ """)
1580
 
1581
  # ============================================================
1582
+ # ASK A QUESTION
1583
  # ============================================================
1584
+ with gr.Accordion("❓ Ask a Question", open=False):
1585
+ gr.Markdown("Ask a simple poultry-feed question. Your question can also help us improve the calculator.")
1586
+ question = gr.Textbox(
1587
+ label="Your question",
1588
+ placeholder="How many bags of feed for 100 broilers for 6 weeks?",
1589
+ lines=3,
1590
+ )
1591
+ ask_btn = gr.Button("πŸ’¬ Get Answer", variant="secondary", size="lg")
1592
+ question_answer = gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1593
 
1594
  # ============================================================
1595
+ # ADMIN
1596
  # ============================================================
1597
+ with gr.Accordion("πŸ”’ Admin Dashboard", open=False):
1598
+ gr.Markdown("Password-protected analytics and CSV exports.")
1599
+ with gr.Group(elem_classes=["admin-login"]):
1600
+ admin_password_input = gr.Textbox(
1601
+ label="Admin Password", placeholder="Enter password...", type="password"
 
 
 
 
 
 
 
 
 
1602
  )
1603
+ with gr.Row():
1604
+ login_btn = gr.Button("Login", variant="primary")
1605
+ logout_btn = gr.Button("Logout", variant="secondary", visible=False)
1606
+ admin_output = gr.Markdown("πŸ”’ Enter the admin password to view analytics.")
1607
 
1608
+ with gr.Row():
1609
+ export_leads_btn = gr.Button("πŸ“₯ Export Leads (CSV)", variant="secondary")
1610
+ export_leads_output = gr.File(label="Leads CSV")
1611
+ with gr.Row():
1612
+ export_calc_btn = gr.Button("πŸ“₯ Export Calculations (CSV)", variant="secondary")
1613
+ export_calc_output = gr.File(label="Calculations CSV")
1614
+
1615
+ gr.HTML("""
1616
+ <div class="footer-note" style="margin-top:18px; text-align:center;">
1617
+ πŸ” <strong>How Much Feed?</strong> β€” A simple broiler feed planning tool.<br>
1618
+ Feed estimates are guides and should be adjusted to actual flock performance.
1619
+ </div>
1620
+ """)
1621
 
1622
  # ============================================================
1623
  # EVENTS
1624
  # ============================================================
 
1625
  breed.change(
1626
  fn=update_benchmark,
1627
  inputs=[breed],
 
1631
  country_dropdown.change(
1632
  fn=on_country_change_handler,
1633
  inputs=[country_dropdown],
1634
+ outputs=[country_code_state, price],
1635
  )
1636
 
1637
  def calculate_wrapper(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request: gr.Request):
1638
  if not country_code_state:
1639
  country_code_state = "NG"
1640
+ return calculate_feed(
1641
+ birds, weeks, days, bag_size, price_per_bag,
1642
+ email, country_code_state, breed, request
1643
+ )
1644
 
1645
  calculate_btn.click(
1646
  fn=calculate_wrapper,
 
1654
  outputs=[question_answer],
1655
  )
1656
 
1657
+ login_btn.click(
1658
+ fn=admin_login,
1659
+ inputs=[admin_password_input],
1660
+ outputs=[admin_output, admin_password_input, logout_btn],
1661
  )
1662
 
1663
+ logout_btn.click(
1664
+ fn=logout_admin,
1665
+ inputs=[],
1666
+ outputs=[admin_output, admin_password_input, logout_btn],
1667
+ )
1668
+
1669
+ export_leads_btn.click(
1670
+ fn=export_leads_wrapper,
1671
+ inputs=[],
1672
+ outputs=[export_leads_output],
1673
+ )
1674
+
1675
+ export_calc_btn.click(
1676
+ fn=export_calculations_wrapper,
1677
+ inputs=[],
1678
+ outputs=[export_calc_output],
1679
+ )
1680
+
1681
+ demo.load(fn=track_visit, inputs=None, outputs=None)
1682
+
1683
 
1684
  # ============================================================
1685
  # START APP