"""
ui/components.py
──────────────────────────────────────────────────────────────────────────────
VoiceVerse Pro — Shared Presentational Components
All raw HTML/CSS and reusable Streamlit snippets live here.
No business logic. No session state mutations.
Every function is a pure renderer.
"""
from __future__ import annotations
import streamlit as st
# ──────────────────────────────────────────────────────────────────────────────
# Global stylesheet (injected once by app.py)
# ──────────────────────────────────────────────────────────────────────────────
def inject_css() -> None:
"""Inject the global dark-studio stylesheet into the Streamlit page."""
st.markdown("""
""", unsafe_allow_html=True)
# ──────────────────────────────────────────────────────────────────────────────
# Header
# ──────────────────────────────────────────────────────────────────────────────
def render_header() -> None:
st.markdown("""
""", unsafe_allow_html=True)
# ──────────────────────────────────────────────────────────────────────────────
# Stage tracker bar
# ──────────────────────────────────────────────────────────────────────────────
_STAGE_LABELS = [
"① Upload & Index",
"② Retrieve Context",
"③ Generate Script",
"④ Synthesize Audio",
]
def render_stage_tracker(current_stage: int) -> None:
pills = "".join(
f" i else ' pending'}'>"
f"{'✓ ' if current_stage > i else ''}{label}"
for i, label in enumerate(_STAGE_LABELS)
)
st.markdown(pills, unsafe_allow_html=True)
st.markdown("")
# ──────────────────────────────────────────────────────────────────────────────
# Mode selector — prominent pill toggle in main area
# ──────────────────────────────────────────────────────────────────────────────
def render_mode_selector() -> str:
"""
Render a prominent two-button mode toggle in the main content area.
Persists selection in st.session_state["output_mode"].
Returns the selected mode value string (matches OutputMode.value).
"""
# Bootstrap default
if "output_mode" not in st.session_state:
st.session_state["output_mode"] = "Audio Transcript"
current = st.session_state["output_mode"]
st.markdown("""
""", unsafe_allow_html=True)
col_t, col_p = st.columns(2)
with col_t:
transcript_active = current == "Audio Transcript"
if st.button(
"🎙️ Audio Transcript",
use_container_width=True,
type="primary" if transcript_active else "secondary",
key="mode_btn_transcript",
):
st.session_state["output_mode"] = "Audio Transcript"
st.rerun()
with col_p:
podcast_active = current == "Podcast (2 Speakers)"
if st.button(
"🎭 Podcast — 2 Speakers",
use_container_width=True,
type="primary" if podcast_active else "secondary",
key="mode_btn_podcast",
):
st.session_state["output_mode"] = "Podcast (2 Speakers)"
st.rerun()
# Visual label showing what's active
icon = "🎙️" if current == "Audio Transcript" else "🎭"
st.markdown(
f""
f"{icon} Active: {current}
",
unsafe_allow_html=True,
)
return st.session_state["output_mode"]
# ──────────────────────────────────────────────────────────────────────────────
# Reusable card primitives
# ──────────────────────────────────────────────────────────────────────────────
def file_card(label: str, filename: str, detail: str) -> None:
st.markdown(
f""
f"
{label}
"
f"
{filename}"
f"
{detail}"
f"
",
unsafe_allow_html=True,
)
def chunk_card(index: int, source: str, page, chunk_id, content: str) -> None:
preview = content[:600] + ("…" if len(content) > 600 else "")
st.markdown(
f""
f"
CHUNK {index} · {source} "
f"· Page {page} · ID {chunk_id}
"
f"{preview}"
f"
",
unsafe_allow_html=True,
)
def script_box(text: str) -> None:
st.markdown(
f"{text}
",
unsafe_allow_html=True,
)