| """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") |
|
|
|
|
| |
| |
| |
| |
| _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() |
| |
| |
| 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 |
|
|
|
|
| |
| |
| |
| |
| _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 "لم تظهر علامة طوارئ من المعلومات التي أدخلتها، لكن هذا لا ينفي وجود مشكلة تحتاج لفحص طبيب الأسنان." |
| ) |
| |
| |
| 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 "" |
| ) |
| |
| |
| 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_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_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 |
| |
| |
| 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 |
| ) |
| |
| |
| |
| |
| |
| 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", |
| } |
| |
| |
| 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> |
| """ |
|
|