Spaces:
Sleeping
Sleeping
Download src/ui/components.py from FajarHidaa/HumanPlusX: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/spaces/FajarHidaa/HumanPlusX/resolve/main/src/ui/components.py
- Command line
-
hf download hf://spaces/FajarHidaa/HumanPlusX/src/ui/components.py
-
curl -L -o components.py https://huggingface.co/spaces/FajarHidaa/HumanPlusX/resolve/main/src/ui/components.py
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} {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, | |
| ) | |