voiceAI / src /ui /components.py
ahanbose's picture
Update src/ui/components.py
f0251df verified
"""
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("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=JetBrains+Mono:wght@400;500&family=Inter:wght@300;400;500&display=swap');
:root {
--bg-primary: #0d0f14;
--bg-secondary: #13161e;
--bg-card: #1a1d27;
--accent: #7c6af7;
--accent-warm: #f7a06a;
--accent-green: #52e5aa;
--text: #e8eaf0;
--text-muted: #8b8fa8;
--border: #2a2d3a;
--radius: 12px;
}
html, body, [class*="css"] {
background-color: var(--bg-primary);
color: var(--text);
font-family: 'Inter', sans-serif;
}
#MainMenu, footer, header { visibility: hidden; }
/* ── Header banner ── */
.vv-header {
background: linear-gradient(135deg, #1a1d27 0%, #13161e 50%, #0f111a 100%);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem 2.5rem;
margin-bottom: 1.5rem;
position: relative;
overflow: hidden;
}
.vv-header::before {
content: '';
position: absolute; top: -50%; left: -50%;
width: 200%; height: 200%;
background:
radial-gradient(ellipse at 30% 50%, rgba(124,106,247,.08) 0%, transparent 60%),
radial-gradient(ellipse at 70% 50%, rgba(247,160,106,.05) 0%, transparent 60%);
pointer-events: none;
}
.vv-title {
font-family: 'Syne', sans-serif;
font-weight: 800; font-size: 2.4rem;
background: linear-gradient(90deg, #7c6af7, #f7a06a, #52e5aa);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text; margin: 0; line-height: 1.1;
}
.vv-subtitle {
color: var(--text-muted); font-size: .95rem;
font-weight: 300; margin-top: .4rem; letter-spacing: .03em;
}
.vv-badge {
display: inline-block;
background: rgba(124,106,247,.15);
border: 1px solid rgba(124,106,247,.3);
color: #a99cf9;
font-family: 'JetBrains Mono', monospace;
font-size: .7rem; padding: 2px 10px;
border-radius: 20px; margin-top: .6rem; letter-spacing: .08em;
}
/* ── Stage tracker ── */
.stage-pill {
display: inline-flex; align-items: center; gap: 6px;
font-family: 'JetBrains Mono', monospace; font-size: .72rem;
padding: 4px 12px; border-radius: 20px;
margin-right: 6px; margin-bottom: 6px;
border: 1px solid rgba(82,229,170,.25);
background: rgba(82,229,170,.1); color: var(--accent-green);
}
.stage-pill.pending {
border-color: rgba(139,143,168,.2);
background: rgba(139,143,168,.1); color: var(--text-muted);
}
/* ── Cards ── */
.vv-card {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: var(--radius); padding: 1.2rem 1.5rem; margin-bottom: .8rem;
}
.vv-card-label {
font-family: 'JetBrains Mono', monospace; font-size: .68rem;
color: var(--accent); text-transform: uppercase;
letter-spacing: .12em; margin-bottom: .4rem;
}
/* ── Sidebar ── */
[data-testid="stSidebar"] {
background: var(--bg-secondary);
border-right: 1px solid var(--border);
}
/* ── Metrics ── */
[data-testid="stMetric"] {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 8px; padding: .75rem 1rem;
}
/* ── Buttons ── */
.stButton > button {
background: linear-gradient(135deg, #7c6af7, #6857e8);
color: white; border: none; border-radius: 8px;
font-family: 'Inter', sans-serif; font-weight: 500;
padding: .6rem 1.6rem; transition: opacity .2s; width: 100%;
}
.stButton > button:hover { opacity: .88; }
/* ── Script box ── */
.script-display {
background: #10121a; border: 1px solid var(--border);
border-left: 3px solid var(--accent); border-radius: 8px;
padding: 1.4rem 1.6rem; font-family: 'Inter', sans-serif;
font-size: .96rem; line-height: 1.85; color: #d4d8e8;
white-space: pre-wrap; max-height: 420px; overflow-y: auto;
}
/* ── Chunk viewer ── */
.chunk-box {
background: #0f111a; border: 1px solid var(--border);
border-radius: 8px; padding: .9rem 1.1rem;
font-family: 'JetBrains Mono', monospace; font-size: .78rem;
color: var(--text-muted); margin-bottom: .6rem; line-height: 1.6;
}
.chunk-meta {
color: var(--accent-warm); font-size: .68rem;
margin-bottom: .4rem; letter-spacing: .05em;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg-primary); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
</style>
""", unsafe_allow_html=True)
# ──────────────────────────────────────────────────────────────────────────────
# Header
# ──────────────────────────────────────────────────────────────────────────────
def render_header() -> None:
st.markdown("""
<div class='vv-header'>
<h1 class='vv-title'>πŸŽ™οΈ VoiceVerse Pro</h1>
<p class='vv-subtitle'>Modular RAG Β· Mistral-24B Β· Synthetic Audio Studio</p>
<span class='vv-badge'>2026 STABLE BUILD Β· ML ASSIGNMENT 2</span>
</div>
""", 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"<span class='stage-pill{'' if current_stage > i else ' pending'}'>"
f"{'βœ“ ' if current_stage > i else ''}{label}</span>"
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("""
<style>
.mode-toggle-wrap {
display: flex; gap: 12px; margin: 0.4rem 0 1.2rem 0;
}
.mode-btn {
flex: 1; padding: 0.65rem 1rem; border-radius: 10px;
font-family: 'Inter', sans-serif; font-size: 0.92rem; font-weight: 500;
text-align: center; cursor: pointer; transition: all 0.2s;
border: 1.5px solid var(--border);
background: var(--bg-card); color: var(--text-muted);
}
.mode-btn.active {
background: linear-gradient(135deg, rgba(124,106,247,0.18), rgba(124,106,247,0.06));
border-color: #7c6af7; color: #c4baff;
}
</style>
""", 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"<p style='font-size:0.8rem; color:#8b8fa8; margin: -0.6rem 0 0.8rem 0;'>"
f"{icon} Active: <strong style='color:#c4baff'>{current}</strong></p>",
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"<div class='vv-card'>"
f"<div class='vv-card-label'>{label}</div>"
f"<strong>{filename}</strong><br>"
f"<small style='color:#8b8fa8'>{detail}</small>"
f"</div>",
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"<div class='chunk-box'>"
f"<div class='chunk-meta'>CHUNK {index} Β· {source} "
f"Β· Page {page} Β· ID {chunk_id}</div>"
f"{preview}"
f"</div>",
unsafe_allow_html=True,
)
def script_box(text: str) -> None:
st.markdown(
f"<div class='script-display'>{text}</div>",
unsafe_allow_html=True,
)