"""
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,
)