Download render.py from build-small-hackathon/dental-soap: direct link, hf CLI and curl.
- Browser
- Download file 33.2 kB
-
https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/render.py
- Command line
-
hf download hf://spaces/build-small-hackathon/dental-soap/render.py
-
curl -L -o render.py https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/render.py
33.2 kB
| """HTML renderers for Dental SOAP. Pure functions over validated HandoffOutput.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import re | |
| from datetime import date | |
| from html import escape | |
| from schema import HandoffOutput | |
| DISCLAIMER = ( | |
| "This tool organizes patient-reported history. It does not diagnose, choose dental " | |
| "treatment, interpret imaging, or replace a licensed dentist." | |
| ) | |
| HARD_RULE_IDS = { | |
| "airway_or_deep_space_infection", | |
| "gca_jaw_claudication", | |
| "hypochlorite_accident", | |
| "neurologic_or_cardiac_mimic", | |
| } | |
| TIER_LABELS = { | |
| "emergency_now": "Emergency now", | |
| "urgent_medical": "Urgent medical", | |
| "same_day_urgent": "Same-day urgent", | |
| "emergency_or_same_day": "Emergency or same-day", | |
| "urgent_dental": "Urgent dental", | |
| "dentist_discussion": "Discuss with dentist", | |
| "none": "No urgent flag", | |
| } | |
| _TIER_PILL = { | |
| "emergency_now": "tier-emergency", | |
| "urgent_medical": "tier-emergency", | |
| "emergency_or_same_day": "tier-emergency", | |
| "same_day_urgent": "tier-urgent", | |
| "urgent_dental": "tier-urgent", | |
| "dentist_discussion": "tier-discuss", | |
| "none": "tier-discuss", | |
| } | |
| TOOTH_SVG = ( | |
| '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" ' | |
| 'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' | |
| '<path d="M7 3c-2.4 0-4 1.9-4 4.5 0 4.2 2 7.5 3 11.5.4 1.6 2.4 1.7 2.8 0 ' | |
| '.5-2.2.9-5 3.2-5s2.7 2.8 3.2 5c.4 1.7 2.4 1.6 2.8 0 1-4 3-7.3 3-11.5C21 4.9 19.4 3 17 3 ' | |
| 'c-1.8 0-2.9 1-5 1S8.8 3 7 3Z"/></svg>' | |
| ) | |
| def tier_label(tier: str) -> str: | |
| return TIER_LABELS.get(tier, tier) | |
| def tier_pill(tier: str) -> str: | |
| cls = _TIER_PILL.get(tier, "tier-discuss") | |
| return f'<span class="tier-pill {cls}">{escape(tier_label(tier))}</span>' | |
| def html_list(items: list[str], css_class: str = "") -> str: | |
| attr = f' class="{css_class}"' if css_class else "" | |
| return f"<ul{attr}>" + "".join(f"<li>{escape(item)}</li>" for item in items) + "</ul>" | |
| def numbered_list(items: list[str]) -> str: | |
| return "<ol>" + "".join(f"<li>{escape(item)}</li>" for item in items) + "</ol>" | |
| def step_head(num: int, title: str, sub: str) -> str: | |
| return ( | |
| f'<div class="step-head"><span class="step-num">{num}</span>' | |
| f'<div><div class="step-title">{escape(title)}</div>' | |
| f'<div class="step-sub">{escape(sub)}</div></div></div>' | |
| ) | |
| _PRIVACY_PULSE_STYLE = ( | |
| "<style>@keyframes privacyPulse {" | |
| "0%{box-shadow:0 0 0 0 rgba(13,13,13,0.40);}" | |
| "70%{box-shadow:0 0 0 8px rgba(13,13,13,0);}" | |
| "100%{box-shadow:0 0 0 0 rgba(13,13,13,0);}}" | |
| "</style>" | |
| ) | |
| def header_html() -> str: | |
| pulse_dot = ( | |
| '<span style="width:8px;height:8px;border-radius:50%;background:#0D0D0D;' | |
| 'display:inline-block;animation:privacyPulse 2.5s infinite;"></span>' | |
| ) | |
| return ( | |
| '<div class="app-header">' | |
| '<div class="brand-row">' | |
| f'<div class="brand-mark">{TOOTH_SVG}</div>' | |
| "<div>" | |
| '<div class="brand-name">Dental SOAP</div>' | |
| '<div class="brand-tag">Turn your dental story into a one-page, dentist-ready handoff — emergencies flagged by fixed rules, not the AI.</div>' | |
| "</div>" | |
| "</div>" | |
| '<div class="privacy-pill" style="display:flex;align-items:center;gap:8px;">' | |
| f"{pulse_dot}" | |
| " In-Space processing · no database · no external AI API" | |
| "</div>" | |
| "</div>" | |
| + _PRIVACY_PULSE_STYLE | |
| ) | |
| def rail_html() -> str: | |
| steps = ["Chief complaint", "Dental history", "Jaw, bite & TMJ", "Medical background"] | |
| chips = "".join( | |
| f'<span class="rail-chip"><span class="n">{i}</span>{escape(label)}</span>' | |
| for i, label in enumerate(steps, start=1) | |
| ) | |
| return f'<div class="step-rail">{chips}</div>' | |
| def footer_html() -> str: | |
| return ( | |
| '<div class="disclaimer-bar" role="note">' | |
| '<span class="disclaimer-kicker">Safety boundary</span>' | |
| '<span class="disclaimer-copy">' | |
| f"<strong>{escape(DISCLAIMER)}</strong> " | |
| "Fixed safety rules may advise urgent in-person care and cannot be suppressed by the model." | |
| "</span>" | |
| "</div>" | |
| ) | |
| def is_hard_interrupt(output: HandoffOutput) -> bool: | |
| return any(flag.rule_id in HARD_RULE_IDS for flag in output.red_flags) | |
| def initial_safety_html() -> str: | |
| """Neutral pre-build state: no safety claim is made before any answers exist.""" | |
| return """ | |
| <div class="safety-panel clear"> | |
| <div class="safety-kicker">Safety Sentinel · Rule-Based · Deterministic</div> | |
| <strong>Waiting for your answers.</strong> | |
| <p>Once you build your handoff, every answer is checked against deterministic | |
| clinical red-flag rules — computed from what <em>you</em> report, never | |
| guessed or hallucinated by the AI model.</p> | |
| </div> | |
| """ | |
| def placeholder_handoff_html() -> str: | |
| """Pre-build 'ghost document': a faint, static preview of the real handoff | |
| card. The empty state shows the artifact itself — real section headings over | |
| unfilled lines — instead of a generic dashed box, so the deliverable is | |
| legible before the first interaction. Static (no shimmer): the shimmer | |
| skeleton means 'composing now'; the ghost means 'awaiting your story'.""" | |
| def sec(label: str, widths: tuple[int, ...]) -> str: | |
| lines = "".join(f'<div class="ghost-line" style="width:{w}%"></div>' for w in widths) | |
| return f"<div class='ghost-sec'><h3>{label}</h3>{lines}</div>" | |
| return f""" | |
| <div class="handoff-card ghost" aria-label="Handoff preview — awaiting your story"> | |
| <div class="doc-band"> | |
| <div> | |
| <div class="product">DENTAL SOAP · VISIT PREP</div> | |
| <div class="artifact">Dentist Visit Handoff</div> | |
| </div> | |
| <div class="doc-meta"><div class="doc-badges"> | |
| <span class="doc-badge ghost-badge">Awaiting your story</span> | |
| </div></div> | |
| </div> | |
| <div class="doc-body"> | |
| <div class="ghost-cta">This page writes itself from your story — | |
| load an example case or start the guided interview.</div> | |
| {sec("Chief Concern", (58,))} | |
| {sec("Concise Summary", (92, 71))} | |
| {sec("Timeline / Story Evidence", (84, 76, 64))} | |
| {sec("Current Symptoms", (52, 66))} | |
| {sec("Questions For The Dentist", (88, 74))} | |
| {sec("After-Visit Tracker", (69,))} | |
| </div> | |
| </div> | |
| """ | |
| def skeleton_handoff_html() -> str: | |
| """Branded loading state shown the instant Build is pressed: a handoff-shaped | |
| shimmer skeleton so the ZeroGPU cold start reads as 'composing' rather than a | |
| broken, half-faded page (the single highest-impact first-impression fix).""" | |
| def sec(label: str, lines: int) -> str: | |
| bars = "".join('<div class="skel-line"></div>' for _ in range(lines)) | |
| return f'<div class="skel-sec"><h3>{label}</h3>{bars}</div>' | |
| return f""" | |
| <div class="handoff-card skeleton" aria-busy="true"> | |
| <div class="doc-band"> | |
| <div> | |
| <div class="product">DENTAL SOAP · VISIT PREP</div> | |
| <div class="skel-title"></div> | |
| </div> | |
| <div class="doc-meta"><div class="skel-badges"> | |
| <span class="skel-badge"></span><span class="skel-badge"></span> | |
| </div></div> | |
| </div> | |
| <div class="doc-body"> | |
| <div class="skel-note">Composing your dentist-ready handoff — safety rules already ran</div> | |
| {sec("CHIEF CONCERN", 1)} | |
| {sec("CONCISE SUMMARY", 2)} | |
| {sec("CURRENT SYMPTOMS", 3)} | |
| {sec("QUESTIONS FOR THE DENTIST", 2)} | |
| </div> | |
| </div> | |
| """ | |
| def _bring_section_html(output: HandoffOutput) -> str: | |
| """'Bring To The Visit' section — omitted entirely for legacy caches without the field.""" | |
| if not output.bring_checklist: | |
| return "" | |
| return "<h3>Bring To The Visit</h3>" + html_list(output.bring_checklist, css_class="tracker") | |
| # Deterministic Arabic renderings for the fixed bring-checklist item pool. | |
| # Prefix-matched because two items carry dynamic tails (meds, allergies). | |
| # All Arabic strings in this module were reviewed in a native-quality pass | |
| # (Gemini 2.5 Pro, 2026-06-09) for grammar, register, and clinical accuracy. | |
| _ARABIC_BRING_PREFIXES = [ | |
| ("your medication list", "قائمة الأدوية والجرعات (أو علب الأدوية نفسها)."), | |
| ("a written list of any medications", "قائمة مكتوبة بأي أدوية وجرعاتها، أو علب الأدوية نفسها."), | |
| ("exact allergy names", "أسماء المواد المسببة للحساسية، مع وصف دقيق لنوع التفاعل الذي حدث."), | |
| ("the actual imaging files", "ملفات الأشعة نفسها (أشعة سينية أو CBCT) على فلاشة أو الهاتف — الملفات وليس التقرير فقط."), | |
| ("dates of recent dental procedures", "تواريخ إجراءات الأسنان الأخيرة وبيانات التواصل مع العيادة المعالجة."), | |
| ("your current night guard", "واقي الأسنان الليلي أو الجبيرة أو المثبت — أحضر الجهاز نفسه."), | |
| ("the crown or fragment", "التاج أو الجزء المكسور في علبة نظيفة — لا تلصقه ولا تعده إلى مكانه."), | |
| ("a short pain log", "سجل قصير للألم: متى يحدث، ما الذي يثيره، ما الذي يخففه، ودرجة الألم من 10 يوميا."), | |
| ("this handoff", "هذا الملخص مطبوعا أو على هاتفك."), | |
| ] | |
| def _arabic_bring_phrase(item: str) -> str: | |
| lower = item.lower() | |
| for prefix, arabic in _ARABIC_BRING_PREFIXES: | |
| if lower.startswith(prefix): | |
| return arabic | |
| return item | |
| def _arabic_phrase(item: str) -> str: | |
| lower = item.lower() | |
| if lower.startswith("pain score reported as"): | |
| score = re.search(r"(\d+/10)", item) | |
| return f"درجة الألم المبلغ عنها: {score.group(1) if score else escape(item)}" | |
| if "biting" in lower or "chewing" in lower: | |
| return "ألم أو إحساس بكدمة عند العض أو المضغ." | |
| if "hot/cold" in lower: | |
| return "حساسية للساخن أو البارد." | |
| if "jaw/tmj" in lower: | |
| return "أعراض في الفك أو مفصله يجب توضيحها للطبيب." | |
| if "swelling" in lower: | |
| return "تم الإبلاغ عن تورم." | |
| if "fever" in lower or "unwell" in lower: | |
| return "تم الإبلاغ عن حرارة أو شعور عام بالتعب." | |
| if lower.startswith("area:"): | |
| return "المنطقة: " + item.split(":", 1)[1].strip() | |
| if lower.startswith("recent dental work:"): | |
| value = item.split(":", 1)[1].strip() | |
| # Translate the common deterministic "none" value; any other value is | |
| # patient-entered free text and stays in the patient's own words. | |
| if re.match(r"^(no recent|none\b|no\b)", value, re.IGNORECASE): | |
| return "إجراءات الأسنان الأخيرة: لا يوجد" | |
| return "إجراءات الأسنان الأخيرة: " + value | |
| if "medications" in lower: | |
| return "الأدوية أو المكملات التي يجب مراجعتها: " + item.split(":", 1)[-1].strip() | |
| if "allergies" in lower: | |
| return "الحساسية أو التفاعلات السابقة التي يجب مراجعتها: " + item.split(":", 1)[-1].strip() | |
| return item | |
| # Vetted Arabic twins for the DETERMINISTIC dentist-question bank and tracker | |
| # items (app._base_questions / app._tracker_items). Exact-string keyed: a model- | |
| # appended question has no twin and is never machine-translated — the Arabic | |
| # card notes that extra case-specific questions appear in the English copy. | |
| _ARABIC_QUESTIONS = { | |
| "Which tooth or area should we prioritize examining first based on my history?": | |
| "بناءً على تاريخي المرضي، أي سن أو منطقة لها الأولوية في الفحص؟", | |
| "What findings on exam or imaging would help separate tooth, crown, bite, and jaw-muscle causes?": | |
| "ما نتائج الفحص أو الأشعة التي تساعد في التمييز بين أسباب المشكلة: هل هي السن، التاج، الإطباق، أم عضلات الفك؟", | |
| "What should I track after today's visit so we can tell whether symptoms are improving?": | |
| "ما الذي يجب أن أتابعه بعد زيارة اليوم لنعرف ما إذا كانت الأعراض تتحسن؟", | |
| "Can you check the crown margin, contacts, cement seal, and whether the bite is high?": | |
| "هل يمكنك فحص حافة التاج، ونقاط التماس، وإحكام المادة اللاصقة، وما إذا كان الإطباق مرتفعًا؟", | |
| "Should this tooth have an endodontic reassessment, and what records or X-rays would help?": | |
| "هل هذا السن بحاجة لإعادة تقييم علاج العصب؟ وما هي السجلات أو الأشعة التي قد تساعد في ذلك؟", | |
| "Can you check the bite with articulating paper and compare both sides of contact?": | |
| "هل يمكنك فحص الإطباق بورق العضة الملون ومقارنة التلامس على الجانبين؟", | |
| "Could jaw muscles or the TMJ be contributing, and do I need referral or conservative jaw care?": | |
| "هل يمكن أن تكون عضلات الفك أو مفصل الفك سببًا في الأعراض؟ وهل أحتاج لتحويل طبي أو رعاية تحفظية للفك؟", | |
| "Could my sinus symptoms and tooth symptoms be related, and how would we tell which is driving which?": | |
| "هل يمكن وجود صلة بين أعراض الجيوب الأنفية وأعراض أسناني؟ وكيف نفرق أيهما المسبب للآخر؟", | |
| "Does my temperature-sensitivity pattern help localize which tooth or surface to test first?": | |
| "هل نمط حساسية أسناني للحرارة والبرودة يساعد في تحديد أي سن أو سطح نبدأ باختباره؟", | |
| "What warning signs of spreading infection should send me to urgent care before our next appointment?": | |
| "ما علامات انتشار العدوى التي تستدعي الذهاب إلى الطوارئ قبل موعدنا القادم؟", | |
| "Do any of my current medications change what is safe or recommended at this visit?": | |
| "هل أي من أدويتي الحالية يغير من سلامة أو توصيات العلاج لهذه الزيارة؟", | |
| } | |
| _ARABIC_TRACKER = { | |
| "What was examined: tooth/area, bite, crown margin, gums, TMJ, imaging reviewed.": | |
| "ما تم فحصه: السن/المنطقة، الإطباق، حافة التاج، اللثة، مفصل الفك، الأشعة التي روجعت.", | |
| "What changed today: adjustment, medication advice, referral, imaging request, or watchful waiting.": | |
| "ما الذي تغير اليوم: تعديل، نصيحة دوائية، تحويل، طلب أشعة، أو مراقبة وانتظار.", | |
| "Pain score before/after visit and whether biting, temperature, or jaw symptoms changed.": | |
| "درجة الألم قبل وبعد الزيارة، وهل تغيرت الأعراض عند العض، أو الحساسية للحرارة والبرودة، أو أعراض الفك.", | |
| "Next step, owner, and follow-up date.": | |
| "الخطوة التالية، المسؤول عنها، وتاريخ المتابعة.", | |
| } | |
| _ARABIC_EXTRA_QUESTIONS_NOTE = "توجد أسئلة إضافية خاصة بالحالة باللغة الإنجليزية." | |
| _ARABIC_AS_WRITTEN = "(كما كتبه المريض)" | |
| _ARABIC_CHARS = re.compile(r"[-ۿ]") | |
| def _arabic_narrative(text: str) -> str: | |
| """Patient/model narrative is never machine-translated. When the text has no | |
| Arabic script at all, isolate it as an LTR run and annotate it as the | |
| patient's own wording so English under an Arabic heading reads as a choice, | |
| not a rendering bug.""" | |
| if _ARABIC_CHARS.search(text): | |
| return escape(text) | |
| return f'<bdi>{escape(text)}</bdi> <span class="as-written">{_ARABIC_AS_WRITTEN}</span>' | |
| def _arabic_card(output: HandoffOutput) -> str: | |
| symptoms = [_arabic_phrase(item) for item in output.current_symptoms] | |
| history = [_arabic_phrase(item) for item in output.dental_history] | |
| medical = [_arabic_phrase(item) for item in output.medical_safety_notes] | |
| safety = ( | |
| "توجد علامات سلامة تتطلب مناقشة أو تعاملًا سريعًا حسب درجة الخطورة." | |
| if output.red_flags | |
| else "لم تظهر علامة طوارئ من المعلومات التي أدخلتها، لكن هذا لا ينفي وجود مشكلة تحتاج لفحص طبيب الأسنان." | |
| ) | |
| # Deterministic question bank twins; model-appended questions have no twin | |
| # and are flagged as English-only rather than machine-translated. | |
| questions = [_ARABIC_QUESTIONS[q] for q in output.dentist_questions if q in _ARABIC_QUESTIONS] | |
| untranslated = len(output.dentist_questions) - len(questions) | |
| if not questions: | |
| questions = [ | |
| "ما السن أو المنطقة التي يجب فحصها أولا؟", | |
| "هل المشكلة مرتبطة بالعضة أو التاج أو علاج العصب أو مفصل الفك؟", | |
| "ما العلامات التي تستدعي مراجعة عاجلة؟", | |
| ] | |
| questions_note = ( | |
| f'<p class="as-written">{_ARABIC_EXTRA_QUESTIONS_NOTE}</p>' if untranslated > 0 else "" | |
| ) | |
| tracker = [_ARABIC_TRACKER[t] for t in output.after_visit_tracker if t in _ARABIC_TRACKER] | |
| tracker_html = ( | |
| "<h3>متتبع ما بعد الزيارة</h3>" + html_list(tracker, css_class="tracker") if tracker else "" | |
| ) | |
| # <bdi> isolates the LTR name/digits inside the RTL line — without it the | |
| # surrounding parentheses render scrambled ("(38" with an orphaned paren). | |
| patient_meta = f"المريض: <bdi>{escape(output.patient_name)}</bdi>" if output.patient_name else "" | |
| if output.patient_age is not None: | |
| age_str = f"العمر: <bdi>{output.patient_age}</bdi>" | |
| patient_meta = f"{patient_meta} · {age_str}" if patient_meta else age_str | |
| patient_meta_html = f'<div class="patient-line">{patient_meta}</div>' if patient_meta else "" | |
| arabic_badges = ( | |
| f'<span class="doc-badge"><bdi>{escape(date.today().isoformat())}</bdi></span>' | |
| '<span class="doc-badge">ليست تشخيصا</span>' | |
| '<span class="doc-badge">من إعداد المريض</span>' | |
| ) | |
| return f""" | |
| <div class="handoff-card arabic" dir="rtl" lang="ar"> | |
| <div class="doc-band"> | |
| <div> | |
| <div class="product">DENTAL SOAP · تحضير الزيارة</div> | |
| <div class="artifact">ملخص زيارة طبيب الأسنان</div> | |
| </div> | |
| <div class="doc-meta">{patient_meta_html}<div class="doc-badges">{arabic_badges}</div></div> | |
| </div> | |
| <div class="doc-body"> | |
| <p>هذا ملخص لمساعدتك في التحضير للزيارة. لا يقدم تشخيصًا ولا يفسر الأشعة.</p> | |
| <h3>الشكوى الرئيسية</h3> | |
| <p>{_arabic_narrative(output.chief_concern)}</p> | |
| <h3>ملخص القصة كما وردت</h3> | |
| <p>{_arabic_narrative(output.concise_summary)}</p> | |
| <h3>الأعراض الحالية</h3> | |
| {html_list(symptoms)} | |
| <h3>تاريخ الأسنان المهم</h3> | |
| {html_list(history)} | |
| <h3>ملاحظات السلامة الطبية</h3> | |
| {html_list(medical)} | |
| {"<h3>ما يجب إحضاره معك للزيارة</h3>" + html_list([_arabic_bring_phrase(i) for i in output.bring_checklist]) if output.bring_checklist else ""} | |
| <h3>حالة السلامة</h3> | |
| <p>{safety}</p> | |
| <h3>أسئلة للطبيب</h3> | |
| {html_list(questions)} | |
| {questions_note} | |
| {tracker_html} | |
| </div> | |
| </div> | |
| """ | |
| def _interrupt_card(output: HandoffOutput, language: str = "English") -> str: | |
| first = output.red_flags[0] | |
| evidence = html_list([span.quote for flag in output.red_flags for span in flag.evidence][:4]) | |
| # Arabic urgency line appears only when the patient chose Arabic or Bilingual — | |
| # an unexplained RTL sentence inside an English card reads as a bug. | |
| arabic_line = ( | |
| '<p dir="rtl" lang="ar" style="margin:6px 0 2px;text-align:right;font-size:14px">' | |
| "يرجى طلب الرعاية الطبية فورًا — هذا تنبيه سلامة.</p>" | |
| if language in {"Arabic", "Bilingual"} | |
| else "" | |
| ) | |
| return f""" | |
| <div class="interrupt-card"> | |
| <div class="interrupt-band">Stop and seek urgent care</div> | |
| {arabic_line} | |
| <div class="interrupt-body"> | |
| {tier_pill(first.tier)} | |
| <p><strong>{escape(first.title)}</strong></p> | |
| <p>{escape(first.patient_message)}</p> | |
| <h3>What to tell the clinician</h3> | |
| <p>{escape(first.clinician_question)}</p> | |
| <h3>Evidence from your story</h3> | |
| {evidence} | |
| <h3>Boundary</h3> | |
| <p>Dental SOAP is pausing the normal handoff because a high-risk rule fired. | |
| This is not a diagnosis; it is a safety escalation to in-person care.</p> | |
| </div> | |
| </div> | |
| """ | |
| def render_handoff_html(output: HandoffOutput, language: str, model_authored: bool = False) -> str: | |
| if is_hard_interrupt(output): | |
| return _interrupt_card(output, language) | |
| count = len(output.red_flags) | |
| flag_text = f"{count} safety flag{'s' if count != 1 else ''}" if count else "No urgent safety flags" | |
| patient_meta = f"Patient: {escape(output.patient_name)}" if output.patient_name else "" | |
| if output.patient_age is not None: | |
| age_str = f"Age: {output.patient_age}" | |
| patient_meta = f"{patient_meta} ({age_str})" if patient_meta else age_str | |
| patient_meta_html = f'<div class="patient-line">{patient_meta}</div>' if patient_meta else "" | |
| doc_id = hashlib.sha1( | |
| f"{output.patient_name}|{output.chief_concern}|{date.today().isoformat()}".encode() | |
| ).hexdigest()[:8].upper() | |
| # Provenance labeling: the narrative body may be AI-summarized; safety flags | |
| # and evidence are always rule-computed and never model-authored. | |
| provenance_badges = ( | |
| ("AI-summarized narrative", "Safety flags rule-computed") | |
| if model_authored | |
| else ("Patient-authored", "Not a diagnosis") | |
| ) | |
| badges = "".join( | |
| f'<span class="doc-badge">{escape(text)}</span>' | |
| for text in (date.today().isoformat(), *provenance_badges) | |
| ) | |
| badges += f'<span class="doc-badge {"flag-alert" if count else "flag-clear"}">{escape(flag_text)}</span>' | |
| html = f""" | |
| <div class="handoff-card"> | |
| <div class="doc-band"> | |
| <div> | |
| <div class="product">DENTAL SOAP · VISIT PREP</div> | |
| <div class="artifact">{escape(output.artifact_label)}</div> | |
| </div> | |
| <div class="doc-meta">{patient_meta_html}<div class="doc-badges">{badges}</div></div> | |
| </div> | |
| <div class="doc-body"> | |
| <h3>Chief Concern</h3> | |
| <p>{escape(output.chief_concern)}</p> | |
| <h3>Concise Summary</h3> | |
| <p>{escape(output.concise_summary)}</p> | |
| <h3>Timeline / Story Evidence</h3> | |
| {html_list(output.timeline)} | |
| <h3>Current Symptoms</h3> | |
| {html_list(output.current_symptoms)} | |
| <h3>Relevant Dental History</h3> | |
| {html_list(output.dental_history)} | |
| <h3>Medical Safety Notes</h3> | |
| {html_list(output.medical_safety_notes)} | |
| <h3>Patient Goals</h3> | |
| {html_list(output.patient_goals)} | |
| {_bring_section_html(output)} | |
| <h3>Questions For The Dentist</h3> | |
| {numbered_list(output.dentist_questions)} | |
| <h3>After-Visit Tracker</h3> | |
| {html_list(output.after_visit_tracker, css_class="tracker")} | |
| <div class="doc-foot"> | |
| <div class="doc-foot-meta">DENTAL SOAP · {date.today().isoformat()} · DOC {doc_id} · AI-generated intake summary — for dental professional review only</div> | |
| {escape(" · ".join(output.limitations))}<br>{escape(DISCLAIMER)} | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| if language in {"Arabic", "Bilingual"}: | |
| arabic = _arabic_card(output) | |
| if language == "Arabic": | |
| return arabic | |
| # Side-by-side on wide screens (the bilingual money shot), stacked on | |
| # narrow ones — .bilingual-wrap is a responsive grid in style.css. | |
| return f'<div class="bilingual-wrap">{html}{arabic}</div>' | |
| return html | |
| def render_safety_html(output: HandoffOutput) -> str: | |
| if not output.red_flags: | |
| return """ | |
| <div class="safety-panel clear"> | |
| <div class="safety-kicker">Safety panel · rule-based</div> | |
| <strong>Clear from provided answers.</strong> | |
| <p>No urgent red flag was detected from the current story and structured fields. | |
| This does not rule out a dental problem; bring the handoff to a licensed dentist.</p> | |
| </div> | |
| """ | |
| body = [ | |
| '<div class="safety-panel">', | |
| '<div class="safety-kicker">Safety panel · rule-based</div>', | |
| "<strong>Deterministic rules fired.</strong>", | |
| "<p>These warnings are computed from your answers, not guessed by the model.</p>", | |
| ] | |
| for flag in output.red_flags: | |
| evidence_items = "".join( | |
| f'<div class="ev-span"><span class="ev-src">{escape(span.source)}</span>' | |
| f"“{escape(span.quote)}”</div>" | |
| for span in flag.evidence | |
| ) | |
| # "Show your work": the evidence is tucked behind a disclosure that names | |
| # the exact deterministic rule that fired — clinician-grade provenance. | |
| # A closed <details> never renders its content on paper, so the printed | |
| # card carries a static print-only copy: a printed urgent flag must show | |
| # the patient's triggering words (FIELD_NOTES promise). | |
| body.append( | |
| f""" | |
| <div class="flag"> | |
| {tier_pill(flag.tier)} | |
| <div class="flag-title">{escape(flag.title)}</div> | |
| <div class="flag-body">{escape(flag.patient_message)}</div> | |
| <details class="flag-rule"> | |
| <summary><span class="flag-rule-k">Why this fired</span><span class="flag-rule-id">rule · {escape(flag.rule_id)}</span></summary> | |
| <div class="flag-evidence">{evidence_items}</div> | |
| </details> | |
| <div class="flag-evidence flag-evidence-print"> | |
| <div class="ev-span"><span class="ev-src">rule</span>{escape(flag.rule_id)}</div> | |
| {evidence_items} | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| body.append("</div>") | |
| return "".join(body) | |
| def plain_text_handoff(output: HandoffOutput) -> str: | |
| """Plain-text version of the handoff for email body and PDF source.""" | |
| lines: list[str] = [ | |
| f"{output.product_name} — {output.artifact_label}", | |
| f"Date: {date.today().isoformat()} · Patient-authored · Not a diagnosis", | |
| "", | |
| ] | |
| if output.red_flags: | |
| lines.append("SAFETY FLAGS (rule-based):") | |
| for flag in output.red_flags: | |
| lines.append(f"- [{tier_label(flag.tier)}] {flag.title}: {flag.patient_message}") | |
| lines.append("") | |
| def section(title: str, items: list[str], numbered: bool = False, boxes: bool = False) -> None: | |
| lines.append(title.upper()) | |
| for i, item in enumerate(items, start=1): | |
| prefix = f"{i}. " if numbered else ("[ ] " if boxes else "- ") | |
| lines.append(f"{prefix}{item}") | |
| lines.append("") | |
| section("Chief concern", [output.chief_concern]) | |
| section("Concise summary", [output.concise_summary]) | |
| section("Timeline / story evidence", output.timeline) | |
| section("Current symptoms", output.current_symptoms) | |
| section("Relevant dental history", output.dental_history) | |
| section("Medical safety notes", output.medical_safety_notes) | |
| section("Patient goals", output.patient_goals) | |
| if output.bring_checklist: | |
| section("Bring to the visit", output.bring_checklist, boxes=True) | |
| section("Questions for the dentist", output.dentist_questions, numbered=True) | |
| section("After-visit tracker", output.after_visit_tracker, boxes=True) | |
| lines.append(" · ".join(output.limitations)) | |
| lines.append(DISCLAIMER) | |
| return "\n".join(lines) | |
| def initial_agent_dashboard_html() -> str: | |
| return """ | |
| <div class="agent-dashboard no-print"> | |
| <div class="agent-dashboard-head"> | |
| <div> | |
| <div class="agent-dashboard-title">One 4B Model · Bounded Agent Workflow</div> | |
| <div class="agent-dashboard-note">The model organizes language. Deterministic code owns safety and validation.</div> | |
| </div> | |
| <span class="workflow-route">Ready</span> | |
| </div> | |
| <div class="agent-list"> | |
| <div class="agent-row"> | |
| <span><span class="agent-name">History agent <span class="agent-type type-llm">LLM</span></span><span class="agent-role">Adaptive, bounded intake questions</span></span> | |
| <span class="agent-status status-ready">Available</span> | |
| </div> | |
| <div class="agent-row"> | |
| <span><span class="agent-name">Handoff agent <span class="agent-type type-llm">LLM</span></span><span class="agent-role">Six narrative fields only</span></span> | |
| <span class="agent-status status-idle">Waiting</span> | |
| </div> | |
| <div class="agent-row"> | |
| <span><span class="agent-name">Safety sentinel <span class="agent-type type-rule">RULE</span></span><span class="agent-role">Rules, urgency tiers, evidence spans</span></span> | |
| <span class="agent-status status-active">Always on</span> | |
| </div> | |
| <div class="agent-row"> | |
| <span><span class="agent-name">Output guard <span class="agent-type type-rule">RULE</span></span><span class="agent-role">Pydantic schema + claim filter</span></span> | |
| <span class="agent-status status-idle">Waiting</span> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| def render_agent_dashboard( | |
| status1: str, | |
| status2: str, | |
| status3: str, | |
| status4: str, | |
| *, | |
| route_note: str = "", | |
| ) -> str: | |
| def pill(status: str) -> str: | |
| classes = { | |
| "Available": "status-ready", | |
| "Complete": "status-complete", | |
| "Cached": "status-complete", | |
| "Checked": "status-complete", | |
| "Passed": "status-complete", | |
| "Clear": "status-complete", | |
| "Always on": "status-active", | |
| "Partial": "status-template", | |
| "Template": "status-template", | |
| "Fallback": "status-template", | |
| "Skipped": "status-bypass", | |
| "No model text": "status-bypass", | |
| "Waiting": "status-idle", | |
| } | |
| # Live sentinel outcome, e.g. "2 flags found" — render as active so the | |
| # real flag count stands out as the run's most important telemetry. | |
| if status.endswith("found"): | |
| css_class = "status-active" | |
| else: | |
| css_class = classes.get(status, "status-idle") | |
| return f'<span class="agent-status {css_class}">{escape(status)}</span>' | |
| return f""" | |
| <div class="agent-dashboard no-print"> | |
| <div class="agent-dashboard-head"> | |
| <div> | |
| <div class="agent-dashboard-title">One 4B Model · Bounded Agent Workflow</div> | |
| <div class="agent-dashboard-note">{escape(route_note)}</div> | |
| </div> | |
| <span class="workflow-route">Auditable run</span> | |
| </div> | |
| <div class="agent-list"> | |
| <div class="agent-row"> | |
| <span><span class="agent-name">History agent <span class="agent-type type-llm">LLM</span></span><span class="agent-role">Adaptive, bounded intake questions</span></span> | |
| {pill(status1)} | |
| </div> | |
| <div class="agent-row"> | |
| <span><span class="agent-name">Handoff agent <span class="agent-type type-llm">LLM</span></span><span class="agent-role">Drafts only validated narrative fields</span></span> | |
| {pill(status2)} | |
| </div> | |
| <div class="agent-row"> | |
| <span><span class="agent-name">Safety sentinel <span class="agent-type type-rule">RULE</span></span><span class="agent-role">Rules, urgency tiers, evidence spans</span></span> | |
| {pill(status3)} | |
| </div> | |
| <div class="agent-row"> | |
| <span><span class="agent-name">Output guard <span class="agent-type type-rule">RULE</span></span><span class="agent-role">Pydantic schema + claim filter</span></span> | |
| {pill(status4)} | |
| </div> | |
| </div> | |
| </div> | |
| """ | |