Spaces:
Sleeping
Sleeping
| import os | |
| import html | |
| import uuid | |
| import tempfile | |
| from datetime import datetime | |
| from urllib.parse import quote | |
| import numpy as np | |
| import pandas as pd | |
| import gradio as gr | |
| from sentence_transformers import SentenceTransformer | |
| from transformers import pipeline | |
| from reportlab.lib import colors | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.units import cm | |
| from reportlab.platypus import ( | |
| SimpleDocTemplate, | |
| Paragraph, | |
| Spacer, | |
| Table, | |
| TableStyle, | |
| PageBreak | |
| ) | |
| # ============================================================ | |
| # HOPEPET AI - SETTINGS | |
| # ============================================================ | |
| APP_NAME = "HopePet AI" | |
| DATASET_PATH = "HopePet_dataset.csv" | |
| HF_DATASET_REPO = os.getenv("HF_DATASET_REPO", "").strip() | |
| EMBEDDINGS_PATH = "HopePet_embeddings.npy" | |
| WINNING_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2" | |
| GENERATION_MODEL = "google/flan-t5-base" | |
| TOP_K = 3 | |
| # ============================================================ | |
| # LOAD DATA | |
| # ============================================================ | |
| def load_data(): | |
| if HF_DATASET_REPO: | |
| from datasets import load_dataset | |
| dataset = load_dataset(HF_DATASET_REPO) | |
| split_name = "train" if "train" in dataset else list(dataset.keys())[0] | |
| data = dataset[split_name].to_pandas() | |
| else: | |
| if not os.path.exists(DATASET_PATH): | |
| raise FileNotFoundError( | |
| f"Dataset file was not found: {DATASET_PATH}. " | |
| "Please upload HopePet_dataset.csv to the Space." | |
| ) | |
| data = pd.read_csv(DATASET_PATH) | |
| data.columns = data.columns.str.strip() | |
| optional_columns = ["secondary_symptoms", "pain_signs", "emergency_signs"] | |
| for col in optional_columns: | |
| if col in data.columns: | |
| data[col] = data[col].fillna("None") | |
| for col in data.select_dtypes(include="object").columns: | |
| data[col] = data[col].fillna("Unknown").astype(str).str.strip() | |
| if "retrieval_text" not in data.columns: | |
| raise ValueError("The dataset must contain a column named 'retrieval_text'.") | |
| return data | |
| df = load_data() | |
| # ============================================================ | |
| # LOAD MODELS | |
| # ============================================================ | |
| embedder = SentenceTransformer(WINNING_EMBEDDING_MODEL) | |
| try: | |
| generator = pipeline( | |
| "text2text-generation", | |
| model=GENERATION_MODEL | |
| ) | |
| except Exception: | |
| generator = None | |
| # ============================================================ | |
| # LOAD OR CREATE EMBEDDINGS | |
| # ============================================================ | |
| def load_or_create_embeddings(): | |
| if os.path.exists(EMBEDDINGS_PATH): | |
| embeddings = np.load(EMBEDDINGS_PATH) | |
| if len(embeddings) == len(df): | |
| return embeddings | |
| texts = df["retrieval_text"].astype(str).tolist() | |
| embeddings = embedder.encode( | |
| texts, | |
| batch_size=64, | |
| show_progress_bar=True, | |
| normalize_embeddings=True | |
| ) | |
| embeddings = np.array(embeddings) | |
| try: | |
| np.save(EMBEDDINGS_PATH, embeddings) | |
| except Exception: | |
| pass | |
| return embeddings | |
| dataset_embeddings = load_or_create_embeddings() | |
| # ============================================================ | |
| # BASIC HELPERS | |
| # ============================================================ | |
| def h(value): | |
| return html.escape(str(value)) | |
| def clean_value(value): | |
| if value is None: | |
| return "Unknown" | |
| if isinstance(value, list): | |
| cleaned = [ | |
| str(v).strip() | |
| for v in value | |
| if str(v).strip().lower() not in ["", "none", "nan"] | |
| ] | |
| return ", ".join(cleaned) if cleaned else "None" | |
| value = str(value).strip() | |
| return value if value else "Unknown" | |
| def get_choices(column_name, fallback, max_values=40): | |
| if column_name not in df.columns: | |
| return fallback | |
| values = ( | |
| df[column_name] | |
| .dropna() | |
| .astype(str) | |
| .str.strip() | |
| .unique() | |
| .tolist() | |
| ) | |
| values = [ | |
| v for v in values | |
| if v and v.lower() not in ["nan", "none", "unknown"] | |
| ] | |
| values = sorted(values)[:max_values] | |
| final_values = fallback + values | |
| final_values = list(dict.fromkeys(final_values)) | |
| return final_values | |
| def sentence_case_pet(pet_type, pet_age_group): | |
| return f"{str(pet_age_group).lower()} {str(pet_type).lower()}" | |
| # ============================================================ | |
| # RETRIEVAL SYSTEM | |
| # ============================================================ | |
| def build_user_case_text( | |
| pet_type, | |
| pet_age_group, | |
| pet_sex, | |
| breed_size, | |
| vaccination_status, | |
| environment, | |
| medical_background, | |
| recent_change, | |
| main_symptom, | |
| secondary_symptoms, | |
| symptom_duration, | |
| appetite_status, | |
| water_intake, | |
| energy_level, | |
| pain_signs, | |
| emergency_signs, | |
| previous_occurrence, | |
| user_goal, | |
| free_text | |
| ): | |
| text = f""" | |
| Pet type: {pet_type}. | |
| Age group: {pet_age_group}. | |
| Sex: {pet_sex}. | |
| Breed size: {breed_size}. | |
| Vaccination status: {vaccination_status}. | |
| Environment: {environment}. | |
| Medical background: {medical_background}. | |
| Recent change: {recent_change}. | |
| Main symptom: {main_symptom}. | |
| Secondary symptoms: {clean_value(secondary_symptoms)}. | |
| Symptom duration: {symptom_duration}. | |
| Appetite status: {appetite_status}. | |
| Water intake: {water_intake}. | |
| Energy level: {energy_level}. | |
| Pain signs: {clean_value(pain_signs)}. | |
| Emergency signs: {clean_value(emergency_signs)}. | |
| Previous occurrence: {previous_occurrence}. | |
| User goal: {user_goal}. | |
| User description: {free_text}. | |
| """ | |
| return " ".join(text.split()) | |
| def retrieve_similar_cases(query_text, top_k=3): | |
| query_embedding = embedder.encode( | |
| [query_text], | |
| normalize_embeddings=True | |
| )[0] | |
| similarities = np.dot(dataset_embeddings, query_embedding) | |
| top_indices = similarities.argsort()[::-1][:top_k] | |
| similar_cases = df.iloc[top_indices].copy() | |
| similar_cases["similarity_score"] = similarities[top_indices] | |
| return similar_cases | |
| def weighted_vote(similar_cases, column_name): | |
| if column_name not in similar_cases.columns: | |
| return "Unknown" | |
| scores = {} | |
| for _, row in similar_cases.iterrows(): | |
| value = str(row[column_name]) | |
| score = float(row["similarity_score"]) | |
| scores[value] = scores.get(value, 0) + score | |
| return max(scores, key=scores.get) | |
| def get_first_value(similar_cases, column_name, fallback="Not available"): | |
| if column_name in similar_cases.columns: | |
| value = similar_cases.iloc[0][column_name] | |
| if pd.notna(value): | |
| return str(value) | |
| return fallback | |
| # ============================================================ | |
| # RISK AND URGENCY LOGIC | |
| # ============================================================ | |
| def emergency_override(main_symptom, emergency_signs, energy_level, pain_signs): | |
| combined_text = " ".join([ | |
| clean_value(main_symptom).lower(), | |
| clean_value(emergency_signs).lower(), | |
| clean_value(energy_level).lower(), | |
| clean_value(pain_signs).lower() | |
| ]) | |
| emergency_keywords = [ | |
| "difficulty breathing", | |
| "seizure", | |
| "collapse", | |
| "uncontrolled bleeding", | |
| "blue or pale gums", | |
| "cannot stand", | |
| "severe weakness" | |
| ] | |
| return any(keyword in combined_text for keyword in emergency_keywords) | |
| def calculate_risk_score( | |
| pet_age_group, | |
| main_symptom, | |
| appetite_status, | |
| water_intake, | |
| energy_level, | |
| pain_signs, | |
| emergency_signs, | |
| symptom_duration | |
| ): | |
| risk = 0 | |
| main_symptom_text = clean_value(main_symptom).lower() | |
| appetite_text = clean_value(appetite_status).lower() | |
| water_text = clean_value(water_intake).lower() | |
| energy_text = clean_value(energy_level).lower() | |
| pain_text = clean_value(pain_signs).lower() | |
| emergency_text = clean_value(emergency_signs).lower() | |
| duration_text = clean_value(symptom_duration).lower() | |
| if pet_age_group in ["Senior", "Puppy", "Kitten"]: | |
| risk += 1 | |
| if "difficulty breathing" in main_symptom_text: | |
| risk += 4 | |
| elif "bleeding" in main_symptom_text: | |
| risk += 2 | |
| elif "not eating" in main_symptom_text: | |
| risk += 1 | |
| elif "vomiting" in main_symptom_text or "diarrhea" in main_symptom_text: | |
| risk += 1 | |
| elif "limping" in main_symptom_text: | |
| risk += 1 | |
| if "not eating" in appetite_text: | |
| risk += 2 | |
| elif "reduced" in appetite_text: | |
| risk += 1 | |
| if "not drinking" in water_text: | |
| risk += 2 | |
| elif "less" in water_text or "more" in water_text: | |
| risk += 1 | |
| if "cannot stand" in energy_text: | |
| risk += 4 | |
| elif "very tired" in energy_text: | |
| risk += 2 | |
| elif "slightly tired" in energy_text or "restless" in energy_text: | |
| risk += 1 | |
| if pain_text not in ["none", "unknown", "", "nan"]: | |
| risk += 2 | |
| if emergency_text not in ["none", "unknown", "", "nan"]: | |
| risk += 3 | |
| if "more than a week" in duration_text: | |
| risk += 2 | |
| elif "more than 3 days" in duration_text: | |
| risk += 1 | |
| return min(risk, 10) | |
| def urgency_from_risk_score(risk_score, main_symptom, emergency_signs, energy_level, pain_signs): | |
| if emergency_override(main_symptom, emergency_signs, energy_level, pain_signs): | |
| return "Emergency", max(risk_score, 7) | |
| if risk_score <= 2: | |
| return "Low", risk_score | |
| elif risk_score <= 4: | |
| return "Medium", risk_score | |
| elif risk_score <= 6: | |
| return "High", risk_score | |
| else: | |
| return "Emergency", risk_score | |
| def urgency_color(urgency): | |
| colors_map = { | |
| "Low": "#B8E0D2", | |
| "Medium": "#FFD6A5", | |
| "High": "#F7A8B8", | |
| "Emergency": "#FF8FAB" | |
| } | |
| return colors_map.get(urgency, "#BDE0FE") | |
| def urgency_icon(urgency): | |
| icons = { | |
| "Low": "🟢", | |
| "Medium": "🟡", | |
| "High": "🟠", | |
| "Emergency": "🔴" | |
| } | |
| return icons.get(urgency, "🐾") | |
| # ============================================================ | |
| # ACTION PLAN | |
| # ============================================================ | |
| def build_situation_summary( | |
| pet_type, | |
| pet_age_group, | |
| main_symptom, | |
| secondary_symptoms, | |
| symptom_duration, | |
| appetite_status, | |
| water_intake, | |
| energy_level, | |
| emergency_signs | |
| ): | |
| pet_label = sentence_case_pet(pet_type, pet_age_group) | |
| secondary = clean_value(secondary_symptoms) | |
| emergency = clean_value(emergency_signs) | |
| summary = ( | |
| f"Your {pet_label} is showing {str(main_symptom).lower()} for {str(symptom_duration).lower()}. " | |
| f"Appetite is reported as {str(appetite_status).lower()}, water intake as {str(water_intake).lower()}, " | |
| f"and energy level as {str(energy_level).lower()}." | |
| ) | |
| if secondary != "None": | |
| summary += f" Additional signs include: {secondary}." | |
| if emergency != "None": | |
| summary += f" Emergency signs were reported: {emergency}." | |
| else: | |
| summary += " No emergency signs were reported." | |
| return summary | |
| def build_do_now_steps(urgency, pet_type, main_symptom, appetite_status): | |
| pet_word = str(pet_type).lower() | |
| symptom_text = str(main_symptom).lower() | |
| steps = [ | |
| f"Keep your {pet_word} in a calm, comfortable, and safe place.", | |
| "Offer fresh water and monitor whether your pet drinks normally.", | |
| "Watch for changes in breathing, energy, appetite, pain, vomiting, diarrhea, or behavior." | |
| ] | |
| if "not eating" in symptom_text or "not eating" in str(appetite_status).lower(): | |
| steps.append("Do not force food; monitor appetite and contact a veterinarian if not eating continues.") | |
| if "shaking" in symptom_text or "anxiety" in symptom_text or "hiding" in symptom_text: | |
| steps.append("Reduce noise and stress, and allow your pet to rest in a quiet area.") | |
| if "limping" in symptom_text: | |
| steps.append("Limit running, jumping, and stairs until the pet is checked or improves.") | |
| if "scratching" in symptom_text: | |
| steps.append("Prevent excessive scratching and check the skin for redness, wounds, or swelling.") | |
| if urgency == "High": | |
| steps.append("Contact a veterinarian today for professional guidance.") | |
| elif urgency == "Emergency": | |
| steps = [ | |
| "Seek emergency veterinary care immediately.", | |
| "Keep your pet as calm and still as possible while arranging urgent care.", | |
| "Do not wait to see if severe emergency signs improve on their own." | |
| ] | |
| return steps | |
| def build_avoid_steps(urgency): | |
| steps = [ | |
| "Do not give human medication unless a veterinarian specifically tells you to.", | |
| "Do not ignore worsening symptoms or sudden changes in breathing, energy, or behavior.", | |
| "Do not force food or water if your pet is struggling, vomiting repeatedly, or seems very weak." | |
| ] | |
| if urgency in ["High", "Emergency"]: | |
| steps.append("Do not delay contacting a veterinarian if the condition looks serious or is getting worse.") | |
| return steps | |
| def build_vet_contact_message(urgency): | |
| if urgency == "Low": | |
| return "Contact a veterinarian if the symptoms continue, become worse, or if new warning signs appear." | |
| if urgency == "Medium": | |
| return "Contact a veterinarian if there is no improvement soon, if symptoms continue, or if your pet becomes weaker." | |
| if urgency == "High": | |
| return "Contact a veterinarian today. The signs are concerning enough to need professional guidance." | |
| return "Contact an emergency veterinarian or veterinary clinic immediately." | |
| def build_why_message(category, risk_score, similar_cases): | |
| top_symptom = get_first_value(similar_cases, "main_symptom", "similar symptoms") | |
| top_category = get_first_value(similar_cases, "problem_category", category) | |
| top_urgency = get_first_value(similar_cases, "urgency_level", "Unknown") | |
| return ( | |
| f"HopePet AI combined the structured information you entered with a rule-based risk score " | |
| f"and retrieved similar cases from the dataset. The closest cases involved '{top_symptom}', " | |
| f"with dataset category '{top_category}' and dataset urgency '{top_urgency}'. " | |
| f"The final urgency shown here is based mainly on the calculated risk score ({risk_score}/10) " | |
| f"and emergency safety logic." | |
| ) | |
| def generate_short_ai_note(query_text, urgency, category): | |
| if generator is None: | |
| return "" | |
| prompt = f""" | |
| You are HopePet AI, a responsible pet-care assistant. | |
| Do not diagnose. Do not replace a veterinarian. | |
| Write two short, calm sentences for the pet owner. | |
| Mention the urgency level and safe next step. | |
| Case: | |
| {query_text} | |
| Category: {category} | |
| Urgency: {urgency} | |
| """ | |
| try: | |
| generated = generator( | |
| prompt, | |
| max_new_tokens=80, | |
| do_sample=False | |
| )[0]["generated_text"].strip() | |
| if len(generated) < 25: | |
| return "" | |
| return generated | |
| except Exception: | |
| return "" | |
| def create_action_plan_html(action_plan): | |
| urgency = action_plan["urgency"] | |
| color = urgency_color(urgency) | |
| icon = urgency_icon(urgency) | |
| do_now_html = "".join([f"<li>{h(step)}</li>" for step in action_plan["do_now"]]) | |
| avoid_html = "".join([f"<li>{h(step)}</li>" for step in action_plan["avoid"]]) | |
| ai_note_block = "" | |
| if action_plan["ai_note"]: | |
| ai_note_block = f""" | |
| <div class="plan-box soft-purple"> | |
| <div class="plan-label">✨ AI-Written Note</div> | |
| <p>{h(action_plan["ai_note"])}</p> | |
| </div> | |
| """ | |
| return f""" | |
| <div class="action-plan-card"> | |
| <div class="plan-header"> | |
| <div class="plan-kicker">Personalized pet-care guidance</div> | |
| <h2>🐾 HopePet AI Action Plan</h2> | |
| <p>Clear first-step guidance based on your pet's details, similar cases, embeddings, and safety logic.</p> | |
| </div> | |
| <div class="urgency-panel" style="background:{color};"> | |
| <div class="urgency-main">{icon} {h(urgency)}</div> | |
| <div class="urgency-sub">Risk Score: {h(action_plan["risk_score"])}/10</div> | |
| </div> | |
| <div class="plan-box"> | |
| <div class="plan-label">1. Situation Summary</div> | |
| <p>{h(action_plan["situation_summary"])}</p> | |
| </div> | |
| <div class="plan-grid"> | |
| <div class="plan-box"> | |
| <div class="plan-label">2. Estimated Category</div> | |
| <p><b>{h(action_plan["category"])}</b></p> | |
| </div> | |
| <div class="plan-box"> | |
| <div class="plan-label">3. Recommended Next Step</div> | |
| <p>{h(action_plan["vet_contact"])}</p> | |
| </div> | |
| </div> | |
| <div class="plan-box soft-blue"> | |
| <div class="plan-label">4. What You Should Do Now</div> | |
| <ul>{do_now_html}</ul> | |
| </div> | |
| <div class="plan-box soft-pink"> | |
| <div class="plan-label">5. What You Should Avoid</div> | |
| <ul>{avoid_html}</ul> | |
| </div> | |
| <div class="plan-box warning-plan"> | |
| <div class="plan-label">6. When to Contact a Vet</div> | |
| <p>{h(action_plan["vet_contact"])}</p> | |
| </div> | |
| <div class="plan-box"> | |
| <div class="plan-label">7. Why HopePet Suggests This</div> | |
| <p>{h(action_plan["why"])}</p> | |
| </div> | |
| {ai_note_block} | |
| <div class="disclaimer-card"> | |
| HopePet AI does not provide a medical diagnosis and does not replace a veterinarian. | |
| </div> | |
| </div> | |
| """ | |
| # ============================================================ | |
| # SIMILAR CASES DISPLAY | |
| # ============================================================ | |
| def create_similar_cases_html(similar_cases): | |
| cards = "" | |
| for i, (_, row) in enumerate(similar_cases.iterrows(), start=1): | |
| pet = f"{row.get('pet_age_group', 'Unknown')} {row.get('pet_type', 'Unknown')}" | |
| similarity = float(row.get("similarity_score", 0)) * 100 | |
| cards += f""" | |
| <div class="similar-case-card"> | |
| <div class="similar-top"> | |
| <div> | |
| <div class="case-number">Similar Case {i}</div> | |
| <h3>{h(pet)}</h3> | |
| </div> | |
| <div class="similar-score">{similarity:.1f}% match</div> | |
| </div> | |
| <div class="case-grid"> | |
| <div><b>Main symptom:</b><br>{h(row.get("main_symptom", "Unknown"))}</div> | |
| <div><b>Secondary symptoms:</b><br>{h(row.get("secondary_symptoms", "None"))}</div> | |
| <div><b>Duration:</b><br>{h(row.get("symptom_duration", "Unknown"))}</div> | |
| <div><b>Appetite:</b><br>{h(row.get("appetite_status", "Unknown"))}</div> | |
| <div><b>Water intake:</b><br>{h(row.get("water_intake", "Unknown"))}</div> | |
| <div><b>Energy level:</b><br>{h(row.get("energy_level", "Unknown"))}</div> | |
| <div><b>Dataset category:</b><br>{h(row.get("problem_category", "Unknown"))}</div> | |
| <div><b>Dataset urgency:</b><br>{h(row.get("urgency_level", "Unknown"))}</div> | |
| </div> | |
| <div class="case-section"> | |
| <b>Recommended next step:</b> | |
| <p>{h(row.get("recommended_next_step", "Not available"))}</p> | |
| </div> | |
| <div class="case-section"> | |
| <b>Triage reason:</b> | |
| <p>{h(row.get("triage_reason", "Not available"))}</p> | |
| </div> | |
| <div class="case-section"> | |
| <b>Safe first steps:</b> | |
| <p>{h(row.get("safe_first_steps", "Not available"))}</p> | |
| </div> | |
| </div> | |
| """ | |
| return f""" | |
| <div class="similar-wrapper"> | |
| <h2>🔍 Top 3 Similar Cases</h2> | |
| <p class="muted"> | |
| These are the closest synthetic cases found by the recommendation system. | |
| They are shown as case snapshots instead of only showing an ID. | |
| </p> | |
| {cards} | |
| </div> | |
| """ | |
| # ============================================================ | |
| # PDF REPORT | |
| # ============================================================ | |
| def pdf_paragraph(text, style): | |
| safe_text = html.escape(str(text)).replace("\n", "<br/>") | |
| return Paragraph(safe_text, style) | |
| def create_pdf_report(user_inputs, action_plan, similar_cases): | |
| filename = f"HopePet_AI_Report_{uuid.uuid4().hex[:8]}.pdf" | |
| pdf_path = os.path.join(tempfile.gettempdir(), filename) | |
| doc = SimpleDocTemplate( | |
| pdf_path, | |
| pagesize=A4, | |
| rightMargin=1.5 * cm, | |
| leftMargin=1.5 * cm, | |
| topMargin=1.4 * cm, | |
| bottomMargin=1.4 * cm | |
| ) | |
| styles = getSampleStyleSheet() | |
| styles.add(ParagraphStyle( | |
| name="HopeTitle", | |
| parent=styles["Title"], | |
| fontName="Helvetica-Bold", | |
| fontSize=24, | |
| textColor=colors.HexColor("#1F2940"), | |
| spaceAfter=12 | |
| )) | |
| styles.add(ParagraphStyle( | |
| name="HopeHeading", | |
| parent=styles["Heading2"], | |
| fontName="Helvetica-Bold", | |
| fontSize=15, | |
| textColor=colors.HexColor("#6C4A78"), | |
| spaceBefore=14, | |
| spaceAfter=8 | |
| )) | |
| styles.add(ParagraphStyle( | |
| name="HopeBody", | |
| parent=styles["BodyText"], | |
| fontName="Helvetica", | |
| fontSize=10.5, | |
| leading=15, | |
| textColor=colors.HexColor("#333333"), | |
| spaceAfter=6 | |
| )) | |
| styles.add(ParagraphStyle( | |
| name="HopeSmall", | |
| parent=styles["BodyText"], | |
| fontName="Helvetica", | |
| fontSize=9, | |
| leading=13, | |
| textColor=colors.HexColor("#555555") | |
| )) | |
| story = [] | |
| story.append(pdf_paragraph("HopePet AI Pet Care Report", styles["HopeTitle"])) | |
| story.append(pdf_paragraph(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M')}", styles["HopeSmall"])) | |
| story.append(pdf_paragraph( | |
| "This report summarizes the information entered by the user, the HopePet AI Action Plan, and the most similar retrieved cases.", | |
| styles["HopeBody"] | |
| )) | |
| story.append(Spacer(1, 10)) | |
| story.append(pdf_paragraph("1. Pet Profile", styles["HopeHeading"])) | |
| profile_items = [ | |
| ["Pet type", user_inputs["pet_type"]], | |
| ["Age group", user_inputs["pet_age_group"]], | |
| ["Sex", user_inputs["pet_sex"]], | |
| ["Breed size", user_inputs["breed_size"]], | |
| ["Vaccination status", user_inputs["vaccination_status"]], | |
| ["Environment", user_inputs["environment"]], | |
| ["Medical background", user_inputs["medical_background"]], | |
| ["Recent change", user_inputs["recent_change"]], | |
| ] | |
| profile_table = Table(profile_items, colWidths=[5 * cm, 10 * cm]) | |
| profile_table.setStyle(TableStyle([ | |
| ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#EAF6FF")), | |
| ("BACKGROUND", (1, 0), (1, -1), colors.HexColor("#FFF9FB")), | |
| ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#DDDDDD")), | |
| ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"), | |
| ("VALIGN", (0, 0), (-1, -1), "TOP"), | |
| ("LEFTPADDING", (0, 0), (-1, -1), 8), | |
| ("RIGHTPADDING", (0, 0), (-1, -1), 8), | |
| ("TOPPADDING", (0, 0), (-1, -1), 6), | |
| ("BOTTOMPADDING", (0, 0), (-1, -1), 6), | |
| ])) | |
| story.append(profile_table) | |
| story.append(pdf_paragraph("2. Symptoms and Context", styles["HopeHeading"])) | |
| symptom_items = [ | |
| ["Main symptom", user_inputs["main_symptom"]], | |
| ["Secondary symptoms", clean_value(user_inputs["secondary_symptoms"])], | |
| ["Symptom duration", user_inputs["symptom_duration"]], | |
| ["Appetite status", user_inputs["appetite_status"]], | |
| ["Water intake", user_inputs["water_intake"]], | |
| ["Energy level", user_inputs["energy_level"]], | |
| ["Pain signs", clean_value(user_inputs["pain_signs"])], | |
| ["Emergency signs", clean_value(user_inputs["emergency_signs"])], | |
| ["Previous occurrence", user_inputs["previous_occurrence"]], | |
| ["User goal", user_inputs["user_goal"]], | |
| ["Free text description", user_inputs["free_text"] if user_inputs["free_text"] else "None"], | |
| ] | |
| symptom_table = Table(symptom_items, colWidths=[5 * cm, 10 * cm]) | |
| symptom_table.setStyle(TableStyle([ | |
| ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#FDEAF1")), | |
| ("BACKGROUND", (1, 0), (1, -1), colors.HexColor("#FFFFFF")), | |
| ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#DDDDDD")), | |
| ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"), | |
| ("VALIGN", (0, 0), (-1, -1), "TOP"), | |
| ("LEFTPADDING", (0, 0), (-1, -1), 8), | |
| ("RIGHTPADDING", (0, 0), (-1, -1), 8), | |
| ("TOPPADDING", (0, 0), (-1, -1), 6), | |
| ("BOTTOMPADDING", (0, 0), (-1, -1), 6), | |
| ])) | |
| story.append(symptom_table) | |
| story.append(pdf_paragraph("3. HopePet AI Action Plan", styles["HopeHeading"])) | |
| story.append(pdf_paragraph(f"Situation summary: {action_plan['situation_summary']}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Estimated category: {action_plan['category']}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Estimated urgency: {action_plan['urgency']}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Risk score: {action_plan['risk_score']}/10", styles["HopeBody"])) | |
| story.append(pdf_paragraph("What you should do now:", styles["HopeBody"])) | |
| for step in action_plan["do_now"]: | |
| story.append(pdf_paragraph(f"- {step}", styles["HopeBody"])) | |
| story.append(pdf_paragraph("What you should avoid:", styles["HopeBody"])) | |
| for step in action_plan["avoid"]: | |
| story.append(pdf_paragraph(f"- {step}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"When to contact a vet: {action_plan['vet_contact']}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Why HopePet suggests this: {action_plan['why']}", styles["HopeBody"])) | |
| if action_plan["ai_note"]: | |
| story.append(pdf_paragraph(f"AI-written note: {action_plan['ai_note']}", styles["HopeBody"])) | |
| story.append(PageBreak()) | |
| story.append(pdf_paragraph("4. Similar Retrieved Cases", styles["HopeHeading"])) | |
| for i, (_, row) in enumerate(similar_cases.iterrows(), start=1): | |
| similarity = float(row.get("similarity_score", 0)) * 100 | |
| story.append(pdf_paragraph(f"Similar Case {i} - {similarity:.1f}% match", styles["HopeHeading"])) | |
| story.append(pdf_paragraph( | |
| f"Pet: {row.get('pet_age_group', 'Unknown')} {row.get('pet_type', 'Unknown')}", | |
| styles["HopeBody"] | |
| )) | |
| story.append(pdf_paragraph(f"Main symptom: {row.get('main_symptom', 'Unknown')}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Secondary symptoms: {row.get('secondary_symptoms', 'None')}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Duration: {row.get('symptom_duration', 'Unknown')}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Appetite: {row.get('appetite_status', 'Unknown')}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Energy level: {row.get('energy_level', 'Unknown')}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Dataset category: {row.get('problem_category', 'Unknown')}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Dataset urgency: {row.get('urgency_level', 'Unknown')}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Recommended next step: {row.get('recommended_next_step', 'Not available')}", styles["HopeBody"])) | |
| story.append(pdf_paragraph(f"Triage reason: {row.get('triage_reason', 'Not available')}", styles["HopeBody"])) | |
| story.append(Spacer(1, 8)) | |
| story.append(pdf_paragraph("5. Safety Disclaimer", styles["HopeHeading"])) | |
| story.append(pdf_paragraph( | |
| "HopePet AI does not provide a medical diagnosis and does not replace a veterinarian. " | |
| "If your pet has emergency signs, becomes worse, or you are unsure, contact a veterinarian or emergency veterinary clinic.", | |
| styles["HopeBody"] | |
| )) | |
| doc.build(story) | |
| return pdf_path | |
| # ============================================================ | |
| # WHATSAPP SHARE | |
| # ============================================================ | |
| def create_whatsapp_html(user_inputs, action_plan): | |
| step_1 = action_plan["do_now"][0] if len(action_plan["do_now"]) > 0 else "" | |
| step_2 = action_plan["do_now"][1] if len(action_plan["do_now"]) > 1 else "" | |
| step_3 = action_plan["do_now"][2] if len(action_plan["do_now"]) > 2 else "" | |
| message = f"""🐾 HopePet AI Action Plan | |
| Pet: {user_inputs['pet_age_group']} {user_inputs['pet_type']} | |
| Main symptom: {user_inputs['main_symptom']} | |
| Urgency: {action_plan['urgency']} | |
| Risk Score: {action_plan['risk_score']}/10 | |
| Situation: | |
| {action_plan['situation_summary']} | |
| What to do now: | |
| - {step_1} | |
| - {step_2} | |
| - {step_3} | |
| When to contact a vet: | |
| {action_plan['vet_contact']} | |
| Important: | |
| HopePet AI does not replace a veterinarian. | |
| """ | |
| url = "https://wa.me/?text=" + quote(message) | |
| return f""" | |
| <div class="share-card"> | |
| <h2>📲 Share on WhatsApp</h2> | |
| <p>Share a short version of the HopePet AI Action Plan through WhatsApp.</p> | |
| <a class="whatsapp-button" href="{url}" target="_blank"> | |
| 📲 Share Action Plan on WhatsApp | |
| </a> | |
| </div> | |
| """ | |
| # ============================================================ | |
| # MAIN ANALYSIS FUNCTION | |
| # ============================================================ | |
| def analyze_case( | |
| pet_type, | |
| pet_age_group, | |
| pet_sex, | |
| breed_size, | |
| vaccination_status, | |
| environment, | |
| medical_background, | |
| recent_change, | |
| main_symptom, | |
| secondary_symptoms, | |
| symptom_duration, | |
| appetite_status, | |
| water_intake, | |
| energy_level, | |
| pain_signs, | |
| emergency_signs, | |
| previous_occurrence, | |
| user_goal, | |
| free_text | |
| ): | |
| user_inputs = { | |
| "pet_type": pet_type, | |
| "pet_age_group": pet_age_group, | |
| "pet_sex": pet_sex, | |
| "breed_size": breed_size, | |
| "vaccination_status": vaccination_status, | |
| "environment": environment, | |
| "medical_background": medical_background, | |
| "recent_change": recent_change, | |
| "main_symptom": main_symptom, | |
| "secondary_symptoms": secondary_symptoms, | |
| "symptom_duration": symptom_duration, | |
| "appetite_status": appetite_status, | |
| "water_intake": water_intake, | |
| "energy_level": energy_level, | |
| "pain_signs": pain_signs, | |
| "emergency_signs": emergency_signs, | |
| "previous_occurrence": previous_occurrence, | |
| "user_goal": user_goal, | |
| "free_text": free_text | |
| } | |
| query_text = build_user_case_text( | |
| pet_type, | |
| pet_age_group, | |
| pet_sex, | |
| breed_size, | |
| vaccination_status, | |
| environment, | |
| medical_background, | |
| recent_change, | |
| main_symptom, | |
| secondary_symptoms, | |
| symptom_duration, | |
| appetite_status, | |
| water_intake, | |
| energy_level, | |
| pain_signs, | |
| emergency_signs, | |
| previous_occurrence, | |
| user_goal, | |
| free_text | |
| ) | |
| similar_cases = retrieve_similar_cases(query_text, TOP_K) | |
| category = weighted_vote(similar_cases, "problem_category") | |
| risk_score = calculate_risk_score( | |
| pet_age_group, | |
| main_symptom, | |
| appetite_status, | |
| water_intake, | |
| energy_level, | |
| pain_signs, | |
| emergency_signs, | |
| symptom_duration | |
| ) | |
| urgency, risk_score = urgency_from_risk_score( | |
| risk_score, | |
| main_symptom, | |
| emergency_signs, | |
| energy_level, | |
| pain_signs | |
| ) | |
| situation_summary = build_situation_summary( | |
| pet_type, | |
| pet_age_group, | |
| main_symptom, | |
| secondary_symptoms, | |
| symptom_duration, | |
| appetite_status, | |
| water_intake, | |
| energy_level, | |
| emergency_signs | |
| ) | |
| do_now = build_do_now_steps( | |
| urgency, | |
| pet_type, | |
| main_symptom, | |
| appetite_status | |
| ) | |
| avoid = build_avoid_steps(urgency) | |
| vet_contact = build_vet_contact_message(urgency) | |
| why = build_why_message(category, risk_score, similar_cases) | |
| ai_note = generate_short_ai_note(query_text, urgency, category) | |
| action_plan = { | |
| "situation_summary": situation_summary, | |
| "category": category, | |
| "urgency": urgency, | |
| "risk_score": risk_score, | |
| "do_now": do_now, | |
| "avoid": avoid, | |
| "vet_contact": vet_contact, | |
| "why": why, | |
| "ai_note": ai_note | |
| } | |
| action_plan_html = create_action_plan_html(action_plan) | |
| similar_cases_html = create_similar_cases_html(similar_cases) | |
| pdf_path = create_pdf_report(user_inputs, action_plan, similar_cases) | |
| whatsapp_html = create_whatsapp_html(user_inputs, action_plan) | |
| return action_plan_html, similar_cases_html, pdf_path, whatsapp_html, query_text | |
| # ============================================================ | |
| # APP CHOICES | |
| # ============================================================ | |
| pet_type_choices = ["Dog", "Cat"] | |
| age_group_choices = ["Puppy", "Kitten", "Adult", "Senior"] | |
| sex_choices = ["Female", "Male", "Unknown"] | |
| breed_choices = get_choices("breed_size", ["Small", "Medium", "Large", "Unknown"]) | |
| vaccination_choices = get_choices("vaccination_status", ["Up to date", "Not up to date", "Unknown"]) | |
| environment_choices = get_choices("environment", ["Indoor only", "Indoor mostly", "Outdoor access", "Mostly outdoor", "Unknown"]) | |
| medical_choices = get_choices( | |
| "medical_background", | |
| [ | |
| "No known medical condition", | |
| "Kidney disease", | |
| "Diabetes", | |
| "Heart disease", | |
| "Arthritis", | |
| "Allergies", | |
| "Recent surgery", | |
| "Unknown" | |
| ] | |
| ) | |
| recent_change_choices = get_choices( | |
| "recent_change", | |
| [ | |
| "No recent change", | |
| "New food", | |
| "New home", | |
| "New pet", | |
| "Travel", | |
| "Fireworks or loud noise", | |
| "Weather change", | |
| "Unknown" | |
| ] | |
| ) | |
| main_symptom_choices = get_choices( | |
| "main_symptom", | |
| [ | |
| "Vomiting", | |
| "Diarrhea", | |
| "Not eating", | |
| "Limping", | |
| "Shaking", | |
| "Hiding", | |
| "Coughing", | |
| "Scratching", | |
| "Biting", | |
| "Anxiety", | |
| "Low energy", | |
| "Bleeding", | |
| "Difficulty breathing" | |
| ] | |
| ) | |
| secondary_symptom_choices = [ | |
| "None", | |
| "Vomiting", | |
| "Diarrhea", | |
| "Hiding", | |
| "Shaking", | |
| "Coughing", | |
| "Scratching", | |
| "Restlessness", | |
| "Whining", | |
| "Tiredness", | |
| "Low energy", | |
| "Refusing water", | |
| "Jumping", | |
| "Excitement" | |
| ] | |
| duration_choices = get_choices( | |
| "symptom_duration", | |
| ["A few hours", "1 day", "2-3 days", "More than 3 days", "More than a week", "Unknown"] | |
| ) | |
| appetite_choices = get_choices("appetite_status", ["Normal", "Reduced", "Not eating", "Increased", "Unknown"]) | |
| water_choices = get_choices("water_intake", ["Normal", "Drinking less", "Drinking more", "Not drinking", "Unknown"]) | |
| energy_choices = get_choices("energy_level", ["Normal", "Slightly tired", "Very tired", "Restless", "Cannot stand", "Unknown"]) | |
| pain_choices = [ | |
| "None", | |
| "Limping", | |
| "Whining", | |
| "Hiding", | |
| "Yelping", | |
| "Sensitivity to touch", | |
| "Cannot stand", | |
| "Restlessness" | |
| ] | |
| emergency_choices = [ | |
| "None", | |
| "Difficulty breathing", | |
| "Seizure", | |
| "Uncontrolled bleeding", | |
| "Collapse", | |
| "Cannot stand", | |
| "Severe weakness", | |
| "Blue or pale gums", | |
| "Repeated vomiting" | |
| ] | |
| previous_choices = ["No", "Yes", "Unknown"] | |
| goal_choices = [ | |
| "Understand urgency", | |
| "Know safe first steps", | |
| "Decide whether to call a vet", | |
| "Calm my pet safely", | |
| "Training advice", | |
| "Nutrition guidance", | |
| "General guidance" | |
| ] | |
| # ============================================================ | |
| # QUICK STARTERS | |
| # ============================================================ | |
| def quick_senior_cat(): | |
| return ( | |
| "Cat", | |
| "Senior", | |
| "Unknown", | |
| "Unknown", | |
| "Unknown", | |
| "Indoor only", | |
| "Kidney disease", | |
| "No recent change", | |
| "Not eating", | |
| ["Vomiting", "Hiding"], | |
| "1 day", | |
| "Not eating", | |
| "Drinking less", | |
| "Very tired", | |
| ["Hiding"], | |
| ["None"], | |
| "No", | |
| "Understand urgency", | |
| "My senior cat stopped eating today, seems very tired, and vomited once." | |
| ) | |
| def quick_fireworks_dog(): | |
| return ( | |
| "Dog", | |
| "Adult", | |
| "Unknown", | |
| "Medium", | |
| "Up to date", | |
| "Indoor mostly", | |
| "No known medical condition", | |
| "Fireworks or loud noise", | |
| "Shaking", | |
| ["Hiding", "Restlessness"], | |
| "A few hours", | |
| "Normal", | |
| "Normal", | |
| "Restless", | |
| ["None"], | |
| ["None"], | |
| "Yes", | |
| "Calm my pet safely", | |
| "My dog is shaking and hiding because of fireworks outside." | |
| ) | |
| def quick_puppy_biting(): | |
| return ( | |
| "Dog", | |
| "Puppy", | |
| "Unknown", | |
| "Small", | |
| "Up to date", | |
| "Indoor mostly", | |
| "No known medical condition", | |
| "New home", | |
| "Biting", | |
| ["Jumping", "Excitement"], | |
| "More than 3 days", | |
| "Normal", | |
| "Normal", | |
| "Normal", | |
| ["None"], | |
| ["None"], | |
| "Yes", | |
| "Training advice", | |
| "My puppy keeps biting my hands during playtime and gets very excited." | |
| ) | |
| # ============================================================ | |
| # DESIGN | |
| # ============================================================ | |
| custom_css = """ | |
| .gradio-container { | |
| background: radial-gradient(circle at top left, #F7DDE8 0%, transparent 28%), | |
| radial-gradient(circle at top right, #CFE9FF 0%, transparent 30%), | |
| linear-gradient(135deg, #FFF9FB 0%, #F3FBFF 48%, #FFF2F8 100%) !important; | |
| color: #1F2940 !important; | |
| } | |
| .hero-wrap { | |
| display: flex; | |
| justify-content: center; | |
| margin-bottom: 24px; | |
| } | |
| .hero-card { | |
| width: 100%; | |
| max-width: 1380px; | |
| background: linear-gradient(135deg, #BDE0FE 0%, #E1D7F5 48%, #F7A8B8 100%); | |
| border-radius: 36px; | |
| padding: 42px 32px; | |
| text-align: center; | |
| color: #1F2940; | |
| box-shadow: 0 20px 50px rgba(110, 110, 160, 0.22); | |
| border: 1px solid rgba(255,255,255,0.9); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .hero-card:before { | |
| content: ""; | |
| position: absolute; | |
| top: -90px; | |
| left: -70px; | |
| width: 250px; | |
| height: 250px; | |
| background: rgba(255,255,255,0.20); | |
| border-radius: 50%; | |
| } | |
| .hero-card:after { | |
| content: ""; | |
| position: absolute; | |
| bottom: -100px; | |
| right: -70px; | |
| width: 300px; | |
| height: 300px; | |
| background: rgba(255,255,255,0.18); | |
| border-radius: 50%; | |
| } | |
| .hero-badge { | |
| display: inline-block; | |
| background: rgba(255,255,255,0.78); | |
| padding: 9px 18px; | |
| border-radius: 999px; | |
| font-size: 13px; | |
| font-weight: 800; | |
| letter-spacing: 0.4px; | |
| color: #59446B; | |
| box-shadow: 0 8px 18px rgba(100,100,140,0.10); | |
| margin-bottom: 14px; | |
| } | |
| .hero-logo { | |
| font-size: 46px; | |
| margin-bottom: 8px; | |
| } | |
| .hero-title { | |
| font-size: 56px; | |
| font-weight: 950; | |
| margin: 0; | |
| letter-spacing: -1px; | |
| color: #1D2742; | |
| } | |
| .hero-title span { | |
| color: #754E7D; | |
| } | |
| .hero-subtitle { | |
| font-size: 19px; | |
| font-weight: 700; | |
| margin-top: 12px; | |
| color: #27314A; | |
| } | |
| .hero-description { | |
| max-width: 900px; | |
| margin: 12px auto 0 auto; | |
| font-size: 15px; | |
| line-height: 1.8; | |
| color: #34415C; | |
| } | |
| .hero-pills { | |
| display: flex; | |
| justify-content: center; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin-top: 24px; | |
| } | |
| .hero-pill { | |
| background: rgba(255,255,255,0.75); | |
| border: 1px solid rgba(255,255,255,0.95); | |
| border-radius: 999px; | |
| padding: 10px 16px; | |
| font-size: 13px; | |
| font-weight: 700; | |
| color: #39445D; | |
| box-shadow: 0 8px 18px rgba(100, 100, 140, 0.09); | |
| } | |
| .info-strip { | |
| background: rgba(255,255,255,0.88); | |
| border-radius: 22px; | |
| padding: 16px 22px; | |
| border: 1px solid rgba(255,255,255,0.95); | |
| box-shadow: 0 10px 26px rgba(140,140,180,0.12); | |
| margin-bottom: 20px; | |
| color: #354056; | |
| } | |
| .page-card { | |
| background: rgba(255,255,255,0.92); | |
| border-radius: 28px; | |
| padding: 28px; | |
| box-shadow: 0 14px 38px rgba(120,120,160,0.16); | |
| border: 1px solid rgba(255,255,255,0.95); | |
| } | |
| button { | |
| border-radius: 14px !important; | |
| font-weight: 800 !important; | |
| } | |
| .action-plan-card { | |
| background: rgba(255,255,255,0.96); | |
| border-radius: 30px; | |
| padding: 30px; | |
| box-shadow: 0 16px 40px rgba(120,120,160,0.18); | |
| border: 1px solid white; | |
| } | |
| .plan-header { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| } | |
| .plan-header h2 { | |
| font-size: 34px; | |
| margin: 6px 0; | |
| color: #1F2940; | |
| } | |
| .plan-kicker { | |
| display: inline-block; | |
| background: #FDEAF1; | |
| border-radius: 999px; | |
| padding: 8px 14px; | |
| font-size: 13px; | |
| font-weight: 800; | |
| color: #754E7D; | |
| } | |
| .urgency-panel { | |
| border-radius: 24px; | |
| padding: 22px; | |
| text-align: center; | |
| margin: 20px 0; | |
| color: #1F2940; | |
| box-shadow: 0 12px 26px rgba(120,120,160,0.14); | |
| } | |
| .urgency-main { | |
| font-size: 28px; | |
| font-weight: 950; | |
| } | |
| .urgency-sub { | |
| font-size: 16px; | |
| font-weight: 800; | |
| margin-top: 6px; | |
| } | |
| .plan-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 14px; | |
| } | |
| .plan-box { | |
| background: #FFFFFF; | |
| border: 1px solid #EEF0F6; | |
| border-radius: 22px; | |
| padding: 18px; | |
| margin-top: 14px; | |
| box-shadow: 0 8px 18px rgba(140,140,180,0.08); | |
| } | |
| .plan-label { | |
| font-size: 14px; | |
| font-weight: 950; | |
| color: #6C4A78; | |
| margin-bottom: 8px; | |
| } | |
| .soft-blue { | |
| background: #F3FAFF; | |
| border-color: #DCEEFF; | |
| } | |
| .soft-pink { | |
| background: #FFF5F8; | |
| border-color: #FFDCE8; | |
| } | |
| .soft-purple { | |
| background: #F8F3FF; | |
| border-color: #E7D8FF; | |
| } | |
| .warning-plan { | |
| background: #FFF1F5; | |
| border-color: #FFC8D8; | |
| } | |
| .disclaimer-card { | |
| margin-top: 18px; | |
| border-radius: 20px; | |
| padding: 16px; | |
| background: #FFF8E8; | |
| border: 1px solid #FFE5A8; | |
| font-weight: 800; | |
| color: #53442A; | |
| } | |
| .similar-wrapper { | |
| background: rgba(255,255,255,0.94); | |
| border-radius: 30px; | |
| padding: 30px; | |
| box-shadow: 0 16px 40px rgba(120,120,160,0.16); | |
| } | |
| .muted { | |
| color: #5B6478; | |
| } | |
| .similar-case-card { | |
| background: #FFFFFF; | |
| border: 1px solid #EEF0F6; | |
| border-radius: 24px; | |
| padding: 22px; | |
| margin-top: 18px; | |
| box-shadow: 0 10px 24px rgba(140,140,180,0.10); | |
| } | |
| .similar-top { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 14px; | |
| align-items: center; | |
| } | |
| .case-number { | |
| display: inline-block; | |
| background: #EAF6FF; | |
| color: #35516D; | |
| padding: 7px 13px; | |
| border-radius: 999px; | |
| font-weight: 900; | |
| font-size: 13px; | |
| } | |
| .similar-score { | |
| background: #FDEAF1; | |
| color: #754E7D; | |
| padding: 10px 14px; | |
| border-radius: 999px; | |
| font-weight: 950; | |
| } | |
| .case-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| margin-top: 14px; | |
| } | |
| .case-grid div { | |
| background: #FAFBFF; | |
| border: 1px solid #EEF0F6; | |
| border-radius: 16px; | |
| padding: 12px; | |
| } | |
| .case-section { | |
| background: #FFF9FB; | |
| border: 1px solid #FFE0EC; | |
| border-radius: 16px; | |
| padding: 14px; | |
| margin-top: 12px; | |
| } | |
| .share-card { | |
| background: rgba(255,255,255,0.96); | |
| border-radius: 28px; | |
| padding: 28px; | |
| box-shadow: 0 14px 34px rgba(120,120,160,0.16); | |
| border: 1px solid white; | |
| text-align: center; | |
| } | |
| .whatsapp-button { | |
| display: inline-block; | |
| background: linear-gradient(135deg, #B8E0D2, #93D7B8); | |
| color: #19372B !important; | |
| text-decoration: none !important; | |
| padding: 15px 22px; | |
| border-radius: 999px; | |
| font-weight: 950; | |
| box-shadow: 0 10px 22px rgba(100,160,130,0.18); | |
| margin-top: 12px; | |
| } | |
| .placeholder-box { | |
| background: #F8F3FF; | |
| border: 1px solid #E7D8FF; | |
| border-radius: 22px; | |
| padding: 20px; | |
| color: #514261; | |
| font-weight: 700; | |
| } | |
| @media (max-width: 800px) { | |
| .hero-title { | |
| font-size: 42px; | |
| } | |
| .plan-grid, | |
| .case-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .similar-top { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| } | |
| """ | |
| # ============================================================ | |
| # BUILD GRADIO APP - STABLE TABS VERSION | |
| # ============================================================ | |
| with gr.Blocks( | |
| title="HopePet AI", | |
| theme=gr.themes.Soft(primary_hue="pink", secondary_hue="blue"), | |
| css=custom_css | |
| ) as demo: | |
| gr.HTML( | |
| """ | |
| <div class="hero-wrap"> | |
| <div class="hero-card"> | |
| <div class="hero-badge">Smart Pet-Care Assistant • Final Project</div> | |
| <div class="hero-logo">🐾✨</div> | |
| <h1 class="hero-title">Hope<span>Pet</span> AI</h1> | |
| <div class="hero-subtitle"> | |
| Gentle AI guidance for dog and cat owners | |
| </div> | |
| <div class="hero-description"> | |
| HopePet AI helps pet owners understand symptoms through structured input, | |
| similar case retrieval, safe risk logic, AI-generated guidance, PDF reports, | |
| and WhatsApp sharing. | |
| </div> | |
| <div class="hero-pills"> | |
| <div class="hero-pill">🐶 Dogs & Cats</div> | |
| <div class="hero-pill">🧠 Embeddings</div> | |
| <div class="hero-pill">🔍 Similar Cases</div> | |
| <div class="hero-pill">✨ Action Plan</div> | |
| <div class="hero-pill">📄 PDF Report</div> | |
| <div class="hero-pill">📲 WhatsApp</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| gr.HTML( | |
| """ | |
| <div class="info-strip"> | |
| <b>Safety note:</b> HopePet AI does not replace a veterinarian. | |
| It provides responsible first-step guidance based on synthetic pet-care cases and safety logic. | |
| </div> | |
| """ | |
| ) | |
| with gr.Tabs(): | |
| with gr.Tab("🐾 Pet Profile"): | |
| with gr.Group(elem_classes=["page-card"]): | |
| gr.Markdown("## 🐾 Tell us about your pet") | |
| gr.Markdown( | |
| """ | |
| Fill in the basic pet profile. | |
| Most fields use fixed choices to make the analysis more structured and accurate. | |
| """ | |
| ) | |
| with gr.Row(): | |
| pet_type = gr.Radio( | |
| pet_type_choices, | |
| label="Pet type", | |
| value="Dog" | |
| ) | |
| pet_age_group = gr.Radio( | |
| age_group_choices, | |
| label="Age group", | |
| value="Adult" | |
| ) | |
| with gr.Row(): | |
| pet_sex = gr.Radio( | |
| sex_choices, | |
| label="Pet sex", | |
| value="Unknown" | |
| ) | |
| breed_size = gr.Dropdown( | |
| breed_choices, | |
| label="Breed size", | |
| value="Unknown", | |
| allow_custom_value=True | |
| ) | |
| with gr.Row(): | |
| vaccination_status = gr.Dropdown( | |
| vaccination_choices, | |
| label="Vaccination status", | |
| value="Unknown", | |
| allow_custom_value=True | |
| ) | |
| environment = gr.Dropdown( | |
| environment_choices, | |
| label="Environment", | |
| value="Indoor mostly", | |
| allow_custom_value=True | |
| ) | |
| with gr.Row(): | |
| medical_background = gr.Dropdown( | |
| medical_choices, | |
| label="Medical background", | |
| value="No known medical condition", | |
| allow_custom_value=True | |
| ) | |
| recent_change = gr.Dropdown( | |
| recent_change_choices, | |
| label="Recent change", | |
| value="No recent change", | |
| allow_custom_value=True | |
| ) | |
| gr.Markdown("## ⚡ Quick Starters") | |
| gr.Markdown( | |
| """ | |
| Use one of these examples to instantly fill the app and test how HopePet AI works. | |
| """ | |
| ) | |
| with gr.Row(): | |
| senior_cat_btn = gr.Button("🐱 Senior cat not eating") | |
| fireworks_dog_btn = gr.Button("🎆 Dog afraid of fireworks") | |
| puppy_biting_btn = gr.Button("🐕 Puppy biting hands") | |
| with gr.Tab("🩺 Symptoms & Context"): | |
| with gr.Group(elem_classes=["page-card"]): | |
| gr.Markdown("## 🩺 Describe the current problem") | |
| gr.Markdown( | |
| """ | |
| Add the symptoms and current situation. | |
| These details are used to calculate the risk score and retrieve similar cases. | |
| """ | |
| ) | |
| with gr.Row(): | |
| main_symptom = gr.Dropdown( | |
| main_symptom_choices, | |
| label="Main symptom", | |
| value="Vomiting", | |
| allow_custom_value=True | |
| ) | |
| symptom_duration = gr.Dropdown( | |
| duration_choices, | |
| label="Symptom duration", | |
| value="A few hours", | |
| allow_custom_value=True | |
| ) | |
| secondary_symptoms = gr.CheckboxGroup( | |
| secondary_symptom_choices, | |
| label="Secondary symptoms", | |
| value=["None"] | |
| ) | |
| with gr.Row(): | |
| appetite_status = gr.Radio( | |
| appetite_choices, | |
| label="Appetite status", | |
| value="Normal" | |
| ) | |
| water_intake = gr.Radio( | |
| water_choices, | |
| label="Water intake", | |
| value="Normal" | |
| ) | |
| energy_level = gr.Radio( | |
| energy_choices, | |
| label="Energy level", | |
| value="Normal" | |
| ) | |
| with gr.Row(): | |
| pain_signs = gr.CheckboxGroup( | |
| pain_choices, | |
| label="Pain signs", | |
| value=["None"] | |
| ) | |
| emergency_signs = gr.CheckboxGroup( | |
| emergency_choices, | |
| label="Emergency signs", | |
| value=["None"] | |
| ) | |
| with gr.Row(): | |
| previous_occurrence = gr.Radio( | |
| previous_choices, | |
| label="Did this happen before?", | |
| value="Unknown" | |
| ) | |
| user_goal = gr.Radio( | |
| goal_choices, | |
| label="What do you need help with?", | |
| value="Understand urgency" | |
| ) | |
| free_text = gr.Textbox( | |
| label="Optional free text description", | |
| placeholder="Example: My senior cat stopped eating today and seems very tired.", | |
| lines=4 | |
| ) | |
| with gr.Tab("✨ AI Action Plan"): | |
| with gr.Group(elem_classes=["page-card"]): | |
| gr.Markdown("## ✨ Generate your HopePet AI Action Plan") | |
| gr.Markdown( | |
| """ | |
| After filling the pet profile and symptoms, click the button below. | |
| The app will calculate risk, retrieve similar cases, generate an action plan, create a PDF report, and prepare a WhatsApp sharing link. | |
| """ | |
| ) | |
| analyze_btn = gr.Button("✨ Analyze Pet Case") | |
| action_plan_output = gr.HTML( | |
| """ | |
| <div class="placeholder-box"> | |
| Your personalized HopePet AI Action Plan will appear here after clicking Analyze. | |
| </div> | |
| """ | |
| ) | |
| with gr.Tab("🔍 Similar Cases"): | |
| with gr.Group(elem_classes=["page-card"]): | |
| similar_cases_output = gr.HTML( | |
| """ | |
| <div class="placeholder-box"> | |
| The top 3 similar cases will appear here after clicking Analyze Pet Case. | |
| </div> | |
| """ | |
| ) | |
| with gr.Accordion("Show retrieval text created by the app", open=False): | |
| query_text_output = gr.Textbox( | |
| label="Retrieval text", | |
| lines=6, | |
| interactive=False | |
| ) | |
| with gr.Tab("📄 PDF & WhatsApp"): | |
| with gr.Group(elem_classes=["page-card"]): | |
| gr.Markdown("## 📄 Download your report and share the action plan") | |
| gr.Markdown( | |
| """ | |
| After analysis, download a structured PDF report or share a short action plan through WhatsApp. | |
| """ | |
| ) | |
| pdf_output = gr.File( | |
| label="📄 Download HopePet AI PDF Report", | |
| interactive=False | |
| ) | |
| whatsapp_output = gr.HTML( | |
| """ | |
| <div class="placeholder-box"> | |
| The WhatsApp sharing button will appear here after clicking Analyze Pet Case. | |
| </div> | |
| """ | |
| ) | |
| gr.Markdown( | |
| """ | |
| ### ℹ️ Safety Reminder | |
| HopePet AI does not provide a medical diagnosis and does not replace a veterinarian. | |
| If your pet shows emergency signs or becomes worse, contact a veterinarian immediately. | |
| """ | |
| ) | |
| with gr.Tab("ℹ️ About & Safety"): | |
| with gr.Group(elem_classes=["page-card"]): | |
| gr.Markdown( | |
| """ | |
| ## About HopePet AI | |
| HopePet AI is an AI-powered pet-care assistant for dog and cat owners. | |
| The application uses: | |
| - Structured user input | |
| - A synthetic pet-care dataset | |
| - The winning embedding model from Part 3 | |
| - Similar case retrieval | |
| - Rule-based risk and urgency logic | |
| - AI-generated guidance | |
| - PDF report generation | |
| - WhatsApp sharing | |
| ## Important Safety Notice | |
| HopePet AI does **not** provide a medical diagnosis. | |
| It does **not** replace a veterinarian. | |
| 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. | |
| """ | |
| ) | |
| all_inputs = [ | |
| pet_type, | |
| pet_age_group, | |
| pet_sex, | |
| breed_size, | |
| vaccination_status, | |
| environment, | |
| medical_background, | |
| recent_change, | |
| main_symptom, | |
| secondary_symptoms, | |
| symptom_duration, | |
| appetite_status, | |
| water_intake, | |
| energy_level, | |
| pain_signs, | |
| emergency_signs, | |
| previous_occurrence, | |
| user_goal, | |
| free_text | |
| ] | |
| analyze_btn.click( | |
| fn=analyze_case, | |
| inputs=all_inputs, | |
| outputs=[ | |
| action_plan_output, | |
| similar_cases_output, | |
| pdf_output, | |
| whatsapp_output, | |
| query_text_output | |
| ] | |
| ) | |
| senior_cat_btn.click( | |
| fn=quick_senior_cat, | |
| inputs=[], | |
| outputs=all_inputs | |
| ) | |
| fireworks_dog_btn.click( | |
| fn=quick_fireworks_dog, | |
| inputs=[], | |
| outputs=all_inputs | |
| ) | |
| puppy_biting_btn.click( | |
| fn=quick_puppy_biting, | |
| inputs=[], | |
| outputs=all_inputs | |
| ) | |
| demo.queue().launch(show_error=True) |