clarke / frontend /components.py
yashvshetty's picture
Restore all features from origin/fresh-main
874f913
Raw
History Blame
24.4 kB
"""HTML builders for Clarke frontend screens."""
from __future__ import annotations
from datetime import date
from html import escape
from typing import Any
def _open_patient_click_js(index: int) -> str:
"""Return debug-friendly JS snippet for dashboard patient button clicks.
Args:
index (int): Dashboard patient-card index.
Returns:
str: Inline JS IIFE for HTML onclick attributes.
"""
return (
"(function(){"
f"var el=document.getElementById('hidden-select-{index}');"
"if(!el){return;}"
"if(el.tagName==='BUTTON'){el.click();}else{"
"var btn=el.querySelector('button');"
"if(btn){btn.click();}"
"}"
"})()"
)
def build_global_style_block() -> str:
"""Return the global CSS block injected via gr.HTML.
Args:
None: This function has no runtime inputs.
Returns:
str: Complete style tag with animation keyframes and resets.
"""
return """
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
</style>
<style>
@keyframes clarkeGradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes btnShine {0% { left: -100%; }50% { left: 100%; }100% { left: 100%; }}
@keyframes fadeSlideIn {0% { opacity: 0; transform: translateY(20px); }100% { opacity: 1; transform: translateY(0); }}
@keyframes recordPulse {0%, 100% { box-shadow: 0 0 0 0 rgba(212, 175, 55, 0.6); }50% { box-shadow: 0 0 0 24px rgba(212, 175, 55, 0); }}
@keyframes progressSpin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }}
@keyframes progressGlow {0%, 100% { opacity: 0.6; }50% { opacity: 1; }}
@keyframes clarkeLogoShimmer {
0% { filter: brightness(1) drop-shadow(0 0 3px rgba(212,175,55,0.3)); }
50% { filter: brightness(1.5) drop-shadow(0 0 12px rgba(255,193,7,0.7)); }
100% { filter: brightness(1) drop-shadow(0 0 3px rgba(212,175,55,0.3)); }
}
html, body { margin: 0 !important; padding: 0 !important; overflow-x: hidden !important; }
/* Letter Preferences Accordion */
#clarke-letter-prefs {
margin: 0 48px 24px 48px !important;
border: 1px solid rgba(212, 175, 55, 0.25) !important;
border-radius: 12px !important;
background: rgba(255, 255, 255, 0.65) !important;
backdrop-filter: blur(8px) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03) !important;
}
#clarke-letter-prefs > .label-wrap {
padding: 14px 20px !important;
background: transparent !important;
border: none !important;
border-bottom: none !important;
cursor: pointer !important;
}
#clarke-letter-prefs > .label-wrap span {
font-family: 'DM Serif Display', serif !important;
font-size: 16px !important;
color: #D4AF37 !important;
}
#clarke-letter-prefs > .label-wrap:hover {
background: rgba(212, 175, 55, 0.04) !important;
}
#clarke-letter-prefs input[type="text"],
#clarke-letter-prefs textarea {
font-family: 'Inter', sans-serif !important;
font-size: 14px !important;
border: 1px solid rgba(212, 175, 55, 0.2) !important;
border-radius: 8px !important;
background: rgba(255, 255, 255, 0.85) !important;
color: #1A1A2E !important;
padding: 10px 14px !important;
transition: border-color 0.3s ease, box-shadow 0.3s ease !important;
}
#clarke-letter-prefs input[type="text"]:focus,
#clarke-letter-prefs textarea:focus {
border-color: #D4AF37 !important;
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.12) !important;
outline: none !important;
}
#clarke-letter-prefs label span {
font-family: 'Inter', sans-serif !important;
font-size: 13px !important;
font-weight: 600 !important;
color: #555 !important;
}
/* Letter Preferences Accordion */
#clarke-letter-prefs {
margin: 0 48px 24px 48px !important;
border: 1px solid rgba(212, 175, 55, 0.2) !important;
border-radius: 12px !important;
background: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(8px) !important;
overflow: hidden !important;
}
#clarke-letter-prefs > .label-wrap {
padding: 14px 20px !important;
background: transparent !important;
border: none !important;
cursor: pointer !important;
}
#clarke-letter-prefs > .label-wrap > span {
font-family: 'DM Serif Display', serif !important;
font-size: 16px !important;
color: #D4AF37 !important;
}
#clarke-letter-prefs > .label-wrap:hover {
background: rgba(212, 175, 55, 0.04) !important;
}
#clarke-letter-prefs .wrap {
padding: 4px 20px 16px 20px !important;
border-top: 1px solid rgba(212, 175, 55, 0.12) !important;
}
#clarke-letter-prefs input[type="text"],
#clarke-letter-prefs textarea {
font-family: 'Inter', sans-serif !important;
font-size: 14px !important;
border: 1px solid rgba(212, 175, 55, 0.2) !important;
border-radius: 8px !important;
background: rgba(255, 255, 255, 0.8) !important;
color: #1A1A2E !important;
padding: 10px 14px !important;
transition: all 0.3s ease !important;
}
#clarke-letter-prefs input[type="text"]:focus,
#clarke-letter-prefs textarea:focus {
border-color: #D4AF37 !important;
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.15) !important;
outline: none !important;
}
#clarke-letter-prefs label span {
font-family: 'Inter', sans-serif !important;
font-size: 13px !important;
font-weight: 600 !important;
color: #555 !important;
}
/* Document Type Radio — matches Letter Preferences */
fieldset#clarke-doc-type,
#clarke-doc-type {
--block-border-width: 0px !important;
--block-border-color: transparent !important;
--border-color-primary: transparent !important;
--block-background-fill: transparent !important;
--block-shadow: none !important;
--block-radius: 12px !important;
margin: 0 48px 16px 48px !important;
border: 1px solid rgba(212, 175, 55, 0.2) !important;
border-radius: 12px !important;
background: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(8px) !important;
-webkit-backdrop-filter: blur(8px) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03) !important;
padding: 0 !important;
overflow: hidden !important;
}
fieldset#clarke-doc-type > div,
#clarke-doc-type > div,
#clarke-doc-type .form {
background: transparent !important;
border: none !important;
box-shadow: none !important;
border-radius: 0 !important;
}
#clarke-doc-type > span[data-testid="block-info"] {
font-family: 'DM Serif Display', serif !important;
font-size: 16px !important;
color: #D4AF37 !important;
padding: 14px 20px 8px 20px !important;
display: block !important;
}
#clarke-doc-type > div.wrap {
padding: 4px 20px 16px 20px !important;
border-top: 1px solid rgba(212, 175, 55, 0.12) !important;
background: transparent !important;
gap: 12px !important;
border-left: none !important;
border-right: none !important;
border-bottom: none !important;
box-shadow: none !important;
}
fieldset#clarke-doc-type label,
#clarke-doc-type label {
font-family: 'DM Serif Display', serif !important;
font-size: 14px !important;
border: 1px solid rgba(212, 175, 55, 0.25) !important;
border-radius: 8px !important;
padding: 10px 20px !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
background: rgba(255, 255, 255, 0.6) !important;
color: #555 !important;
}
#clarke-doc-type label.selected {
background: rgba(212, 175, 55, 0.12) !important;
border-color: #D4AF37 !important;
color: #1A1A2E !important;
font-weight: 600 !important;
}
#clarke-doc-type label:hover {
background: rgba(212, 175, 55, 0.06) !important;
border-color: rgba(212, 175, 55, 0.4) !important;
}
#clarke-doc-type input[type="radio"] {
accent-color: #D4AF37 !important;
}
#clarke-doc-type *:not(label):not(input):not(span) {
background: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
}
#clarke-doc-type > div[class*="form"],
#clarke-doc-type > div[class*="svelte"] {
background: transparent !important;
border: none !important;
box-shadow: none !important;
overflow: visible !important;
}
/* Make Gradio progress bar gold instead of red */
.progress-bar, .progress-bar > .progress-bar-wrap, .progress-bar > .progress-bar-wrap > .progress-bar-fill {
background: linear-gradient(135deg, #D4AF37, #F0D060) !important;
}
.eta-bar { background: rgba(212, 175, 55, 0.15) !important; }
/* Hide Gradio error toasts */
.toast-wrap, .toast-body, .error { display: none !important; }
#hidden-select-0, #hidden-select-1, #hidden-select-2, #hidden-select-3, #hidden-select-4,
#hidden-start-consultation, #hidden-back, #hidden-cancel, #hidden-regenerate, #hidden-copy, #hidden-download,
#hidden-end-consultation, #hidden-sign-off, #hidden-next-patient {
display: block !important;
position: fixed !important;
top: -9999px !important;
left: -9999px !important;
width: 1px !important;
height: 1px !important;
opacity: 0 !important;
overflow: hidden !important;
}
.patient-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
cursor: pointer !important;
position: relative !important;
overflow: hidden !important;
}
.patient-card:hover {
transform: translateY(-6px) scale(1.015) !important;
box-shadow: 0 12px 32px rgba(212, 175, 55, 0.25), 0 4px 12px rgba(0,0,0,0.1) !important;
border-color: #D4AF37 !important;
}
.patient-card:active { transform: translateY(-2px) scale(1.005) !important; }
.patient-card::after {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: -100% !important;
width: 100% !important;
height: 100% !important;
background: linear-gradient(90deg, transparent, rgba(212,175,55,0.1), transparent) !important;
transition: left 0.6s ease !important;
border-radius: inherit !important;
}
.patient-card:hover::after { left: 100% !important; }
.clarke-btn-gold {
background: linear-gradient(135deg, #D4AF37 0%, #F0D060 100%) !important;
color: #1A1A2E !important;
border: none !important;
padding: 12px 28px !important;
border-radius: 8px !important;
font-family: 'Inter', sans-serif !important;
font-weight: 600 !important;
font-size: 15px !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.3) !important;
}
.clarke-btn-gold:hover {
background: linear-gradient(135deg, #E8C84A 0%, #F5E070 100%) !important;
box-shadow: 0 0 20px rgba(212, 175, 55, 0.5), 0 4px 16px rgba(212, 175, 55, 0.3) !important;
transform: translateY(-2px) !important;
}
.clarke-btn-gold:active {
transform: translateY(0) !important;
box-shadow: 0 0 12px rgba(212, 175, 55, 0.6) !important;
}
.clarke-btn-secondary {
background: transparent !important;
color: #1A1A2E !important;
border: 2px solid #D4AF37 !important;
padding: 10px 24px !important;
border-radius: 8px !important;
font-family: 'Inter', sans-serif !important;
font-weight: 500 !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
}
.clarke-btn-secondary:hover {
background: rgba(212, 175, 55, 0.1) !important;
box-shadow: 0 0 12px rgba(212, 175, 55, 0.3) !important;
transform: translateY(-1px) !important;
}
#clarke-audio-input, [data-testid="audio"] {
position: fixed !important;
top: -9999px !important;
left: -9999px !important;
width: 1px !important;
height: 1px !important;
opacity: 0 !important;
overflow: hidden !important;
}
</style>
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="(function(){function e(){document.documentElement.style.setProperty('background','#F8F6F1','important');var a=document.querySelector('gradio-app');if(a){a.style.setProperty('background','transparent','important');a.style.setProperty('padding','0','important');a.style.setProperty('margin','0','important');a.style.setProperty('overflow-x','hidden','important');}document.querySelectorAll('.gradio-container,[class*=gradio-container-]').forEach(function(c){c.style.setProperty('max-width','100vw','important');c.style.setProperty('padding','0','important');c.style.setProperty('margin','0','important');c.style.setProperty('background','transparent','important');});document.body.style.setProperty('margin','0','important');document.body.style.setProperty('padding','0','important');document.body.style.setProperty('background','transparent','important');var f=document.querySelector('footer');if(f)f.style.display='none';}if(!document.getElementById('clarke-sunrise-glow')){var s=document.createElement('style');s.textContent='@keyframes clarkeWarmthPulse{0%{opacity:0.55;transform:scaleY(1) scaleX(1);}50%{opacity:1;transform:scaleY(1.35) scaleX(1.12);}100%{opacity:0.55;transform:scaleY(1) scaleX(1);}}';document.head.appendChild(s);var g=document.createElement('div');g.id='clarke-sunrise-glow';g.style.cssText='position:fixed;top:0;left:0;width:100vw;height:600px;pointer-events:none;z-index:0;background:radial-gradient(ellipse 140% 110% at 50% 0%, rgba(255,193,7,0.80) 0%, rgba(255,213,79,0.55) 20%, rgba(212,175,55,0.28) 45%, transparent 75%);animation:clarkeWarmthPulse 8s ease-in-out infinite;transform-origin:top center;';document.body.insertBefore(g,document.body.firstChild);console.log('Clarke: Sunrise glow injected');}window.clarkePrintPDF=function(){var el=document.getElementById('signed-letter-text');var text='';if(el){text=el.innerText||el.textContent;}if(!text){alert('No letter text found');return;}text=text.trim();var headings=['History of Presenting Complaint','Examination','Investigations','Assessment','Plan'];var lines=text.split('\\n');var css='@page{size:A4;margin:25mm 20mm 25mm 20mm;}body{font-family:Helvetica,Arial,sans-serif;font-size:12pt;line-height:1.6;color:#1a1a2e;margin:0;padding:0;}.hdr{border-top:3px solid #D4AF37;margin-bottom:8px;}.trust{text-align:right;color:#888;font-size:12pt;margin-bottom:16px;}.gl{border-top:1.5px solid #D4AF37;margin:12px 0;}.sh{font-weight:bold;font-size:14pt;color:#1a1a2e;margin-top:20px;margin-bottom:4px;border-bottom:2px solid #D4AF37;display:inline-block;padding-bottom:2px;}.rl{font-weight:bold;font-size:13pt;}.pi{margin-left:12px;}.so{margin-top:24px;}.sn{font-weight:bold;}.ft{margin-top:40px;text-align:center;color:#bbb;font-size:9pt;}';var h='<!DOCTYPE html><html><head><style>'+css+'</style></head><body>';h+='<div class=hdr></div>';h+='<div class=trust>Clarke NHS Trust<br>General Practice Department<br>University Hospital London</div>';h+='<div class=gl></div>';var inSignoff=false;for(var i=0;i<lines.length;i++){var line=lines[i].trim();if(!line){h+='<br>';continue;}var isH=false;for(var j=0;j<headings.length;j++){if(line===headings[j]){isH=true;break;}}if(isH){h+='<div class=sh>'+line+'</div>';continue;}if(line.match(/^Re:/)){h+='<div class=rl>'+line+'</div>';continue;}if(line.match(/^Warm regards/)||line.match(/^Yours sincerely/)){inSignoff=true;h+='<div class=so>'+line+'</div>';continue;}if(inSignoff){h+='<div class=sn>'+line+'</div>';continue;}if(line.match(/^\d+\./)){h+='<div class=pi>'+line+'</div>';continue;}h+='<div>'+line+'</div>';}h+='<div class=ft>Generated by Clarke - AI Clinical Documentation System</div></body></html>';var iframe=document.createElement('iframe');iframe.style.cssText='position:fixed;top:-9999px;left:-9999px;width:210mm;height:297mm;';document.body.appendChild(iframe);iframe.contentDocument.open();iframe.contentDocument.write(h);iframe.contentDocument.close();setTimeout(function(){iframe.contentWindow.print();setTimeout(function(){document.body.removeChild(iframe);},2000);},500);console.log('Clarke: Print PDF dialog opened');};console.log('Clarke: clarkePrintPDF registered');e();[100,300,600,1200,2500,5000].forEach(function(t){setTimeout(e,t);});new MutationObserver(function(){e();}).observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:['style','class']});console.log('Clarke: Layout enforcer active via img onload');})()" style="display:none;position:absolute;width:0;height:0;">
"""
def build_dashboard_html(clinic_payload: dict[str, Any], completed_patients: list[int] | None = None) -> str:
"""Render the S1 dashboard markup with top gradient and white card area.
Args:
clinic_payload (dict[str, Any]): Clinic roster and clinician metadata.
Returns:
str: Complete HTML markup for the dashboard screen.
"""
clinician = clinic_payload.get("clinician", {})
_today = date.today().strftime("%d %B %Y")
completed_lookup = set(completed_patients or [])
cards: list[str] = []
for index, patient in enumerate(clinic_payload.get("patients", [])):
click_js = _open_patient_click_js(index)
completed = index in completed_lookup
card_style = "opacity:0.55;" if completed else ""
hover_class = "completed-patient-card" if completed else ""
badge = "<div style=\"position:absolute;top:12px;right:12px;background:#27ae60;color:white;padding:4px 12px;border-radius:12px;font-family:'Inter',sans-serif;font-size:12px;font-weight:600;\">✓ Complete</div>" if completed else ""
button_markup = (
f"<button onclick=\"{click_js}\" style=\"background:transparent;border:2px solid #27ae60;color:#27ae60;padding:8px 20px;border-radius:8px;font-family:'Inter',sans-serif;font-weight:600;cursor:pointer;transition:all 0.3s ease;\" onmouseover=\"this.style.background='rgba(39,174,96,0.1)'\" onmouseout=\"this.style.background='transparent'\">Review Letter</button>"
if completed
else f"<button class=\"clarke-btn-gold\" onclick=\"{click_js}\">Open Patient</button>"
)
cards.append(
f"""
<div class="patient-card {hover_class}" style="background:rgba(255,255,255,0.95);border:1px solid rgba(212,175,55,0.12);border-radius:16px;padding:24px 28px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,0.04);animation:fadeSlideIn 0.5s ease forwards;animation-delay:{index * 0.1}s;opacity:0;{card_style}">
{badge}
<div style="position:absolute;left:0;top:0;width:3px;height:100%;background:linear-gradient(180deg,#D4AF37,#F0D060);"></div>
<div style="display:flex;justify-content:space-between;align-items:center;gap:16px;">
<div>
<div style="font-family:'Inter',sans-serif;font-size:12px;font-weight:600;color:#D4AF37;letter-spacing:0.06em;text-transform:uppercase;margin-bottom:6px;">{escape(str(patient.get('time', '--:--')))}</div>
<div style="font-family:'DM Serif Display',serif;font-size:20px;color:#1A1A2E;margin-bottom:4px;">{escape(str(patient.get('name', 'Unknown patient')))}</div>
<div style="font-family:'Inter',sans-serif;font-size:14px;color:#555;margin-bottom:2px;">{escape(str(patient.get('age', '')))} · {escape(str(patient.get('sex', '')))}</div>
<div style="font-family:'Inter',sans-serif;font-size:14px;color:#555;font-style:italic;">{escape(str(patient.get('summary', '')))}</div>
</div>
{button_markup}
</div>
</div>
"""
)
return f"""
<style>
@keyframes clarkeGradientShift {{
0% {{ background-position: 0% 50%; }}
50% {{ background-position: 100% 50%; }}
100% {{ background-position: 0% 50%; }}
}}
.completed-patient-card:hover {{ opacity: 0.7 !important; box-shadow: 0 4px 14px rgba(0,0,0,0.08) !important; transform: translateY(-2px) scale(1.005) !important; }}
@keyframes clarkeWarmth {{
0% {{ opacity: 0.6; transform: scaleY(1) scaleX(1); }}
50% {{ opacity: 1; transform: scaleY(1.2) scaleX(1.05); }}
100% {{ opacity: 0.6; transform: scaleY(1) scaleX(1); }}
}}
</style>
<div id="clarke-app-wrapper" style="min-height:100vh;margin:0;padding:0;background:transparent;position:relative;">
<div style="position:relative;z-index:1;padding:32px 48px 24px 48px;">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px;">
<div style="display:flex; align-items:center; gap:12px;">
<div style="display:inline-block;">
<svg width="48" height="44" viewBox="0 0 100 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 5-sided Superman pentagon: flat top, wide shoulders, long sides, bottom point -->
<!-- Outer border (gold) -->
<path d="M20,2 L80,2 L98,28 L50,88 L2,28 Z" fill="#D4AF37"/>
<!-- Gap (warm white) -->
<path d="M26,8 L74,8 L89,30 L50,80 L11,30 Z" fill="#F8F6F1"/>
<!-- Inner fill (gold, matches Clarke text) -->
<path d="M31,14 L69,14 L82,32 L50,73 L18,32 Z" fill="#D4AF37"/>
</svg>
</div>
<span style="font-family:'DM Serif Display',serif; font-size:32px; color:#D4AF37; font-weight:400;">Clarke</span>
</div>
<div style="position:relative;">
<input id="clarke-patient-search" type="text" placeholder="Search patients..." oninput="(function(v){{document.querySelectorAll('.patient-card').forEach(function(c){{var t=c.innerText.toLowerCase();c.style.display=t.includes(v.toLowerCase())?'':'none';}});}})(this.value)" style="padding:10px 16px 10px 38px;border:1px solid rgba(212,175,55,0.25);border-radius:10px;background:rgba(255,255,255,0.8);backdrop-filter:blur(8px);font-family:'DM Serif Display',serif;font-size:15px;color:#1A1A2E;width:240px;outline:none;transition:all 0.3s ease;" onfocus="this.style.borderColor='#D4AF37';this.style.boxShadow='0 0 12px rgba(212,175,55,0.15)'" onblur="this.style.borderColor='rgba(212,175,55,0.25)';this.style.boxShadow='none'" />
<span style="position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#D4AF37;font-size:15px;pointer-events:none;">&#128269;</span>
</div>
</div>
<div style="padding:14px 22px;background:rgba(212,175,55,0.06);backdrop-filter:blur(12px);border:1px solid rgba(212,175,55,0.15);border-radius:12px;border-left:4px solid #D4AF37;">
<span style="font-family:'DM Serif Display',serif;font-size:20px;color:#1A1A2E;">{escape(str(clinician.get('name', 'Dr. Sarah Chen')))}</span>
<span style="font-family:'Inter',sans-serif;font-size:14px;color:#555;margin-left:12px;">{escape(str(clinician.get('specialty', 'General Practice')))}{escape(_today)}</span>
</div>
</div>
<div id="clarke-content-area" style="position:relative;z-index:1;background:transparent;margin:0;padding:32px 48px;min-height:70vh;">
{''.join(cards)}
</div>
</div>
"""
def build_status_badge_html(label: str, color: str) -> str:
"""Build a rounded status badge.
Args:
label (str): Badge text.
color (str): Accent color hex.
Returns:
str: Badge HTML markup.
"""
return f"<div style='display:inline-flex;align-items:center;gap:6px;padding:6px 16px;border-radius:20px;background:color-mix(in oklab, {color} 12%, white);border:1px solid color-mix(in oklab, {color} 24%, white);font-family:Inter,sans-serif;font-size:13px;font-weight:600;color:{color};'>{escape(label)}</div>"
class _RenderedFragment:
"""Lightweight rendered fragment container for legacy test compatibility."""
def __init__(self, value: str) -> None:
"""Store rendered HTML fragment value."""
self.value = value
def build_patient_card(patient: dict[str, Any]) -> _RenderedFragment:
"""Build a legacy patient card fragment for tests and compatibility helpers."""
markup = f"<div class='patient-card'><strong>{escape(str(patient.get('name', 'Unknown patient')))}</strong></div>"
return _RenderedFragment(markup)
def build_status_badge(status: str) -> _RenderedFragment:
"""Build a legacy status badge fragment for tests and compatibility helpers."""
css_class = "clarke-badge-review" if status == "ready" else "clarke-badge"
return _RenderedFragment(f"<span class='{css_class}'>{escape(status)}</span>")