""" Human+ Reusable UI Components Prinsip refactor: gunakan Streamlit native sebanyak mungkin. HTML hanya untuk elemen dekoratif (hero logo, gradient) yang tidak ada padanan native-nya. """ import streamlit as st from typing import Literal, Optional from ui.theme import COLORS, BIOMARKER_STATUS, PROTOCOL_SECTIONS # ════════════════════════════════════════════════ # HERO & NAVIGATION # ════════════════════════════════════════════════ def render_hero( subtitle: str = "Upload hasil lab Anda → Dapatkan rekomendasi IV drip & protocol personal" ): """ Hero header — HTML dipertahankan hanya untuk gradient text logo yang tidak bisa dibuat dengan st.title() biasa. """ st.markdown( """

Human+ Bali · By Benjamin White

Lab-to-Protocol

""", unsafe_allow_html=True, ) st.caption(f"🧬 {subtitle}") st.write("") def render_step_indicator(current_step: int): """ 3-step progress indicator menggunakan st.columns + native markdown. Tidak ada HTML kompleks — hanya emoji dan teks. """ steps = ["📤 Upload", "🔬 Analisis", "📋 Protocol"] cols = st.columns(3) for i, (col, label) in enumerate(zip(cols, steps), 1): with col: if i < current_step: # Done st.markdown( f"
✓ {label}
", unsafe_allow_html=True, ) elif i == current_step: # Active st.markdown( f"
◉ {label}
", unsafe_allow_html=True, ) else: # Pending st.markdown( f"
○ {label}
", unsafe_allow_html=True, ) st.divider() def render_trust_badges(): """Trust badges menggunakan st.columns — tidak ada HTML.""" col1, col2, col3, col4 = st.columns(4) badges = [ ("🔒", "Stateless"), ("🗑️", "Tidak disimpan"), ("⚡", "Gemini AI"), ("🌿", "Human+ Bali"), ] for col, (icon, text) in zip([col1, col2, col3, col4], badges): with col: st.caption(f"{icon} {text}") st.write("") def render_section_label(text: str, icon: str = ""): """ Section label menggunakan st.markdown dengan minimal styling — hanya border-left yang tidak ada padanan native. """ prefix = f"{icon} " if icon else "" st.markdown( f"

{prefix}{text}

", unsafe_allow_html=True, ) # ════════════════════════════════════════════════ # LOADING STATES # ════════════════════════════════════════════════ ANALYSIS_STEPS = [ "Membaca dokumen PDF", "Mengidentifikasi biomarker", "Mencocokkan dengan range Human+", "Menyusun temuan utama", ] PROTOCOL_STEPS = [ "Mengambil knowledge base Human+", "Menyesuaikan dengan hasil lab Anda", "Menyusun rekomendasi IV drip", "Finalisasi protocol personal", ] def render_loading_steps(steps: list[str], current_index: int): """ Loading steps menggunakan st.markdown per baris — lebih stabil dari satu blok HTML besar. """ for i, step in enumerate(steps): if i < current_index: icon, color = "✓", "#10b981" elif i == current_index: icon, color = "◉", "#8b5cf6" else: icon, color = "○", "#4a4a6a" st.markdown( f"

" f"{icon}   {step}

", unsafe_allow_html=True, ) def render_progress_bar(value: float, label: str = ""): """Native st.progress — tidak butuh HTML sama sekali.""" if label: st.caption(label) st.progress(value) # ════════════════════════════════════════════════ # BIOMARKER COMPONENTS # ════════════════════════════════════════════════ StatusType = Literal["optimal", "warning", "danger", "neutral"] _STATUS_EMOJI = { "optimal": "🟢", "warning": "🟡", "danger": "🔴", "neutral": "⚪", } _STATUS_COLOR = { "optimal": "#10b981", "warning": "#f59e0b", "danger": "#ef4444", "neutral": "#6b7280", } def render_biomarker_row( name: str, value: str, unit: str, status: StatusType, reference: str = "", ): """ Biomarker row menggunakan st.columns + st.metric. Jauh lebih stabil dari HTML div, metric punya built-in styling. Border kiri status tetap pakai satu baris HTML minimal. """ color = _STATUS_COLOR.get(status, "#6b7280") emoji = _STATUS_EMOJI.get(status, "⚪") # Border kiri berwarna — satu-satunya HTML di sini st.markdown( f"
", unsafe_allow_html=True, ) col_name, col_val = st.columns([3, 1]) with col_name: st.markdown(f"**{emoji} {name}**") if reference: st.caption(f"Target Human+: {reference}") with col_val: st.metric( label=unit, value=value, label_visibility="visible", ) st.markdown("
", unsafe_allow_html=True) st.write("") def render_biomarker_summary_banner(optimal: int, warning: int, danger: int): """ Summary count banner menggunakan st.columns + st.metric native. Tidak ada HTML sama sekali. """ total = optimal + warning + danger st.caption(f"🔬 RINGKASAN BIOMARKER — {total} MARKER DIANALISIS") col1, col2, col3 = st.columns(3) with col1: st.metric(label="🔴 Perlu Perhatian", value=str(danger)) with col2: st.metric(label="🟡 Sub-Optimal", value=str(warning)) with col3: st.metric(label="🟢 Optimal", value=str(optimal)) st.write("") def render_biomarker_chip(name: str, status: StatusType): """Inline chip menggunakan caption + emoji — tidak butuh HTML.""" emoji = _STATUS_EMOJI.get(status, "⚪") st.caption(f"{emoji} {name}") # ════════════════════════════════════════════════ # PROTOCOL COMPONENTS # ════════════════════════════════════════════════ def render_protocol_section( section_key: str, content: str, custom_icon: str = "", custom_title: str = "", accent_color: str = "", ): """ Protocol section menggunakan st.expander native sebagai container, dengan header yang jelas. Content di-render sebagai st.markdown biasa. """ cfg = PROTOCOL_SECTIONS.get(section_key, { "icon": custom_icon or "📋", "title": custom_title or "Section", "color": accent_color or COLORS["accent_violet"], }) icon = custom_icon or cfg["icon"] title = custom_title or cfg["title"] with st.expander(f"{icon} {title}", expanded=True): if content and content.strip(): st.markdown(content) else: st.caption("Tidak ada data untuk section ini.") # ════════════════════════════════════════════════ # ERROR & EMPTY STATES # ════════════════════════════════════════════════ def render_error_state( title: str = "Terjadi Kesalahan", message: str = "Silakan coba lagi.", retry_label: str = "Coba Lagi", show_contact: bool = False, ): """Error state menggunakan st.error native — selalu render dengan benar.""" st.error(f"**{title}**\n\n{message}", icon="⚠️") if show_contact: st.caption("Butuh bantuan? Hubungi [@bennywhitethatsright](https://instagram.com/bennywhitethatsright)") def render_empty_state( emoji: str = "🧬", title: str = "Belum ada data", message: str = "", ): """Empty state menggunakan st.info native.""" st.info(f"{emoji} **{title}**\n\n{message}" if message else f"{emoji} **{title}**") # ════════════════════════════════════════════════ # FOOTER # ════════════════════════════════════════════════ def render_footer(): """Footer menggunakan st.divider + st.caption native.""" st.divider() st.caption( "**FROM INJURED → OPTIMIZED** \n" "Human+ Bali · Didirikan oleh Benjamin White · " "[@bennywhitethatsright](https://instagram.com/bennywhitethatsright) \n" "*Data Anda tidak disimpan. Aplikasi 100% stateless. " "Bukan pengganti konsultasi medis.*" ) # ════════════════════════════════════════════════ # UTILITY CARDS # ════════════════════════════════════════════════ def render_info_card(title: str, body: str, icon: str = "ℹ️"): """ Info card menggunakan st.info — native, selalu render. Body sebaiknya plain text, bukan HTML. """ # Strip HTML tags sederhana dari body sebelum render import re clean_body = re.sub(r"<[^>]+>", "", body).strip() st.info(f"{icon} **{title}**\n\n{clean_body}") def render_success_banner(title: str, subtitle: str = ""): """Success banner menggunakan st.success native.""" text = f"**{title}**" if subtitle: text += f"\n\n{subtitle}" st.success(text, icon="✅") def render_highlight_quote(text: str): """ Motivational quote — satu-satunya komponen yang tetap pakai HTML minimal karena border-left gradient tidak ada di native Streamlit. """ st.markdown( f"
" f"{text}
" f"— Benjamin White, Founder Human+ Bali" f"
", unsafe_allow_html=True, )