| | """ |
| | 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 |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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) |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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_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("") |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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). |
| | """ |
| | |
| | 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() |
| |
|
| | |
| | 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"] |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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, |
| | ) |