import streamlit as st
import json
import os
from datetime import datetime
from dotenv import load_dotenv
# Import custom functions
from pipelines_functions import (
generate_technical_content, correct_technical_content, refine_technical_content,
generate_operational_content, correct_operational_content, refine_operational_content
)
from utils_functions import (
validate_and_sanitize_topic, check_cache, save_to_cache, validate_and_select_urls,
get_collections, PipelineMetrics
)
from image_generation_functions import process_images_for_pipeline
load_dotenv()
# ============================================================
# PAGE CONFIGURATION
# ============================================================
st.set_page_config(
page_title="LearnOnTheGo",
page_icon="🎓",
layout="wide",
initial_sidebar_state="collapsed"
)
# ============================================================
# HELPER FUNCTIONS
# ============================================================
def sanitize_for_html(raw_text: str) -> str:
"""Escape HTML special characters for safe embedding."""
if not isinstance(raw_text, str):
raw_text = str(raw_text)
return raw_text.replace("&", "&").replace("<", "<").replace(">", ">")
def detect_code_content(text: str) -> bool:
"""Detect if content looks like code (has HTML tags, brackets, etc)."""
code_indicators = ['
/* Root color palette */
:root {
--primary-blue: #2563eb;
--accent-teal: #0891b2;
--light-blue: #eff6ff;
--light-teal: #e0f2fe;
--text-dark: #1e293b;
--text-light: #64748b;
--border-color: #bae6fd;
--shadow: 0 8px 24px rgba(37, 99, 235, 0.12);
--shadow-lg: 0 20px 50px rgba(37, 99, 235, 0.25);
}
/* Overall app styling */
.stApp {
background: linear-gradient(135deg, #dbeafe 0%, #cffafe 100%);
}
/* Hide default Streamlit elements */
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
header {visibility: hidden;}
/* Main container */
.block-container {
max-width: 1400px;
padding-top: 2rem;
padding-bottom: 2rem;
}
/* Header styling */
.header-container {
background: linear-gradient(110deg, var(--primary-blue) 0%, var(--accent-teal) 100%);
padding: 60px 30px;
border-radius: 28px;
text-align: center;
margin-bottom: 40px;
box-shadow: var(--shadow-lg);
}
.header-container h1 {
color: white;
font-size: 62px;
margin: 0;
font-weight: 900;
letter-spacing: 3px;
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* Search box styling */
.search-container {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
padding: 35px;
border-radius: 24px;
margin-bottom: 30px;
border: 3px solid var(--border-color);
box-shadow: var(--shadow);
}
/* Text input styling */
.stTextInput > div > div > input {
border: 3px solid var(--accent-teal) !important;
border-radius: 14px !important;
padding: 16px 20px !important;
font-size: 17px !important;
background-color: white !important;
transition: all 0.3s ease !important;
}
.stTextInput > div > div > input:focus {
border-color: var(--primary-blue) !important;
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.15) !important;
outline: none !important;
}
.stTextInput > div > div > input::placeholder {
color: rgba(100, 116, 139, 0.6) !important;
font-weight: 500 !important;
}
/* Radio container */
.stRadio > div[role="radiogroup"] {
display: flex !important;
gap: 0 !important;
background: #e0e7ff !important;
border-radius: 14px !important;
padding: 4px !important;
border: 3px solid var(--border-color) !important;
width: fit-content !important;
margin: 0 auto !important;
}
/* Individual radio labels */
.stRadio > div[role="radiogroup"] > label {
background: transparent !important;
border: none !important;
padding: 12px 32px !important;
border-radius: 10px !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
font-weight: 700 !important;
color: var(--text-light) !important;
font-size: 15px !important;
text-align: center !important;
min-width: 140px !important;
margin: 0 !important;
flex: 1 !important;
}
.stRadio > div[role="radiogroup"] > label:hover {
background: rgba(255, 255, 255, 0.5) !important;
}
.stRadio > div[role="radiogroup"] > label[data-checked="true"],
.stRadio > div[role="radiogroup"] > label:has(input:checked),
.stRadio > div[role="radiogroup"] > label[aria-checked="true"] {
background: linear-gradient(110deg, var(--primary-blue) 0%, var(--accent-teal) 100%) !important;
color: white !important;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.35) !important;
}
.stRadio input[type="radio"] {
display: none !important;
}
/* Button styling */
.stButton > button {
background: linear-gradient(100deg, var(--primary-blue) 0%, var(--accent-teal) 100%) !important;
color: white !important;
font-weight: 800 !important;
padding: 18px 40px !important;
border-radius: 14px !important;
border: none !important;
transition: all 0.3s ease !important;
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.3) !important;
font-size: 17px !important;
letter-spacing: 0.5px !important;
}
.stButton > button:hover {
background: linear-gradient(100deg, #1d4ed8 0%, #0e7490 100%) !important;
transform: translateY(-3px) !important;
box-shadow: 0 10px 30px rgba(37, 99, 235, 0.4) !important;
}
/* SLIDE BOX - WITHOUT PROGRESS BAR */
.slide-box-wrapper {
background: white;
border-radius: 28px;
border: 3px solid var(--border-color);
box-shadow: 0 20px 60px rgba(37, 99, 235, 0.18);
padding: 50px 45px;
margin: 40px auto;
max-width: 1000px;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Slide content */
.slide-box-wrapper > * {
display: block !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* Slide title */
.slide-box-wrapper h2 {
font-size: 42px;
font-weight: 900;
color: var(--primary-blue);
margin: 0 0 20px 0 !important;
letter-spacing: 1px;
line-height: 1.3;
text-shadow: 0 2px 8px rgba(37, 99, 235, 0.15);
word-wrap: break-word;
overflow-wrap: break-word;
text-align: center;
}
/* Slide text and paragraphs */
.slide-box-wrapper p {
font-size: 20px;
color: var(--text-dark);
line-height: 2.2;
margin: 0 0 24px 0 !important;
font-weight: 500;
text-align: left;
padding: 0 20px;
box-sizing: border-box;
}
/* Code block styling */
.slide-box-wrapper pre {
background: #f8fafc;
border-radius: 12px;
padding: 20px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
color: #0f172a;
overflow-x: auto;
border: 2px solid #e0e7ff;
margin: 16px 20px !important;
text-align: left;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Images inside slide box */
.slide-box-wrapper img {
max-width: 90% !important;
height: auto !important;
border-radius: 20px !important;
box-shadow: 0 8px 24px rgba(37, 99, 235, 0.2) !important;
display: block !important;
margin: 24px auto !important;
}
/* Learning Resources */
.resources-section {
margin-top: 32px;
padding-top: 28px;
border-top: 3px solid var(--border-color);
text-align: left;
}
.resources-section h4 {
color: var(--primary-blue);
font-size: 22px;
margin: 0 0 18px 0 !important;
font-weight: 800;
text-align: center;
}
.resources-section a {
color: var(--accent-teal);
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
display: block;
padding: 10px 15px;
font-size: 16px;
border-radius: 8px;
margin-bottom: 8px;
}
.resources-section a:hover {
color: white;
background: var(--primary-blue);
padding-left: 20px;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
}
/* PROGRESS CONTAINER - MOVED OUTSIDE BOX */
.progress-container {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin: 30px auto;
padding: 20px 0;
max-width: 1000px;
width: 100%;
}
.progress-bar {
flex: 1;
max-width: 700px;
height: 10px;
background: #e0e7ff;
border-radius: 12px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(37, 99, 235, 0.1);
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary-blue) 0%, var(--accent-teal) 100%);
transition: width 0.4s ease;
box-shadow: 0 0 10px rgba(37, 99, 235, 0.4);
}
/* Slide counter badge */
.slide-counter-badge {
background: linear-gradient(110deg, var(--primary-blue) 0%, var(--accent-teal) 100%);
color: white;
padding: 10px 20px;
border-radius: 24px;
font-size: 16px;
font-weight: 800;
min-width: 90px;
text-align: center;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
white-space: nowrap;
}
/* Footer */
.footer-bar {
margin-top: 60px;
text-align: center;
color: var(--text-dark);
font-size: 16px;
letter-spacing: 0.5px;
padding: 32px 0;
border-top: 3px solid var(--border-color);
background: white;
border-radius: 20px;
box-shadow: 0 4px 16px rgba(37, 99, 235, 0.08);
}
.footer-bar p {
margin: 10px 0;
font-weight: 600;
}
.footer-bar p:last-child {
font-size: 14px;
color: var(--text-light);
font-weight: 500;
}
/* Alert messages */
.stSuccess {
border-radius: 14px !important;
border-left: 5px solid #10b981 !important;
background-color: #ecfdf5 !important;
padding: 16px !important;
font-weight: 600 !important;
}
.stInfo {
border-radius: 14px !important;
border-left: 5px solid var(--primary-blue) !important;
background-color: #f0f9ff !important;
padding: 16px !important;
font-weight: 600 !important;
}
.stError {
border-radius: 14px !important;
border-left: 5px solid #ef4444 !important;
background-color: #fef2f2 !important;
padding: 16px !important;
font-weight: 600 !important;
}
/* Mobile responsive */
@media (max-width: 768px) {
.header-container h1 {
font-size: 40px;
}
.slide-box-wrapper {
padding: 30px 20px;
}
.slide-box-wrapper h2 {
font-size: 30px;
}
.slide-box-wrapper p {
font-size: 17px;
line-height: 1.9;
padding: 0 10px;
}
.progress-container {
flex-direction: row;
gap: 15px;
margin: 20px auto;
padding: 15px 0;
}
.progress-bar {
width: 100%;
max-width: none;
}
}
""", unsafe_allow_html=True)
# ============================================================
# SESSION STATE INITIALIZATION
# ============================================================
if "current_slide" not in st.session_state:
st.session_state.current_slide = 0
if "slides_data" not in st.session_state:
st.session_state.slides_data = None
if "search_query" not in st.session_state:
st.session_state.search_query = ""
if "mode" not in st.session_state:
st.session_state.mode = "technical"
if "is_loading" not in st.session_state:
st.session_state.is_loading = False
if "error_message" not in st.session_state:
st.session_state.error_message = None
if "metrics" not in st.session_state:
st.session_state.metrics = None
# ============================================================
# PIPELINE FUNCTION
# ============================================================
def run_pipeline(query, mode):
"""Execute the 5-stage pipeline with metrics tracking."""
try:
metrics = PipelineMetrics(query, mode)
query = validate_and_sanitize_topic(query)
technical_col, operational_col, db = get_collections()
collection = operational_col if mode == "operational" else technical_col
# Cache check
metrics.start_stage("Cache Check")
cached_content, is_cached = check_cache(query, collection)
if is_cached:
metrics.set_cache_hit("mongodb")
metrics.end_stage("Cache Check")
if is_cached:
st.session_state.slides_data = cached_content
st.session_state.current_slide = 0
metrics.end()
metrics.save_metrics()
return True, "✅ Retrieved from cache (instant!)"
st.session_state.is_loading = True
with st.spinner(f"🔄 Generating {mode} content with images (5 stages)..."):
if mode == "technical":
metrics.start_stage("Generate")
generated = generate_technical_content(query)
metrics.end_stage("Generate", f"{len(generated.get('content', []))} slides")
metrics.start_stage("Correct")
corrected = correct_technical_content(generated)
metrics.end_stage("Correct", "Content improved")
metrics.start_stage("Validate URLs")
validated, _ = validate_and_select_urls(corrected)
metrics.end_stage("Validate URLs", f"{len(validated.get('urls', []))} URLs validated")
metrics.start_stage("Refine")
refined = refine_technical_content(validated)
metrics.end_stage("Refine", "Content refined")
metrics.start_stage("Generate Images")
final_result = process_images_for_pipeline(refined, mode="technical")
metrics.end_stage("Generate Images", "Images generated")
else:
metrics.start_stage("Generate")
generated = generate_operational_content(query)
metrics.end_stage("Generate", f"{len(generated.get('content', []))} slides")
metrics.start_stage("Correct")
corrected = correct_operational_content(generated)
metrics.end_stage("Correct", "Content improved")
metrics.start_stage("Validate URLs")
validated, _ = validate_and_select_urls(corrected)
metrics.end_stage("Validate URLs", f"{len(validated.get('urls', []))} URLs validated")
metrics.start_stage("Refine")
refined = refine_operational_content(validated)
metrics.end_stage("Refine", "Content refined")
metrics.start_stage("Generate Images")
final_result = process_images_for_pipeline(refined, mode="operational")
metrics.end_stage("Generate Images", "Images generated")
save_to_cache(query, final_result, collection)
st.session_state.slides_data = final_result
st.session_state.current_slide = 0
st.session_state.is_loading = False
pipeline_metrics = metrics.end()
metrics.save_metrics()
st.session_state.metrics = pipeline_metrics
total_time = pipeline_metrics.get('total_duration_seconds', 0)
return True, f"✅ Generated {len(final_result.get('content', []))} slides in {total_time:.1f}s!"
except Exception as e:
st.session_state.is_loading = False
st.session_state.error_message = str(e)
return False, f"❌ Error: {str(e)}"
# ============================================================
# DISPLAY SLIDE FUNCTION - PROGRESS BAR OUTSIDE BOX
# ============================================================
def display_slide(slide_index):
"""Display current slide with progress bar OUTSIDE the white box."""
if not st.session_state.slides_data:
return
slides = st.session_state.slides_data.get('content', [])
if not slides or slide_index >= len(slides):
return
slide = slides[slide_index]
total_slides = len(slides)
progress_percent = ((slide_index + 1) / total_slides) * 100
# Build the slide box HTML (WITHOUT progress bar)
title = sanitize_for_html(slide.get("slide_title", ""))
raw_content = slide.get("slide_content", "")
# Determine content type
if detect_code_content(raw_content):
sanitized_content = f"
{sanitize_for_html(raw_content)}"
else:
sanitized_content = f"
{sanitize_for_html(raw_content)}
"
# Start building the slide HTML (NO progress bar inside)
slide_html = f"""
{title}
{sanitized_content}
"""
# Add image if available
img_url = slide.get('image_description')
if isinstance(img_url, str) and img_url.startswith('http'):
slide_html += f'

'
# Add learning resources (last slide only)
if slide_index == total_slides - 1:
urls = st.session_state.slides_data.get('urls', [])
if urls:
slide_html += '
📚 Learning Resources
'
for i, url_obj in enumerate(urls, 1):
url_title = sanitize_for_html(url_obj.get('title', 'Documentation'))
url = url_obj.get('url', '#')
slide_html += f'
{i}. {url_title}'
slide_html += '
'
# Close the slide box (NO progress bar)
slide_html += '
'
# Render the slide box
st.markdown(slide_html, unsafe_allow_html=True)
# RENDER PROGRESS BAR OUTSIDE THE BOX
progress_html = f"""
{slide_index + 1} / {total_slides}
"""
st.markdown(progress_html, unsafe_allow_html=True)
# Navigation buttons below
st.markdown('
', unsafe_allow_html=True)
col_left, col_center, col_right = st.columns([1, 8, 1])
with col_left:
if slide_index > 0:
if st.button("⬅", key="prev_btn", help="Previous slide", use_container_width=True):
st.session_state.current_slide -= 1
st.rerun()
with col_right:
if slide_index < total_slides - 1:
if st.button("➡", key="next_btn", help="Next slide", use_container_width=True):
st.session_state.current_slide += 1
st.rerun()
# ============================================================
# PAGE LAYOUT
# ============================================================
# Header
st.markdown(
'',
unsafe_allow_html=True
)
# Search container
st.markdown('
', unsafe_allow_html=True)
col1, col2 = st.columns([3, 1])
with col1:
search_query = st.text_input(
"Search",
value=st.session_state.search_query,
placeholder="e.g., Python, Machine Learning, Cloud Computing...",
key="search_input",
label_visibility="collapsed"
)
st.session_state.search_query = search_query
with col2:
mode = st.radio(
"Mode",
options=["Technical", "Operational"],
index=0 if st.session_state.mode == "technical" else 1,
key="mode_radio",
horizontal=True,
label_visibility="collapsed"
)
st.session_state.mode = mode.lower()
st.markdown('
', unsafe_allow_html=True)
# Generate button
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
search_button = st.button("🔍 Generate Slides", key="search_btn", use_container_width=True)
# Error handling
if st.session_state.error_message:
st.error(st.session_state.error_message)
st.session_state.error_message = None
# Execute pipeline
if search_button and st.session_state.search_query:
success, message = run_pipeline(st.session_state.search_query, st.session_state.mode)
if success:
st.success(message)
else:
st.error(message)
# Display slides
if st.session_state.slides_data:
st.markdown("---")
if st.session_state.current_slide >= len(st.session_state.slides_data.get('content', [])):
st.session_state.current_slide = 0
display_slide(st.session_state.current_slide)
else:
st.info("👆 Enter a topic and click 'Generate Slides' to get started!")
# Footer
st.markdown(
"""""",
unsafe_allow_html=True
)
print("✅ LearnOnTheGo - Progress bar moved outside box - Fixed!")