HumanPlusX / src /ui /components.py
FajarHidaa's picture
Upload 2 files
f10d92f verified
Raw History Blame Contribute Delete
11.9 kB
"""
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(
"""
<div style="text-align:center; padding: 32px 0 8px;">
<p style="
font-size:0.72rem; font-weight:600;
letter-spacing:0.2em; text-transform:uppercase;
color:#4a4a6a; margin-bottom:6px;
">Human+ Bali Β· By Benjamin White</p>
<h1 style="
font-family:'DM Serif Display', Georgia, serif;
font-size:2.6rem; font-weight:900; line-height:1;
background: linear-gradient(135deg, #a78bfa, #38bdf8, #22d3ee);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text; margin:0 0 8px;
">Lab-to-Protocol</h1>
</div>
""",
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"<div class='hp-step done'>βœ“ {label}</div>",
unsafe_allow_html=True,
)
elif i == current_step:
# Active
st.markdown(
f"<div class='hp-step active'>β—‰ {label}</div>",
unsafe_allow_html=True,
)
else:
# Pending
st.markdown(
f"<div class='hp-step pending'>β—‹ {label}</div>",
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"<p style='font-size:0.7rem; font-weight:700; letter-spacing:0.14em; "
f"color:#8b8ba7; border-left:2px solid #8b5cf6; padding-left:8px; "
f"margin:20px 0 10px;'>{prefix}{text}</p>",
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"<p style='color:{color}; font-size:0.85rem; margin:4px 0;'>"
f"{icon} &nbsp; {step}</p>",
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"<div style='border-left:3px solid {color}; padding-left:10px; margin:2px 0 0;'>",
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("</div>", 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"<blockquote style='"
f"border-left:3px solid #8b5cf6; padding:12px 16px; margin:16px 0; "
f"background:rgba(109,40,217,0.06); border-radius:0 8px 8px 0;'>"
f"<em style='color:#c4c4e0;'>{text}</em><br>"
f"<small style='color:#4a4a6a;'>β€” Benjamin White, Founder Human+ Bali</small>"
f"</blockquote>",
unsafe_allow_html=True,
)