Files changed (1) hide show
  1. app.py +0 -1954
app.py DELETED
@@ -1,1954 +0,0 @@
1
- import os
2
- import html
3
- import uuid
4
- import tempfile
5
- from datetime import datetime
6
- from urllib.parse import quote
7
-
8
- import numpy as np
9
- import pandas as pd
10
- import gradio as gr
11
-
12
- from sentence_transformers import SentenceTransformer
13
- from transformers import pipeline
14
-
15
- from reportlab.lib import colors
16
- from reportlab.lib.pagesizes import A4
17
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
18
- from reportlab.lib.units import cm
19
- from reportlab.platypus import (
20
- SimpleDocTemplate,
21
- Paragraph,
22
- Spacer,
23
- Table,
24
- TableStyle,
25
- PageBreak
26
- )
27
-
28
-
29
- # ============================================================
30
- # HOPEPET AI - SETTINGS
31
- # ============================================================
32
-
33
- APP_NAME = "HopePet AI"
34
-
35
- DATASET_PATH = "HopePet_dataset.csv"
36
- HF_DATASET_REPO = os.getenv("HF_DATASET_REPO", "").strip()
37
-
38
- EMBEDDINGS_PATH = "HopePet_embeddings.npy"
39
-
40
- WINNING_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
41
- GENERATION_MODEL = "google/flan-t5-base"
42
-
43
- TOP_K = 3
44
-
45
-
46
- # ============================================================
47
- # LOAD DATA
48
- # ============================================================
49
-
50
- def load_data():
51
- if HF_DATASET_REPO:
52
- from datasets import load_dataset
53
-
54
- dataset = load_dataset(HF_DATASET_REPO)
55
- split_name = "train" if "train" in dataset else list(dataset.keys())[0]
56
- data = dataset[split_name].to_pandas()
57
- else:
58
- if not os.path.exists(DATASET_PATH):
59
- raise FileNotFoundError(
60
- f"Dataset file was not found: {DATASET_PATH}. "
61
- "Please upload HopePet_dataset.csv to the Space."
62
- )
63
- data = pd.read_csv(DATASET_PATH)
64
-
65
- data.columns = data.columns.str.strip()
66
-
67
- optional_columns = ["secondary_symptoms", "pain_signs", "emergency_signs"]
68
-
69
- for col in optional_columns:
70
- if col in data.columns:
71
- data[col] = data[col].fillna("None")
72
-
73
- for col in data.select_dtypes(include="object").columns:
74
- data[col] = data[col].fillna("Unknown").astype(str).str.strip()
75
-
76
- if "retrieval_text" not in data.columns:
77
- raise ValueError("The dataset must contain a column named 'retrieval_text'.")
78
-
79
- return data
80
-
81
-
82
- df = load_data()
83
-
84
-
85
- # ============================================================
86
- # LOAD MODELS
87
- # ============================================================
88
-
89
- embedder = SentenceTransformer(WINNING_EMBEDDING_MODEL)
90
-
91
- try:
92
- generator = pipeline(
93
- "text2text-generation",
94
- model=GENERATION_MODEL
95
- )
96
- except Exception:
97
- generator = None
98
-
99
-
100
- # ============================================================
101
- # LOAD OR CREATE EMBEDDINGS
102
- # ============================================================
103
-
104
- def load_or_create_embeddings():
105
- if os.path.exists(EMBEDDINGS_PATH):
106
- embeddings = np.load(EMBEDDINGS_PATH)
107
-
108
- if len(embeddings) == len(df):
109
- return embeddings
110
-
111
- texts = df["retrieval_text"].astype(str).tolist()
112
-
113
- embeddings = embedder.encode(
114
- texts,
115
- batch_size=64,
116
- show_progress_bar=True,
117
- normalize_embeddings=True
118
- )
119
-
120
- embeddings = np.array(embeddings)
121
-
122
- try:
123
- np.save(EMBEDDINGS_PATH, embeddings)
124
- except Exception:
125
- pass
126
-
127
- return embeddings
128
-
129
-
130
- dataset_embeddings = load_or_create_embeddings()
131
-
132
-
133
- # ============================================================
134
- # BASIC HELPERS
135
- # ============================================================
136
-
137
- def h(value):
138
- return html.escape(str(value))
139
-
140
-
141
- def clean_value(value):
142
- if value is None:
143
- return "Unknown"
144
-
145
- if isinstance(value, list):
146
- cleaned = [
147
- str(v).strip()
148
- for v in value
149
- if str(v).strip().lower() not in ["", "none", "nan"]
150
- ]
151
- return ", ".join(cleaned) if cleaned else "None"
152
-
153
- value = str(value).strip()
154
- return value if value else "Unknown"
155
-
156
-
157
- def get_choices(column_name, fallback, max_values=40):
158
- if column_name not in df.columns:
159
- return fallback
160
-
161
- values = (
162
- df[column_name]
163
- .dropna()
164
- .astype(str)
165
- .str.strip()
166
- .unique()
167
- .tolist()
168
- )
169
-
170
- values = [
171
- v for v in values
172
- if v and v.lower() not in ["nan", "none", "unknown"]
173
- ]
174
-
175
- values = sorted(values)[:max_values]
176
- final_values = fallback + values
177
- final_values = list(dict.fromkeys(final_values))
178
-
179
- return final_values
180
-
181
-
182
- def sentence_case_pet(pet_type, pet_age_group):
183
- return f"{str(pet_age_group).lower()} {str(pet_type).lower()}"
184
-
185
-
186
- # ============================================================
187
- # RETRIEVAL SYSTEM
188
- # ============================================================
189
-
190
- def build_user_case_text(
191
- pet_type,
192
- pet_age_group,
193
- pet_sex,
194
- breed_size,
195
- vaccination_status,
196
- environment,
197
- medical_background,
198
- recent_change,
199
- main_symptom,
200
- secondary_symptoms,
201
- symptom_duration,
202
- appetite_status,
203
- water_intake,
204
- energy_level,
205
- pain_signs,
206
- emergency_signs,
207
- previous_occurrence,
208
- user_goal,
209
- free_text
210
- ):
211
- text = f"""
212
- Pet type: {pet_type}.
213
- Age group: {pet_age_group}.
214
- Sex: {pet_sex}.
215
- Breed size: {breed_size}.
216
- Vaccination status: {vaccination_status}.
217
- Environment: {environment}.
218
- Medical background: {medical_background}.
219
- Recent change: {recent_change}.
220
- Main symptom: {main_symptom}.
221
- Secondary symptoms: {clean_value(secondary_symptoms)}.
222
- Symptom duration: {symptom_duration}.
223
- Appetite status: {appetite_status}.
224
- Water intake: {water_intake}.
225
- Energy level: {energy_level}.
226
- Pain signs: {clean_value(pain_signs)}.
227
- Emergency signs: {clean_value(emergency_signs)}.
228
- Previous occurrence: {previous_occurrence}.
229
- User goal: {user_goal}.
230
- User description: {free_text}.
231
- """
232
-
233
- return " ".join(text.split())
234
-
235
-
236
- def retrieve_similar_cases(query_text, top_k=3):
237
- query_embedding = embedder.encode(
238
- [query_text],
239
- normalize_embeddings=True
240
- )[0]
241
-
242
- similarities = np.dot(dataset_embeddings, query_embedding)
243
- top_indices = similarities.argsort()[::-1][:top_k]
244
-
245
- similar_cases = df.iloc[top_indices].copy()
246
- similar_cases["similarity_score"] = similarities[top_indices]
247
-
248
- return similar_cases
249
-
250
-
251
- def weighted_vote(similar_cases, column_name):
252
- if column_name not in similar_cases.columns:
253
- return "Unknown"
254
-
255
- scores = {}
256
-
257
- for _, row in similar_cases.iterrows():
258
- value = str(row[column_name])
259
- score = float(row["similarity_score"])
260
- scores[value] = scores.get(value, 0) + score
261
-
262
- return max(scores, key=scores.get)
263
-
264
-
265
- def get_first_value(similar_cases, column_name, fallback="Not available"):
266
- if column_name in similar_cases.columns:
267
- value = similar_cases.iloc[0][column_name]
268
- if pd.notna(value):
269
- return str(value)
270
-
271
- return fallback
272
-
273
-
274
- # ============================================================
275
- # RISK AND URGENCY LOGIC
276
- # ============================================================
277
-
278
- def emergency_override(main_symptom, emergency_signs, energy_level, pain_signs):
279
- combined_text = " ".join([
280
- clean_value(main_symptom).lower(),
281
- clean_value(emergency_signs).lower(),
282
- clean_value(energy_level).lower(),
283
- clean_value(pain_signs).lower()
284
- ])
285
-
286
- emergency_keywords = [
287
- "difficulty breathing",
288
- "seizure",
289
- "collapse",
290
- "uncontrolled bleeding",
291
- "blue or pale gums",
292
- "cannot stand",
293
- "severe weakness"
294
- ]
295
-
296
- return any(keyword in combined_text for keyword in emergency_keywords)
297
-
298
-
299
- def calculate_risk_score(
300
- pet_age_group,
301
- main_symptom,
302
- appetite_status,
303
- water_intake,
304
- energy_level,
305
- pain_signs,
306
- emergency_signs,
307
- symptom_duration
308
- ):
309
- risk = 0
310
-
311
- main_symptom_text = clean_value(main_symptom).lower()
312
- appetite_text = clean_value(appetite_status).lower()
313
- water_text = clean_value(water_intake).lower()
314
- energy_text = clean_value(energy_level).lower()
315
- pain_text = clean_value(pain_signs).lower()
316
- emergency_text = clean_value(emergency_signs).lower()
317
- duration_text = clean_value(symptom_duration).lower()
318
-
319
- if pet_age_group in ["Senior", "Puppy", "Kitten"]:
320
- risk += 1
321
-
322
- if "difficulty breathing" in main_symptom_text:
323
- risk += 4
324
- elif "bleeding" in main_symptom_text:
325
- risk += 2
326
- elif "not eating" in main_symptom_text:
327
- risk += 1
328
- elif "vomiting" in main_symptom_text or "diarrhea" in main_symptom_text:
329
- risk += 1
330
- elif "limping" in main_symptom_text:
331
- risk += 1
332
-
333
- if "not eating" in appetite_text:
334
- risk += 2
335
- elif "reduced" in appetite_text:
336
- risk += 1
337
-
338
- if "not drinking" in water_text:
339
- risk += 2
340
- elif "less" in water_text or "more" in water_text:
341
- risk += 1
342
-
343
- if "cannot stand" in energy_text:
344
- risk += 4
345
- elif "very tired" in energy_text:
346
- risk += 2
347
- elif "slightly tired" in energy_text or "restless" in energy_text:
348
- risk += 1
349
-
350
- if pain_text not in ["none", "unknown", "", "nan"]:
351
- risk += 2
352
-
353
- if emergency_text not in ["none", "unknown", "", "nan"]:
354
- risk += 3
355
-
356
- if "more than a week" in duration_text:
357
- risk += 2
358
- elif "more than 3 days" in duration_text:
359
- risk += 1
360
-
361
- return min(risk, 10)
362
-
363
-
364
- def urgency_from_risk_score(risk_score, main_symptom, emergency_signs, energy_level, pain_signs):
365
- if emergency_override(main_symptom, emergency_signs, energy_level, pain_signs):
366
- return "Emergency", max(risk_score, 7)
367
-
368
- if risk_score <= 2:
369
- return "Low", risk_score
370
- elif risk_score <= 4:
371
- return "Medium", risk_score
372
- elif risk_score <= 6:
373
- return "High", risk_score
374
- else:
375
- return "Emergency", risk_score
376
-
377
-
378
- def urgency_color(urgency):
379
- colors_map = {
380
- "Low": "#B8E0D2",
381
- "Medium": "#FFD6A5",
382
- "High": "#F7A8B8",
383
- "Emergency": "#FF8FAB"
384
- }
385
-
386
- return colors_map.get(urgency, "#BDE0FE")
387
-
388
-
389
- def urgency_icon(urgency):
390
- icons = {
391
- "Low": "🟢",
392
- "Medium": "🟡",
393
- "High": "🟠",
394
- "Emergency": "🔴"
395
- }
396
-
397
- return icons.get(urgency, "🐾")
398
-
399
-
400
- # ============================================================
401
- # ACTION PLAN
402
- # ============================================================
403
-
404
- def build_situation_summary(
405
- pet_type,
406
- pet_age_group,
407
- main_symptom,
408
- secondary_symptoms,
409
- symptom_duration,
410
- appetite_status,
411
- water_intake,
412
- energy_level,
413
- emergency_signs
414
- ):
415
- pet_label = sentence_case_pet(pet_type, pet_age_group)
416
- secondary = clean_value(secondary_symptoms)
417
- emergency = clean_value(emergency_signs)
418
-
419
- summary = (
420
- f"Your {pet_label} is showing {str(main_symptom).lower()} for {str(symptom_duration).lower()}. "
421
- f"Appetite is reported as {str(appetite_status).lower()}, water intake as {str(water_intake).lower()}, "
422
- f"and energy level as {str(energy_level).lower()}."
423
- )
424
-
425
- if secondary != "None":
426
- summary += f" Additional signs include: {secondary}."
427
-
428
- if emergency != "None":
429
- summary += f" Emergency signs were reported: {emergency}."
430
- else:
431
- summary += " No emergency signs were reported."
432
-
433
- return summary
434
-
435
-
436
- def build_do_now_steps(urgency, pet_type, main_symptom, appetite_status):
437
- pet_word = str(pet_type).lower()
438
- symptom_text = str(main_symptom).lower()
439
-
440
- steps = [
441
- f"Keep your {pet_word} in a calm, comfortable, and safe place.",
442
- "Offer fresh water and monitor whether your pet drinks normally.",
443
- "Watch for changes in breathing, energy, appetite, pain, vomiting, diarrhea, or behavior."
444
- ]
445
-
446
- if "not eating" in symptom_text or "not eating" in str(appetite_status).lower():
447
- steps.append("Do not force food; monitor appetite and contact a veterinarian if not eating continues.")
448
-
449
- if "shaking" in symptom_text or "anxiety" in symptom_text or "hiding" in symptom_text:
450
- steps.append("Reduce noise and stress, and allow your pet to rest in a quiet area.")
451
-
452
- if "limping" in symptom_text:
453
- steps.append("Limit running, jumping, and stairs until the pet is checked or improves.")
454
-
455
- if "scratching" in symptom_text:
456
- steps.append("Prevent excessive scratching and check the skin for redness, wounds, or swelling.")
457
-
458
- if urgency == "High":
459
- steps.append("Contact a veterinarian today for professional guidance.")
460
- elif urgency == "Emergency":
461
- steps = [
462
- "Seek emergency veterinary care immediately.",
463
- "Keep your pet as calm and still as possible while arranging urgent care.",
464
- "Do not wait to see if severe emergency signs improve on their own."
465
- ]
466
-
467
- return steps
468
-
469
-
470
- def build_avoid_steps(urgency):
471
- steps = [
472
- "Do not give human medication unless a veterinarian specifically tells you to.",
473
- "Do not ignore worsening symptoms or sudden changes in breathing, energy, or behavior.",
474
- "Do not force food or water if your pet is struggling, vomiting repeatedly, or seems very weak."
475
- ]
476
-
477
- if urgency in ["High", "Emergency"]:
478
- steps.append("Do not delay contacting a veterinarian if the condition looks serious or is getting worse.")
479
-
480
- return steps
481
-
482
-
483
- def build_vet_contact_message(urgency):
484
- if urgency == "Low":
485
- return "Contact a veterinarian if the symptoms continue, become worse, or if new warning signs appear."
486
- if urgency == "Medium":
487
- return "Contact a veterinarian if there is no improvement soon, if symptoms continue, or if your pet becomes weaker."
488
- if urgency == "High":
489
- return "Contact a veterinarian today. The signs are concerning enough to need professional guidance."
490
- return "Contact an emergency veterinarian or veterinary clinic immediately."
491
-
492
-
493
- def build_why_message(category, risk_score, similar_cases):
494
- top_symptom = get_first_value(similar_cases, "main_symptom", "similar symptoms")
495
- top_category = get_first_value(similar_cases, "problem_category", category)
496
- top_urgency = get_first_value(similar_cases, "urgency_level", "Unknown")
497
-
498
- return (
499
- f"HopePet AI combined the structured information you entered with a rule-based risk score "
500
- f"and retrieved similar cases from the dataset. The closest cases involved '{top_symptom}', "
501
- f"with dataset category '{top_category}' and dataset urgency '{top_urgency}'. "
502
- f"The final urgency shown here is based mainly on the calculated risk score ({risk_score}/10) "
503
- f"and emergency safety logic."
504
- )
505
-
506
-
507
- def generate_short_ai_note(query_text, urgency, category):
508
- if generator is None:
509
- return ""
510
-
511
- prompt = f"""
512
- You are HopePet AI, a responsible pet-care assistant.
513
- Do not diagnose. Do not replace a veterinarian.
514
- Write two short, calm sentences for the pet owner.
515
- Mention the urgency level and safe next step.
516
-
517
- Case:
518
- {query_text}
519
-
520
- Category: {category}
521
- Urgency: {urgency}
522
- """
523
-
524
- try:
525
- generated = generator(
526
- prompt,
527
- max_new_tokens=80,
528
- do_sample=False
529
- )[0]["generated_text"].strip()
530
-
531
- if len(generated) < 25:
532
- return ""
533
-
534
- return generated
535
-
536
- except Exception:
537
- return ""
538
-
539
-
540
- def create_action_plan_html(action_plan):
541
- urgency = action_plan["urgency"]
542
- color = urgency_color(urgency)
543
- icon = urgency_icon(urgency)
544
-
545
- do_now_html = "".join([f"<li>{h(step)}</li>" for step in action_plan["do_now"]])
546
- avoid_html = "".join([f"<li>{h(step)}</li>" for step in action_plan["avoid"]])
547
-
548
- ai_note_block = ""
549
- if action_plan["ai_note"]:
550
- ai_note_block = f"""
551
- <div class="plan-box soft-purple">
552
- <div class="plan-label">✨ AI-Written Note</div>
553
- <p>{h(action_plan["ai_note"])}</p>
554
- </div>
555
- """
556
-
557
- return f"""
558
- <div class="action-plan-card">
559
-
560
- <div class="plan-header">
561
- <div class="plan-kicker">Personalized pet-care guidance</div>
562
- <h2>🐾 HopePet AI Action Plan</h2>
563
- <p>Clear first-step guidance based on your pet's details, similar cases, embeddings, and safety logic.</p>
564
- </div>
565
-
566
- <div class="urgency-panel" style="background:{color};">
567
- <div class="urgency-main">{icon} {h(urgency)}</div>
568
- <div class="urgency-sub">Risk Score: {h(action_plan["risk_score"])}/10</div>
569
- </div>
570
-
571
- <div class="plan-box">
572
- <div class="plan-label">1. Situation Summary</div>
573
- <p>{h(action_plan["situation_summary"])}</p>
574
- </div>
575
-
576
- <div class="plan-grid">
577
- <div class="plan-box">
578
- <div class="plan-label">2. Estimated Category</div>
579
- <p><b>{h(action_plan["category"])}</b></p>
580
- </div>
581
-
582
- <div class="plan-box">
583
- <div class="plan-label">3. Recommended Next Step</div>
584
- <p>{h(action_plan["vet_contact"])}</p>
585
- </div>
586
- </div>
587
-
588
- <div class="plan-box soft-blue">
589
- <div class="plan-label">4. What You Should Do Now</div>
590
- <ul>{do_now_html}</ul>
591
- </div>
592
-
593
- <div class="plan-box soft-pink">
594
- <div class="plan-label">5. What You Should Avoid</div>
595
- <ul>{avoid_html}</ul>
596
- </div>
597
-
598
- <div class="plan-box warning-plan">
599
- <div class="plan-label">6. When to Contact a Vet</div>
600
- <p>{h(action_plan["vet_contact"])}</p>
601
- </div>
602
-
603
- <div class="plan-box">
604
- <div class="plan-label">7. Why HopePet Suggests This</div>
605
- <p>{h(action_plan["why"])}</p>
606
- </div>
607
-
608
- {ai_note_block}
609
-
610
- <div class="disclaimer-card">
611
- HopePet AI does not provide a medical diagnosis and does not replace a veterinarian.
612
- </div>
613
-
614
- </div>
615
- """
616
-
617
-
618
- # ============================================================
619
- # SIMILAR CASES DISPLAY
620
- # ============================================================
621
-
622
- def create_similar_cases_html(similar_cases):
623
- cards = ""
624
-
625
- for i, (_, row) in enumerate(similar_cases.iterrows(), start=1):
626
- pet = f"{row.get('pet_age_group', 'Unknown')} {row.get('pet_type', 'Unknown')}"
627
- similarity = float(row.get("similarity_score", 0)) * 100
628
-
629
- cards += f"""
630
- <div class="similar-case-card">
631
- <div class="similar-top">
632
- <div>
633
- <div class="case-number">Similar Case {i}</div>
634
- <h3>{h(pet)}</h3>
635
- </div>
636
- <div class="similar-score">{similarity:.1f}% match</div>
637
- </div>
638
-
639
- <div class="case-grid">
640
- <div><b>Main symptom:</b><br>{h(row.get("main_symptom", "Unknown"))}</div>
641
- <div><b>Secondary symptoms:</b><br>{h(row.get("secondary_symptoms", "None"))}</div>
642
- <div><b>Duration:</b><br>{h(row.get("symptom_duration", "Unknown"))}</div>
643
- <div><b>Appetite:</b><br>{h(row.get("appetite_status", "Unknown"))}</div>
644
- <div><b>Water intake:</b><br>{h(row.get("water_intake", "Unknown"))}</div>
645
- <div><b>Energy level:</b><br>{h(row.get("energy_level", "Unknown"))}</div>
646
- <div><b>Dataset category:</b><br>{h(row.get("problem_category", "Unknown"))}</div>
647
- <div><b>Dataset urgency:</b><br>{h(row.get("urgency_level", "Unknown"))}</div>
648
- </div>
649
-
650
- <div class="case-section">
651
- <b>Recommended next step:</b>
652
- <p>{h(row.get("recommended_next_step", "Not available"))}</p>
653
- </div>
654
-
655
- <div class="case-section">
656
- <b>Triage reason:</b>
657
- <p>{h(row.get("triage_reason", "Not available"))}</p>
658
- </div>
659
-
660
- <div class="case-section">
661
- <b>Safe first steps:</b>
662
- <p>{h(row.get("safe_first_steps", "Not available"))}</p>
663
- </div>
664
- </div>
665
- """
666
-
667
- return f"""
668
- <div class="similar-wrapper">
669
- <h2>🔍 Top 3 Similar Cases</h2>
670
- <p class="muted">
671
- These are the closest synthetic cases found by the recommendation system.
672
- They are shown as case snapshots instead of only showing an ID.
673
- </p>
674
- {cards}
675
- </div>
676
- """
677
-
678
-
679
- # ============================================================
680
- # PDF REPORT
681
- # ============================================================
682
-
683
- def pdf_paragraph(text, style):
684
- safe_text = html.escape(str(text)).replace("\n", "<br/>")
685
- return Paragraph(safe_text, style)
686
-
687
-
688
- def create_pdf_report(user_inputs, action_plan, similar_cases):
689
- filename = f"HopePet_AI_Report_{uuid.uuid4().hex[:8]}.pdf"
690
- pdf_path = os.path.join(tempfile.gettempdir(), filename)
691
-
692
- doc = SimpleDocTemplate(
693
- pdf_path,
694
- pagesize=A4,
695
- rightMargin=1.5 * cm,
696
- leftMargin=1.5 * cm,
697
- topMargin=1.4 * cm,
698
- bottomMargin=1.4 * cm
699
- )
700
-
701
- styles = getSampleStyleSheet()
702
-
703
- styles.add(ParagraphStyle(
704
- name="HopeTitle",
705
- parent=styles["Title"],
706
- fontName="Helvetica-Bold",
707
- fontSize=24,
708
- textColor=colors.HexColor("#1F2940"),
709
- spaceAfter=12
710
- ))
711
-
712
- styles.add(ParagraphStyle(
713
- name="HopeHeading",
714
- parent=styles["Heading2"],
715
- fontName="Helvetica-Bold",
716
- fontSize=15,
717
- textColor=colors.HexColor("#6C4A78"),
718
- spaceBefore=14,
719
- spaceAfter=8
720
- ))
721
-
722
- styles.add(ParagraphStyle(
723
- name="HopeBody",
724
- parent=styles["BodyText"],
725
- fontName="Helvetica",
726
- fontSize=10.5,
727
- leading=15,
728
- textColor=colors.HexColor("#333333"),
729
- spaceAfter=6
730
- ))
731
-
732
- styles.add(ParagraphStyle(
733
- name="HopeSmall",
734
- parent=styles["BodyText"],
735
- fontName="Helvetica",
736
- fontSize=9,
737
- leading=13,
738
- textColor=colors.HexColor("#555555")
739
- ))
740
-
741
- story = []
742
-
743
- story.append(pdf_paragraph("HopePet AI Pet Care Report", styles["HopeTitle"]))
744
- story.append(pdf_paragraph(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M')}", styles["HopeSmall"]))
745
- story.append(pdf_paragraph(
746
- "This report summarizes the information entered by the user, the HopePet AI Action Plan, and the most similar retrieved cases.",
747
- styles["HopeBody"]
748
- ))
749
- story.append(Spacer(1, 10))
750
-
751
- story.append(pdf_paragraph("1. Pet Profile", styles["HopeHeading"]))
752
-
753
- profile_items = [
754
- ["Pet type", user_inputs["pet_type"]],
755
- ["Age group", user_inputs["pet_age_group"]],
756
- ["Sex", user_inputs["pet_sex"]],
757
- ["Breed size", user_inputs["breed_size"]],
758
- ["Vaccination status", user_inputs["vaccination_status"]],
759
- ["Environment", user_inputs["environment"]],
760
- ["Medical background", user_inputs["medical_background"]],
761
- ["Recent change", user_inputs["recent_change"]],
762
- ]
763
-
764
- profile_table = Table(profile_items, colWidths=[5 * cm, 10 * cm])
765
- profile_table.setStyle(TableStyle([
766
- ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#EAF6FF")),
767
- ("BACKGROUND", (1, 0), (1, -1), colors.HexColor("#FFF9FB")),
768
- ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#DDDDDD")),
769
- ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
770
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
771
- ("LEFTPADDING", (0, 0), (-1, -1), 8),
772
- ("RIGHTPADDING", (0, 0), (-1, -1), 8),
773
- ("TOPPADDING", (0, 0), (-1, -1), 6),
774
- ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
775
- ]))
776
- story.append(profile_table)
777
-
778
- story.append(pdf_paragraph("2. Symptoms and Context", styles["HopeHeading"]))
779
-
780
- symptom_items = [
781
- ["Main symptom", user_inputs["main_symptom"]],
782
- ["Secondary symptoms", clean_value(user_inputs["secondary_symptoms"])],
783
- ["Symptom duration", user_inputs["symptom_duration"]],
784
- ["Appetite status", user_inputs["appetite_status"]],
785
- ["Water intake", user_inputs["water_intake"]],
786
- ["Energy level", user_inputs["energy_level"]],
787
- ["Pain signs", clean_value(user_inputs["pain_signs"])],
788
- ["Emergency signs", clean_value(user_inputs["emergency_signs"])],
789
- ["Previous occurrence", user_inputs["previous_occurrence"]],
790
- ["User goal", user_inputs["user_goal"]],
791
- ["Free text description", user_inputs["free_text"] if user_inputs["free_text"] else "None"],
792
- ]
793
-
794
- symptom_table = Table(symptom_items, colWidths=[5 * cm, 10 * cm])
795
- symptom_table.setStyle(TableStyle([
796
- ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#FDEAF1")),
797
- ("BACKGROUND", (1, 0), (1, -1), colors.HexColor("#FFFFFF")),
798
- ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#DDDDDD")),
799
- ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
800
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
801
- ("LEFTPADDING", (0, 0), (-1, -1), 8),
802
- ("RIGHTPADDING", (0, 0), (-1, -1), 8),
803
- ("TOPPADDING", (0, 0), (-1, -1), 6),
804
- ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
805
- ]))
806
- story.append(symptom_table)
807
-
808
- story.append(pdf_paragraph("3. HopePet AI Action Plan", styles["HopeHeading"]))
809
- story.append(pdf_paragraph(f"Situation summary: {action_plan['situation_summary']}", styles["HopeBody"]))
810
- story.append(pdf_paragraph(f"Estimated category: {action_plan['category']}", styles["HopeBody"]))
811
- story.append(pdf_paragraph(f"Estimated urgency: {action_plan['urgency']}", styles["HopeBody"]))
812
- story.append(pdf_paragraph(f"Risk score: {action_plan['risk_score']}/10", styles["HopeBody"]))
813
-
814
- story.append(pdf_paragraph("What you should do now:", styles["HopeBody"]))
815
- for step in action_plan["do_now"]:
816
- story.append(pdf_paragraph(f"- {step}", styles["HopeBody"]))
817
-
818
- story.append(pdf_paragraph("What you should avoid:", styles["HopeBody"]))
819
- for step in action_plan["avoid"]:
820
- story.append(pdf_paragraph(f"- {step}", styles["HopeBody"]))
821
-
822
- story.append(pdf_paragraph(f"When to contact a vet: {action_plan['vet_contact']}", styles["HopeBody"]))
823
- story.append(pdf_paragraph(f"Why HopePet suggests this: {action_plan['why']}", styles["HopeBody"]))
824
-
825
- if action_plan["ai_note"]:
826
- story.append(pdf_paragraph(f"AI-written note: {action_plan['ai_note']}", styles["HopeBody"]))
827
-
828
- story.append(PageBreak())
829
- story.append(pdf_paragraph("4. Similar Retrieved Cases", styles["HopeHeading"]))
830
-
831
- for i, (_, row) in enumerate(similar_cases.iterrows(), start=1):
832
- similarity = float(row.get("similarity_score", 0)) * 100
833
-
834
- story.append(pdf_paragraph(f"Similar Case {i} - {similarity:.1f}% match", styles["HopeHeading"]))
835
- story.append(pdf_paragraph(
836
- f"Pet: {row.get('pet_age_group', 'Unknown')} {row.get('pet_type', 'Unknown')}",
837
- styles["HopeBody"]
838
- ))
839
- story.append(pdf_paragraph(f"Main symptom: {row.get('main_symptom', 'Unknown')}", styles["HopeBody"]))
840
- story.append(pdf_paragraph(f"Secondary symptoms: {row.get('secondary_symptoms', 'None')}", styles["HopeBody"]))
841
- story.append(pdf_paragraph(f"Duration: {row.get('symptom_duration', 'Unknown')}", styles["HopeBody"]))
842
- story.append(pdf_paragraph(f"Appetite: {row.get('appetite_status', 'Unknown')}", styles["HopeBody"]))
843
- story.append(pdf_paragraph(f"Energy level: {row.get('energy_level', 'Unknown')}", styles["HopeBody"]))
844
- story.append(pdf_paragraph(f"Dataset category: {row.get('problem_category', 'Unknown')}", styles["HopeBody"]))
845
- story.append(pdf_paragraph(f"Dataset urgency: {row.get('urgency_level', 'Unknown')}", styles["HopeBody"]))
846
- story.append(pdf_paragraph(f"Recommended next step: {row.get('recommended_next_step', 'Not available')}", styles["HopeBody"]))
847
- story.append(pdf_paragraph(f"Triage reason: {row.get('triage_reason', 'Not available')}", styles["HopeBody"]))
848
- story.append(Spacer(1, 8))
849
-
850
- story.append(pdf_paragraph("5. Safety Disclaimer", styles["HopeHeading"]))
851
- story.append(pdf_paragraph(
852
- "HopePet AI does not provide a medical diagnosis and does not replace a veterinarian. "
853
- "If your pet has emergency signs, becomes worse, or you are unsure, contact a veterinarian or emergency veterinary clinic.",
854
- styles["HopeBody"]
855
- ))
856
-
857
- doc.build(story)
858
-
859
- return pdf_path
860
-
861
-
862
- # ============================================================
863
- # WHATSAPP SHARE
864
- # ============================================================
865
-
866
- def create_whatsapp_html(user_inputs, action_plan):
867
- step_1 = action_plan["do_now"][0] if len(action_plan["do_now"]) > 0 else ""
868
- step_2 = action_plan["do_now"][1] if len(action_plan["do_now"]) > 1 else ""
869
- step_3 = action_plan["do_now"][2] if len(action_plan["do_now"]) > 2 else ""
870
-
871
- message = f"""🐾 HopePet AI Action Plan
872
-
873
- Pet: {user_inputs['pet_age_group']} {user_inputs['pet_type']}
874
- Main symptom: {user_inputs['main_symptom']}
875
- Urgency: {action_plan['urgency']}
876
- Risk Score: {action_plan['risk_score']}/10
877
-
878
- Situation:
879
- {action_plan['situation_summary']}
880
-
881
- What to do now:
882
- - {step_1}
883
- - {step_2}
884
- - {step_3}
885
-
886
- When to contact a vet:
887
- {action_plan['vet_contact']}
888
-
889
- Important:
890
- HopePet AI does not replace a veterinarian.
891
- """
892
-
893
- url = "https://wa.me/?text=" + quote(message)
894
-
895
- return f"""
896
- <div class="share-card">
897
- <h2>📲 Share on WhatsApp</h2>
898
- <p>Share a short version of the HopePet AI Action Plan through WhatsApp.</p>
899
- <a class="whatsapp-button" href="{url}" target="_blank">
900
- 📲 Share Action Plan on WhatsApp
901
- </a>
902
- </div>
903
- """
904
-
905
-
906
- # ============================================================
907
- # MAIN ANALYSIS FUNCTION
908
- # ============================================================
909
-
910
- def analyze_case(
911
- pet_type,
912
- pet_age_group,
913
- pet_sex,
914
- breed_size,
915
- vaccination_status,
916
- environment,
917
- medical_background,
918
- recent_change,
919
- main_symptom,
920
- secondary_symptoms,
921
- symptom_duration,
922
- appetite_status,
923
- water_intake,
924
- energy_level,
925
- pain_signs,
926
- emergency_signs,
927
- previous_occurrence,
928
- user_goal,
929
- free_text
930
- ):
931
- user_inputs = {
932
- "pet_type": pet_type,
933
- "pet_age_group": pet_age_group,
934
- "pet_sex": pet_sex,
935
- "breed_size": breed_size,
936
- "vaccination_status": vaccination_status,
937
- "environment": environment,
938
- "medical_background": medical_background,
939
- "recent_change": recent_change,
940
- "main_symptom": main_symptom,
941
- "secondary_symptoms": secondary_symptoms,
942
- "symptom_duration": symptom_duration,
943
- "appetite_status": appetite_status,
944
- "water_intake": water_intake,
945
- "energy_level": energy_level,
946
- "pain_signs": pain_signs,
947
- "emergency_signs": emergency_signs,
948
- "previous_occurrence": previous_occurrence,
949
- "user_goal": user_goal,
950
- "free_text": free_text
951
- }
952
-
953
- query_text = build_user_case_text(
954
- pet_type,
955
- pet_age_group,
956
- pet_sex,
957
- breed_size,
958
- vaccination_status,
959
- environment,
960
- medical_background,
961
- recent_change,
962
- main_symptom,
963
- secondary_symptoms,
964
- symptom_duration,
965
- appetite_status,
966
- water_intake,
967
- energy_level,
968
- pain_signs,
969
- emergency_signs,
970
- previous_occurrence,
971
- user_goal,
972
- free_text
973
- )
974
-
975
- similar_cases = retrieve_similar_cases(query_text, TOP_K)
976
- category = weighted_vote(similar_cases, "problem_category")
977
-
978
- risk_score = calculate_risk_score(
979
- pet_age_group,
980
- main_symptom,
981
- appetite_status,
982
- water_intake,
983
- energy_level,
984
- pain_signs,
985
- emergency_signs,
986
- symptom_duration
987
- )
988
-
989
- urgency, risk_score = urgency_from_risk_score(
990
- risk_score,
991
- main_symptom,
992
- emergency_signs,
993
- energy_level,
994
- pain_signs
995
- )
996
-
997
- situation_summary = build_situation_summary(
998
- pet_type,
999
- pet_age_group,
1000
- main_symptom,
1001
- secondary_symptoms,
1002
- symptom_duration,
1003
- appetite_status,
1004
- water_intake,
1005
- energy_level,
1006
- emergency_signs
1007
- )
1008
-
1009
- do_now = build_do_now_steps(
1010
- urgency,
1011
- pet_type,
1012
- main_symptom,
1013
- appetite_status
1014
- )
1015
-
1016
- avoid = build_avoid_steps(urgency)
1017
- vet_contact = build_vet_contact_message(urgency)
1018
- why = build_why_message(category, risk_score, similar_cases)
1019
- ai_note = generate_short_ai_note(query_text, urgency, category)
1020
-
1021
- action_plan = {
1022
- "situation_summary": situation_summary,
1023
- "category": category,
1024
- "urgency": urgency,
1025
- "risk_score": risk_score,
1026
- "do_now": do_now,
1027
- "avoid": avoid,
1028
- "vet_contact": vet_contact,
1029
- "why": why,
1030
- "ai_note": ai_note
1031
- }
1032
-
1033
- action_plan_html = create_action_plan_html(action_plan)
1034
- similar_cases_html = create_similar_cases_html(similar_cases)
1035
- pdf_path = create_pdf_report(user_inputs, action_plan, similar_cases)
1036
- whatsapp_html = create_whatsapp_html(user_inputs, action_plan)
1037
-
1038
- return action_plan_html, similar_cases_html, pdf_path, whatsapp_html, query_text
1039
-
1040
-
1041
- # ============================================================
1042
- # APP CHOICES
1043
- # ============================================================
1044
-
1045
- pet_type_choices = ["Dog", "Cat"]
1046
- age_group_choices = ["Puppy", "Kitten", "Adult", "Senior"]
1047
- sex_choices = ["Female", "Male", "Unknown"]
1048
-
1049
- breed_choices = get_choices("breed_size", ["Small", "Medium", "Large", "Unknown"])
1050
- vaccination_choices = get_choices("vaccination_status", ["Up to date", "Not up to date", "Unknown"])
1051
- environment_choices = get_choices("environment", ["Indoor only", "Indoor mostly", "Outdoor access", "Mostly outdoor", "Unknown"])
1052
-
1053
- medical_choices = get_choices(
1054
- "medical_background",
1055
- [
1056
- "No known medical condition",
1057
- "Kidney disease",
1058
- "Diabetes",
1059
- "Heart disease",
1060
- "Arthritis",
1061
- "Allergies",
1062
- "Recent surgery",
1063
- "Unknown"
1064
- ]
1065
- )
1066
-
1067
- recent_change_choices = get_choices(
1068
- "recent_change",
1069
- [
1070
- "No recent change",
1071
- "New food",
1072
- "New home",
1073
- "New pet",
1074
- "Travel",
1075
- "Fireworks or loud noise",
1076
- "Weather change",
1077
- "Unknown"
1078
- ]
1079
- )
1080
-
1081
- main_symptom_choices = get_choices(
1082
- "main_symptom",
1083
- [
1084
- "Vomiting",
1085
- "Diarrhea",
1086
- "Not eating",
1087
- "Limping",
1088
- "Shaking",
1089
- "Hiding",
1090
- "Coughing",
1091
- "Scratching",
1092
- "Biting",
1093
- "Anxiety",
1094
- "Low energy",
1095
- "Bleeding",
1096
- "Difficulty breathing"
1097
- ]
1098
- )
1099
-
1100
- secondary_symptom_choices = [
1101
- "None",
1102
- "Vomiting",
1103
- "Diarrhea",
1104
- "Hiding",
1105
- "Shaking",
1106
- "Coughing",
1107
- "Scratching",
1108
- "Restlessness",
1109
- "Whining",
1110
- "Tiredness",
1111
- "Low energy",
1112
- "Refusing water",
1113
- "Jumping",
1114
- "Excitement"
1115
- ]
1116
-
1117
- duration_choices = get_choices(
1118
- "symptom_duration",
1119
- ["A few hours", "1 day", "2-3 days", "More than 3 days", "More than a week", "Unknown"]
1120
- )
1121
-
1122
- appetite_choices = get_choices("appetite_status", ["Normal", "Reduced", "Not eating", "Increased", "Unknown"])
1123
- water_choices = get_choices("water_intake", ["Normal", "Drinking less", "Drinking more", "Not drinking", "Unknown"])
1124
- energy_choices = get_choices("energy_level", ["Normal", "Slightly tired", "Very tired", "Restless", "Cannot stand", "Unknown"])
1125
-
1126
- pain_choices = [
1127
- "None",
1128
- "Limping",
1129
- "Whining",
1130
- "Hiding",
1131
- "Yelping",
1132
- "Sensitivity to touch",
1133
- "Cannot stand",
1134
- "Restlessness"
1135
- ]
1136
-
1137
- emergency_choices = [
1138
- "None",
1139
- "Difficulty breathing",
1140
- "Seizure",
1141
- "Uncontrolled bleeding",
1142
- "Collapse",
1143
- "Cannot stand",
1144
- "Severe weakness",
1145
- "Blue or pale gums",
1146
- "Repeated vomiting"
1147
- ]
1148
-
1149
- previous_choices = ["No", "Yes", "Unknown"]
1150
-
1151
- goal_choices = [
1152
- "Understand urgency",
1153
- "Know safe first steps",
1154
- "Decide whether to call a vet",
1155
- "Calm my pet safely",
1156
- "Training advice",
1157
- "Nutrition guidance",
1158
- "General guidance"
1159
- ]
1160
-
1161
-
1162
- # ============================================================
1163
- # QUICK STARTERS
1164
- # ============================================================
1165
-
1166
- def quick_senior_cat():
1167
- return (
1168
- "Cat",
1169
- "Senior",
1170
- "Unknown",
1171
- "Unknown",
1172
- "Unknown",
1173
- "Indoor only",
1174
- "Kidney disease",
1175
- "No recent change",
1176
- "Not eating",
1177
- ["Vomiting", "Hiding"],
1178
- "1 day",
1179
- "Not eating",
1180
- "Drinking less",
1181
- "Very tired",
1182
- ["Hiding"],
1183
- ["None"],
1184
- "No",
1185
- "Understand urgency",
1186
- "My senior cat stopped eating today, seems very tired, and vomited once."
1187
- )
1188
-
1189
-
1190
- def quick_fireworks_dog():
1191
- return (
1192
- "Dog",
1193
- "Adult",
1194
- "Unknown",
1195
- "Medium",
1196
- "Up to date",
1197
- "Indoor mostly",
1198
- "No known medical condition",
1199
- "Fireworks or loud noise",
1200
- "Shaking",
1201
- ["Hiding", "Restlessness"],
1202
- "A few hours",
1203
- "Normal",
1204
- "Normal",
1205
- "Restless",
1206
- ["None"],
1207
- ["None"],
1208
- "Yes",
1209
- "Calm my pet safely",
1210
- "My dog is shaking and hiding because of fireworks outside."
1211
- )
1212
-
1213
-
1214
- def quick_puppy_biting():
1215
- return (
1216
- "Dog",
1217
- "Puppy",
1218
- "Unknown",
1219
- "Small",
1220
- "Up to date",
1221
- "Indoor mostly",
1222
- "No known medical condition",
1223
- "New home",
1224
- "Biting",
1225
- ["Jumping", "Excitement"],
1226
- "More than 3 days",
1227
- "Normal",
1228
- "Normal",
1229
- "Normal",
1230
- ["None"],
1231
- ["None"],
1232
- "Yes",
1233
- "Training advice",
1234
- "My puppy keeps biting my hands during playtime and gets very excited."
1235
- )
1236
-
1237
-
1238
- # ============================================================
1239
- # DESIGN
1240
- # ============================================================
1241
-
1242
- custom_css = """
1243
- .gradio-container {
1244
- background: radial-gradient(circle at top left, #F7DDE8 0%, transparent 28%),
1245
- radial-gradient(circle at top right, #CFE9FF 0%, transparent 30%),
1246
- linear-gradient(135deg, #FFF9FB 0%, #F3FBFF 48%, #FFF2F8 100%) !important;
1247
- color: #1F2940 !important;
1248
- }
1249
-
1250
- .hero-wrap {
1251
- display: flex;
1252
- justify-content: center;
1253
- margin-bottom: 24px;
1254
- }
1255
-
1256
- .hero-card {
1257
- width: 100%;
1258
- max-width: 1380px;
1259
- background: linear-gradient(135deg, #BDE0FE 0%, #E1D7F5 48%, #F7A8B8 100%);
1260
- border-radius: 36px;
1261
- padding: 42px 32px;
1262
- text-align: center;
1263
- color: #1F2940;
1264
- box-shadow: 0 20px 50px rgba(110, 110, 160, 0.22);
1265
- border: 1px solid rgba(255,255,255,0.9);
1266
- position: relative;
1267
- overflow: hidden;
1268
- }
1269
-
1270
- .hero-card:before {
1271
- content: "";
1272
- position: absolute;
1273
- top: -90px;
1274
- left: -70px;
1275
- width: 250px;
1276
- height: 250px;
1277
- background: rgba(255,255,255,0.20);
1278
- border-radius: 50%;
1279
- }
1280
-
1281
- .hero-card:after {
1282
- content: "";
1283
- position: absolute;
1284
- bottom: -100px;
1285
- right: -70px;
1286
- width: 300px;
1287
- height: 300px;
1288
- background: rgba(255,255,255,0.18);
1289
- border-radius: 50%;
1290
- }
1291
-
1292
- .hero-badge {
1293
- display: inline-block;
1294
- background: rgba(255,255,255,0.78);
1295
- padding: 9px 18px;
1296
- border-radius: 999px;
1297
- font-size: 13px;
1298
- font-weight: 800;
1299
- letter-spacing: 0.4px;
1300
- color: #59446B;
1301
- box-shadow: 0 8px 18px rgba(100,100,140,0.10);
1302
- margin-bottom: 14px;
1303
- }
1304
-
1305
- .hero-logo {
1306
- font-size: 46px;
1307
- margin-bottom: 8px;
1308
- }
1309
-
1310
- .hero-title {
1311
- font-size: 56px;
1312
- font-weight: 950;
1313
- margin: 0;
1314
- letter-spacing: -1px;
1315
- color: #1D2742;
1316
- }
1317
-
1318
- .hero-title span {
1319
- color: #754E7D;
1320
- }
1321
-
1322
- .hero-subtitle {
1323
- font-size: 19px;
1324
- font-weight: 700;
1325
- margin-top: 12px;
1326
- color: #27314A;
1327
- }
1328
-
1329
- .hero-description {
1330
- max-width: 900px;
1331
- margin: 12px auto 0 auto;
1332
- font-size: 15px;
1333
- line-height: 1.8;
1334
- color: #34415C;
1335
- }
1336
-
1337
- .hero-pills {
1338
- display: flex;
1339
- justify-content: center;
1340
- flex-wrap: wrap;
1341
- gap: 10px;
1342
- margin-top: 24px;
1343
- }
1344
-
1345
- .hero-pill {
1346
- background: rgba(255,255,255,0.75);
1347
- border: 1px solid rgba(255,255,255,0.95);
1348
- border-radius: 999px;
1349
- padding: 10px 16px;
1350
- font-size: 13px;
1351
- font-weight: 700;
1352
- color: #39445D;
1353
- box-shadow: 0 8px 18px rgba(100, 100, 140, 0.09);
1354
- }
1355
-
1356
- .info-strip {
1357
- background: rgba(255,255,255,0.88);
1358
- border-radius: 22px;
1359
- padding: 16px 22px;
1360
- border: 1px solid rgba(255,255,255,0.95);
1361
- box-shadow: 0 10px 26px rgba(140,140,180,0.12);
1362
- margin-bottom: 20px;
1363
- color: #354056;
1364
- }
1365
-
1366
- .page-card {
1367
- background: rgba(255,255,255,0.92);
1368
- border-radius: 28px;
1369
- padding: 28px;
1370
- box-shadow: 0 14px 38px rgba(120,120,160,0.16);
1371
- border: 1px solid rgba(255,255,255,0.95);
1372
- }
1373
-
1374
- button {
1375
- border-radius: 14px !important;
1376
- font-weight: 800 !important;
1377
- }
1378
-
1379
- .action-plan-card {
1380
- background: rgba(255,255,255,0.96);
1381
- border-radius: 30px;
1382
- padding: 30px;
1383
- box-shadow: 0 16px 40px rgba(120,120,160,0.18);
1384
- border: 1px solid white;
1385
- }
1386
-
1387
- .plan-header {
1388
- text-align: center;
1389
- margin-bottom: 20px;
1390
- }
1391
-
1392
- .plan-header h2 {
1393
- font-size: 34px;
1394
- margin: 6px 0;
1395
- color: #1F2940;
1396
- }
1397
-
1398
- .plan-kicker {
1399
- display: inline-block;
1400
- background: #FDEAF1;
1401
- border-radius: 999px;
1402
- padding: 8px 14px;
1403
- font-size: 13px;
1404
- font-weight: 800;
1405
- color: #754E7D;
1406
- }
1407
-
1408
- .urgency-panel {
1409
- border-radius: 24px;
1410
- padding: 22px;
1411
- text-align: center;
1412
- margin: 20px 0;
1413
- color: #1F2940;
1414
- box-shadow: 0 12px 26px rgba(120,120,160,0.14);
1415
- }
1416
-
1417
- .urgency-main {
1418
- font-size: 28px;
1419
- font-weight: 950;
1420
- }
1421
-
1422
- .urgency-sub {
1423
- font-size: 16px;
1424
- font-weight: 800;
1425
- margin-top: 6px;
1426
- }
1427
-
1428
- .plan-grid {
1429
- display: grid;
1430
- grid-template-columns: 1fr 1fr;
1431
- gap: 14px;
1432
- }
1433
-
1434
- .plan-box {
1435
- background: #FFFFFF;
1436
- border: 1px solid #EEF0F6;
1437
- border-radius: 22px;
1438
- padding: 18px;
1439
- margin-top: 14px;
1440
- box-shadow: 0 8px 18px rgba(140,140,180,0.08);
1441
- }
1442
-
1443
- .plan-label {
1444
- font-size: 14px;
1445
- font-weight: 950;
1446
- color: #6C4A78;
1447
- margin-bottom: 8px;
1448
- }
1449
-
1450
- .soft-blue {
1451
- background: #F3FAFF;
1452
- border-color: #DCEEFF;
1453
- }
1454
-
1455
- .soft-pink {
1456
- background: #FFF5F8;
1457
- border-color: #FFDCE8;
1458
- }
1459
-
1460
- .soft-purple {
1461
- background: #F8F3FF;
1462
- border-color: #E7D8FF;
1463
- }
1464
-
1465
- .warning-plan {
1466
- background: #FFF1F5;
1467
- border-color: #FFC8D8;
1468
- }
1469
-
1470
- .disclaimer-card {
1471
- margin-top: 18px;
1472
- border-radius: 20px;
1473
- padding: 16px;
1474
- background: #FFF8E8;
1475
- border: 1px solid #FFE5A8;
1476
- font-weight: 800;
1477
- color: #53442A;
1478
- }
1479
-
1480
- .similar-wrapper {
1481
- background: rgba(255,255,255,0.94);
1482
- border-radius: 30px;
1483
- padding: 30px;
1484
- box-shadow: 0 16px 40px rgba(120,120,160,0.16);
1485
- }
1486
-
1487
- .muted {
1488
- color: #5B6478;
1489
- }
1490
-
1491
- .similar-case-card {
1492
- background: #FFFFFF;
1493
- border: 1px solid #EEF0F6;
1494
- border-radius: 24px;
1495
- padding: 22px;
1496
- margin-top: 18px;
1497
- box-shadow: 0 10px 24px rgba(140,140,180,0.10);
1498
- }
1499
-
1500
- .similar-top {
1501
- display: flex;
1502
- justify-content: space-between;
1503
- gap: 14px;
1504
- align-items: center;
1505
- }
1506
-
1507
- .case-number {
1508
- display: inline-block;
1509
- background: #EAF6FF;
1510
- color: #35516D;
1511
- padding: 7px 13px;
1512
- border-radius: 999px;
1513
- font-weight: 900;
1514
- font-size: 13px;
1515
- }
1516
-
1517
- .similar-score {
1518
- background: #FDEAF1;
1519
- color: #754E7D;
1520
- padding: 10px 14px;
1521
- border-radius: 999px;
1522
- font-weight: 950;
1523
- }
1524
-
1525
- .case-grid {
1526
- display: grid;
1527
- grid-template-columns: 1fr 1fr;
1528
- gap: 12px;
1529
- margin-top: 14px;
1530
- }
1531
-
1532
- .case-grid div {
1533
- background: #FAFBFF;
1534
- border: 1px solid #EEF0F6;
1535
- border-radius: 16px;
1536
- padding: 12px;
1537
- }
1538
-
1539
- .case-section {
1540
- background: #FFF9FB;
1541
- border: 1px solid #FFE0EC;
1542
- border-radius: 16px;
1543
- padding: 14px;
1544
- margin-top: 12px;
1545
- }
1546
-
1547
- .share-card {
1548
- background: rgba(255,255,255,0.96);
1549
- border-radius: 28px;
1550
- padding: 28px;
1551
- box-shadow: 0 14px 34px rgba(120,120,160,0.16);
1552
- border: 1px solid white;
1553
- text-align: center;
1554
- }
1555
-
1556
- .whatsapp-button {
1557
- display: inline-block;
1558
- background: linear-gradient(135deg, #B8E0D2, #93D7B8);
1559
- color: #19372B !important;
1560
- text-decoration: none !important;
1561
- padding: 15px 22px;
1562
- border-radius: 999px;
1563
- font-weight: 950;
1564
- box-shadow: 0 10px 22px rgba(100,160,130,0.18);
1565
- margin-top: 12px;
1566
- }
1567
-
1568
- .placeholder-box {
1569
- background: #F8F3FF;
1570
- border: 1px solid #E7D8FF;
1571
- border-radius: 22px;
1572
- padding: 20px;
1573
- color: #514261;
1574
- font-weight: 700;
1575
- }
1576
-
1577
- @media (max-width: 800px) {
1578
- .hero-title {
1579
- font-size: 42px;
1580
- }
1581
-
1582
- .plan-grid,
1583
- .case-grid {
1584
- grid-template-columns: 1fr;
1585
- }
1586
-
1587
- .similar-top {
1588
- flex-direction: column;
1589
- align-items: flex-start;
1590
- }
1591
- }
1592
- """
1593
-
1594
-
1595
- # ============================================================
1596
- # BUILD GRADIO APP - STABLE TABS VERSION
1597
- # ============================================================
1598
-
1599
- with gr.Blocks(
1600
- title="HopePet AI",
1601
- theme=gr.themes.Soft(primary_hue="pink", secondary_hue="blue"),
1602
- css=custom_css
1603
- ) as demo:
1604
-
1605
- gr.HTML(
1606
- """
1607
- <div class="hero-wrap">
1608
- <div class="hero-card">
1609
- <div class="hero-badge">Smart Pet-Care Assistant • Final Project</div>
1610
- <div class="hero-logo">🐾✨</div>
1611
- <h1 class="hero-title">Hope<span>Pet</span> AI</h1>
1612
- <div class="hero-subtitle">
1613
- Gentle AI guidance for dog and cat owners
1614
- </div>
1615
- <div class="hero-description">
1616
- HopePet AI helps pet owners understand symptoms through structured input,
1617
- similar case retrieval, safe risk logic, AI-generated guidance, PDF reports,
1618
- and WhatsApp sharing.
1619
- </div>
1620
- <div class="hero-pills">
1621
- <div class="hero-pill">🐶 Dogs & Cats</div>
1622
- <div class="hero-pill">🧠 Embeddings</div>
1623
- <div class="hero-pill">🔍 Similar Cases</div>
1624
- <div class="hero-pill">✨ Action Plan</div>
1625
- <div class="hero-pill">📄 PDF Report</div>
1626
- <div class="hero-pill">📲 WhatsApp</div>
1627
- </div>
1628
- </div>
1629
- </div>
1630
- """
1631
- )
1632
-
1633
- gr.HTML(
1634
- """
1635
- <div class="info-strip">
1636
- <b>Safety note:</b> HopePet AI does not replace a veterinarian.
1637
- It provides responsible first-step guidance based on synthetic pet-care cases and safety logic.
1638
- </div>
1639
- """
1640
- )
1641
-
1642
- with gr.Tabs():
1643
-
1644
- with gr.Tab("🐾 Pet Profile"):
1645
- with gr.Group(elem_classes=["page-card"]):
1646
- gr.Markdown("## 🐾 Tell us about your pet")
1647
- gr.Markdown(
1648
- """
1649
- Fill in the basic pet profile.
1650
- Most fields use fixed choices to make the analysis more structured and accurate.
1651
- """
1652
- )
1653
-
1654
- with gr.Row():
1655
- pet_type = gr.Radio(
1656
- pet_type_choices,
1657
- label="Pet type",
1658
- value="Dog"
1659
- )
1660
-
1661
- pet_age_group = gr.Radio(
1662
- age_group_choices,
1663
- label="Age group",
1664
- value="Adult"
1665
- )
1666
-
1667
- with gr.Row():
1668
- pet_sex = gr.Radio(
1669
- sex_choices,
1670
- label="Pet sex",
1671
- value="Unknown"
1672
- )
1673
-
1674
- breed_size = gr.Dropdown(
1675
- breed_choices,
1676
- label="Breed size",
1677
- value="Unknown",
1678
- allow_custom_value=True
1679
- )
1680
-
1681
- with gr.Row():
1682
- vaccination_status = gr.Dropdown(
1683
- vaccination_choices,
1684
- label="Vaccination status",
1685
- value="Unknown",
1686
- allow_custom_value=True
1687
- )
1688
-
1689
- environment = gr.Dropdown(
1690
- environment_choices,
1691
- label="Environment",
1692
- value="Indoor mostly",
1693
- allow_custom_value=True
1694
- )
1695
-
1696
- with gr.Row():
1697
- medical_background = gr.Dropdown(
1698
- medical_choices,
1699
- label="Medical background",
1700
- value="No known medical condition",
1701
- allow_custom_value=True
1702
- )
1703
-
1704
- recent_change = gr.Dropdown(
1705
- recent_change_choices,
1706
- label="Recent change",
1707
- value="No recent change",
1708
- allow_custom_value=True
1709
- )
1710
-
1711
- gr.Markdown("## ⚡ Quick Starters")
1712
- gr.Markdown(
1713
- """
1714
- Use one of these examples to instantly fill the app and test how HopePet AI works.
1715
- """
1716
- )
1717
-
1718
- with gr.Row():
1719
- senior_cat_btn = gr.Button("🐱 Senior cat not eating")
1720
- fireworks_dog_btn = gr.Button("🎆 Dog afraid of fireworks")
1721
- puppy_biting_btn = gr.Button("🐕 Puppy biting hands")
1722
-
1723
- with gr.Tab("🩺 Symptoms & Context"):
1724
- with gr.Group(elem_classes=["page-card"]):
1725
- gr.Markdown("## 🩺 Describe the current problem")
1726
- gr.Markdown(
1727
- """
1728
- Add the symptoms and current situation.
1729
- These details are used to calculate the risk score and retrieve similar cases.
1730
- """
1731
- )
1732
-
1733
- with gr.Row():
1734
- main_symptom = gr.Dropdown(
1735
- main_symptom_choices,
1736
- label="Main symptom",
1737
- value="Vomiting",
1738
- allow_custom_value=True
1739
- )
1740
-
1741
- symptom_duration = gr.Dropdown(
1742
- duration_choices,
1743
- label="Symptom duration",
1744
- value="A few hours",
1745
- allow_custom_value=True
1746
- )
1747
-
1748
- secondary_symptoms = gr.CheckboxGroup(
1749
- secondary_symptom_choices,
1750
- label="Secondary symptoms",
1751
- value=["None"]
1752
- )
1753
-
1754
- with gr.Row():
1755
- appetite_status = gr.Radio(
1756
- appetite_choices,
1757
- label="Appetite status",
1758
- value="Normal"
1759
- )
1760
-
1761
- water_intake = gr.Radio(
1762
- water_choices,
1763
- label="Water intake",
1764
- value="Normal"
1765
- )
1766
-
1767
- energy_level = gr.Radio(
1768
- energy_choices,
1769
- label="Energy level",
1770
- value="Normal"
1771
- )
1772
-
1773
- with gr.Row():
1774
- pain_signs = gr.CheckboxGroup(
1775
- pain_choices,
1776
- label="Pain signs",
1777
- value=["None"]
1778
- )
1779
-
1780
- emergency_signs = gr.CheckboxGroup(
1781
- emergency_choices,
1782
- label="Emergency signs",
1783
- value=["None"]
1784
- )
1785
-
1786
- with gr.Row():
1787
- previous_occurrence = gr.Radio(
1788
- previous_choices,
1789
- label="Did this happen before?",
1790
- value="Unknown"
1791
- )
1792
-
1793
- user_goal = gr.Radio(
1794
- goal_choices,
1795
- label="What do you need help with?",
1796
- value="Understand urgency"
1797
- )
1798
-
1799
- free_text = gr.Textbox(
1800
- label="Optional free text description",
1801
- placeholder="Example: My senior cat stopped eating today and seems very tired.",
1802
- lines=4
1803
- )
1804
-
1805
- with gr.Tab("✨ AI Action Plan"):
1806
- with gr.Group(elem_classes=["page-card"]):
1807
- gr.Markdown("## ✨ Generate your HopePet AI Action Plan")
1808
- gr.Markdown(
1809
- """
1810
- After filling the pet profile and symptoms, click the button below.
1811
- The app will calculate risk, retrieve similar cases, generate an action plan, create a PDF report, and prepare a WhatsApp sharing link.
1812
- """
1813
- )
1814
-
1815
- analyze_btn = gr.Button("✨ Analyze Pet Case")
1816
-
1817
- action_plan_output = gr.HTML(
1818
- """
1819
- <div class="placeholder-box">
1820
- Your personalized HopePet AI Action Plan will appear here after clicking Analyze.
1821
- </div>
1822
- """
1823
- )
1824
-
1825
- with gr.Tab("🔍 Similar Cases"):
1826
- with gr.Group(elem_classes=["page-card"]):
1827
- similar_cases_output = gr.HTML(
1828
- """
1829
- <div class="placeholder-box">
1830
- The top 3 similar cases will appear here after clicking Analyze Pet Case.
1831
- </div>
1832
- """
1833
- )
1834
-
1835
- with gr.Accordion("Show retrieval text created by the app", open=False):
1836
- query_text_output = gr.Textbox(
1837
- label="Retrieval text",
1838
- lines=6,
1839
- interactive=False
1840
- )
1841
-
1842
- with gr.Tab("📄 PDF & WhatsApp"):
1843
- with gr.Group(elem_classes=["page-card"]):
1844
- gr.Markdown("## 📄 Download your report and share the action plan")
1845
- gr.Markdown(
1846
- """
1847
- After analysis, download a structured PDF report or share a short action plan through WhatsApp.
1848
- """
1849
- )
1850
-
1851
- pdf_output = gr.File(
1852
- label="📄 Download HopePet AI PDF Report",
1853
- interactive=False
1854
- )
1855
-
1856
- whatsapp_output = gr.HTML(
1857
- """
1858
- <div class="placeholder-box">
1859
- The WhatsApp sharing button will appear here after clicking Analyze Pet Case.
1860
- </div>
1861
- """
1862
- )
1863
-
1864
- gr.Markdown(
1865
- """
1866
- ### ℹ️ Safety Reminder
1867
-
1868
- HopePet AI does not provide a medical diagnosis and does not replace a veterinarian.
1869
- If your pet shows emergency signs or becomes worse, contact a veterinarian immediately.
1870
- """
1871
- )
1872
-
1873
- with gr.Tab("ℹ️ About & Safety"):
1874
- with gr.Group(elem_classes=["page-card"]):
1875
- gr.Markdown(
1876
- """
1877
- ## About HopePet AI
1878
-
1879
- HopePet AI is an AI-powered pet-care assistant for dog and cat owners.
1880
-
1881
- The application uses:
1882
-
1883
- - Structured user input
1884
- - A synthetic pet-care dataset
1885
- - The winning embedding model from Part 3
1886
- - Similar case retrieval
1887
- - Rule-based risk and urgency logic
1888
- - AI-generated guidance
1889
- - PDF report generation
1890
- - WhatsApp sharing
1891
-
1892
- ## Important Safety Notice
1893
-
1894
- HopePet AI does **not** provide a medical diagnosis.
1895
- It does **not** replace a veterinarian.
1896
-
1897
- If your pet has difficulty breathing, seizures, collapse, uncontrolled bleeding, severe weakness, cannot stand, or shows any emergency sign, contact a veterinarian or emergency veterinary clinic immediately.
1898
- """
1899
- )
1900
-
1901
- all_inputs = [
1902
- pet_type,
1903
- pet_age_group,
1904
- pet_sex,
1905
- breed_size,
1906
- vaccination_status,
1907
- environment,
1908
- medical_background,
1909
- recent_change,
1910
- main_symptom,
1911
- secondary_symptoms,
1912
- symptom_duration,
1913
- appetite_status,
1914
- water_intake,
1915
- energy_level,
1916
- pain_signs,
1917
- emergency_signs,
1918
- previous_occurrence,
1919
- user_goal,
1920
- free_text
1921
- ]
1922
-
1923
- analyze_btn.click(
1924
- fn=analyze_case,
1925
- inputs=all_inputs,
1926
- outputs=[
1927
- action_plan_output,
1928
- similar_cases_output,
1929
- pdf_output,
1930
- whatsapp_output,
1931
- query_text_output
1932
- ]
1933
- )
1934
-
1935
- senior_cat_btn.click(
1936
- fn=quick_senior_cat,
1937
- inputs=[],
1938
- outputs=all_inputs
1939
- )
1940
-
1941
- fireworks_dog_btn.click(
1942
- fn=quick_fireworks_dog,
1943
- inputs=[],
1944
- outputs=all_inputs
1945
- )
1946
-
1947
- puppy_biting_btn.click(
1948
- fn=quick_puppy_biting,
1949
- inputs=[],
1950
- outputs=all_inputs
1951
- )
1952
-
1953
-
1954
- demo.queue().launch(show_error=True)