""" app.py โ€” SPJIMR Strategic ESG Consultant ========================================= Streamlit multi-page application with SPJIMR brand theme. Colours: #F67D31 (orange) ยท #500073 (purple) Navigation: ๐Ÿ“ค Data Ingestion โ†’ upload & process documents ๐Ÿ“Š ESG Dashboard โ†’ automated charts (Energy / Water / Waste) ๐Ÿค– AI Consultant โ†’ RAG-powered Q&A ๐ŸŽจ Creative Studio โ†’ marketing prompt generator ๐Ÿ“ Data Entry โ†’ daily waste entry form โ™ป๏ธ Waste Analytics โ†’ block-wise & multi-month waste dashboard ๐Ÿ† Gamification โ†’ monthly leaderboard & scoring ๐Ÿซ Peer Benchmarkingโ†’ institution comparison Run: streamlit run app.py """ import logging import os import textwrap import re import sys from pathlib import Path from pages.waste_analytics import render_waste_analytics from pages.gamification import render_gamification from pages.data_entry import render_data_entry from pages.peer_benchmarking import render_peer_benchmarking import streamlit as st ROOT = Path(__file__).parent if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) _IS_HF = os.getenv("SPACE_ID") is not None _DATA_ROOT = Path("/tmp") if _IS_HF else Path("data") for _d in [_DATA_ROOT / "uploads", _DATA_ROOT / "faiss_index"]: _d.mkdir(parents=True, exist_ok=True) from core.processor import (DocumentProcessor, extract_waste_series, extract_energy_series, extract_spjimr_metrics_raw) from core.consultant import ESGConsultant logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Page Config # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• st.set_page_config( page_title="SPJIMR ESG Consultant", page_icon="๐ŸŒฟ", layout="wide", initial_sidebar_state="expanded", ) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Brand CSS โ€” SPJIMR Orange #F67D31 ยท Purple #500073 # Applied ONLY when logged in to prevent overriding the login page light theme # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• SPJIMR_CSS = """ """ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Login Page CSS โ€” light theme, injected ONLY on the login page # Resets Streamlit's default dark overrides so the form panel is visible # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• LOGIN_CSS = """ """ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Session-state initialisation # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def _init_state(): defaults = { "logged_in": False, "login_user": "", "hf_token": os.getenv("HF_TOKEN", ""), "consultant": None, "processed_docs": [], "waste_df": None, "waste_full": None, "energy_df": None, "energy_full": None, "water_df": None, } for k, v in defaults.items(): if k not in st.session_state: st.session_state[k] = v _init_state() # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Chart layout shared across all plotly charts # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• _PLOT_LAYOUT = dict( paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", font=dict(color="#C4A4D4", family="DM Sans"), legend=dict(bgcolor="rgba(0,0,0,0)"), xaxis=dict(gridcolor="rgba(255,255,255,0.06)", tickangle=30), yaxis=dict(gridcolor="rgba(255,255,255,0.06)"), margin=dict(l=0, r=0, t=45, b=0), ) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Login Page # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def render_login(): """SPJIMR login โ€” light right panel, dark left brand panel.""" st.markdown(LOGIN_CSS, unsafe_allow_html=True) # โ”€โ”€ Brand panel (pure HTML, fixed left) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("""
Bharatiya Vidya Bhavan's
SPJIMR
Where purpose
meets
management.

S.P. Jain Institute of Management and Research โ€” shaping responsible leaders for a complex world.

62+
Years
10K+
Alumni
#1
Social impact
""", unsafe_allow_html=True) # โ”€โ”€ Form heading โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("""
ESG Sustainability Platform
Welcome back
Sign in with your SPJIMR email to continue.
""", unsafe_allow_html=True) err_slot = st.empty() with st.form("login_form", clear_on_submit=False): email_val = st.text_input( "Email address", placeholder="yourname@spjimr.org", key="login_email_input", ) pass_val = st.text_input( "Password", type="password", placeholder="Enter your password", key="login_pass_input", ) col_r, col_f = st.columns([1, 1]) with col_r: st.checkbox("Remember me", key="login_remember") with col_f: st.markdown( '''
Forgot password?
''', unsafe_allow_html=True, ) submitted = st.form_submit_button("Sign in โ†’") if submitted: _app_password = os.getenv("APP_PASSWORD") if not _app_password: err_slot.error("โš™๏ธ APP_PASSWORD secret is not set. Add it in HuggingFace Space settings.") elif not email_val or not email_val.lower().endswith("@spjimr.org"): err_slot.error("Access restricted to SPJIMR email addresses (@spjimr.org).") elif pass_val != _app_password: err_slot.error("Incorrect password. Please try again.") else: st.session_state["logged_in"] = True st.session_state["login_user"] = email_val.lower() st.rerun() st.markdown("""
or continue with
""", unsafe_allow_html=True) if st.button("๐Ÿ” SPJIMR Single Sign-On (SSO)", use_container_width=True, key="sso_btn"): st.session_state["logged_in"] = True st.session_state["login_user"] = "sso_user@spjimr.org" st.rerun() st.markdown("""
New to the platform? Request access
Having trouble? Contact IT support
""", unsafe_allow_html=True) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Sidebar (logged-in only) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def render_sidebar() -> str: with st.sidebar: # Brand mark st.markdown("""
Bharatiya Vidya Bhavan's
SPJIMR
ESG Sustainability Platform
""", unsafe_allow_html=True) st.markdown("---") # Logged-in user pill user = st.session_state.get("login_user", "") if user: initials = "".join(p[0].upper() for p in user.split("@")[0].split(".")[:2]) or "U" st.markdown(f"""
{initials}
{user.split("@")[0]}
{user.split("@")[1] if "@" in user else ""}
""", unsafe_allow_html=True) st.markdown("### ๐Ÿ—บ Navigate") page = st.radio( "Go to", [ "๐Ÿ“ค Data Ingestion", "๐Ÿ“Š ESG Dashboard", "๐Ÿค– AI Consultant", "๐ŸŽจ Creative Studio", "๐Ÿ“ Data Entry", "โ™ป๏ธ Waste Analytics", "๐Ÿ† Gamification", "๐Ÿซ Peer Benchmarking", ], label_visibility="collapsed", ) st.markdown("---") # HF Token st.markdown("### ๐Ÿ”‘ Hugging Face API") _env_token = os.getenv("HF_TOKEN", "") if _env_token and st.session_state.hf_token == _env_token: st.success("โœ… Token loaded from .env") hf_override = st.text_input("Override token (optional)", value="", type="password", placeholder="Leave blank to use .env") if hf_override.strip(): st.session_state.hf_token = hf_override.strip() st.session_state.consultant = None else: st.warning("โš ๏ธ No HF_TOKEN in .env") hf_input = st.text_input("Hugging Face Token", value=st.session_state.hf_token, type="password", placeholder="hf_...") if hf_input != st.session_state.hf_token: st.session_state.hf_token = hf_input st.session_state.consultant = None st.markdown("---") # RAG status if st.session_state.consultant and st.session_state.consultant.is_ready: vc = st.session_state.consultant.vector_count st.success(f"โœ… RAG Index: {vc:,} vectors") else: st.warning("โš ๏ธ RAG Index: empty") if st.button("๐Ÿ—‘ Reset FAISS Index"): if st.session_state.consultant: st.session_state.consultant.reset_index() for k in ["processed_docs","waste_df","energy_df","water_df","waste_full","energy_full"]: st.session_state[k] = None if k != "processed_docs" else [] st.rerun() st.markdown("---") # Logout if st.button("๐Ÿšช Sign Out", use_container_width=True): st.session_state["logged_in"] = False st.session_state["login_user"] = "" st.rerun() st.markdown("""
Built for SPJIMR ยท Powered by Mistral AI + FAISS
All data stays local
""", unsafe_allow_html=True) return page # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Consultant factory # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def _get_consultant() -> ESGConsultant: if st.session_state.consultant is None or not st.session_state.hf_token: token = st.session_state.hf_token or "NO_TOKEN" st.session_state.consultant = ESGConsultant(hf_token=token) return st.session_state.consultant # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Hero header # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def render_hero(): user = st.session_state.get("login_user", "") greeting = f"Welcome back, {user.split('@')[0].replace('.', ' ').title()}" if user else "Strategic ESG Consultant" st.markdown(f"""
SP Jain Institute of Management & Research

Strategic ESG Consultant

{greeting} ยท Local RAG Pipeline ยท Qwen2.5 + Phi-3.5 ยท FAISS ยท Zero Data Egress

""", unsafe_allow_html=True) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # PAGE: Data Ingestion # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def page_ingestion(): st.markdown("# ๐Ÿ“ค Data Ingestion") st.markdown("Upload campus sustainability files to build the local RAG knowledge base.") processor = DocumentProcessor(upload_dir=str(_DATA_ROOT / "uploads")) col1, col2 = st.columns([3, 2], gap="large") with col1: st.markdown("### Upload Files") uploaded_files = st.file_uploader( "Drag & drop or browse", type=["csv", "xlsx", "xls", "pdf", "docx", "html", "htm"], accept_multiple_files=True, help="Supports: Waste CSV/XLSX, Environmental Metrics XLSX, SPJIMR ESG PDF/DOCX. " "HTML form exports also supported. " "For peer institution reports use the ๐Ÿซ Peer Benchmarking page.", ) st.markdown("### Manual Context / Anomaly Notes") manual_context = st.text_area( "Optional: explain data gaps, anomalies, or additional notes", height=130, placeholder="e.g. 'The June 2024 dry waste spike was due to a campus renovation projectโ€ฆ'", ) reset_index = st.checkbox("Reset existing FAISS index before adding new documents", value=False) vec_count = _get_consultant().vector_count if vec_count >= 8_000: st.warning(f"โš ๏ธ FAISS index has **{vec_count:,} vectors** โ€” tick Reset index to avoid duplicates.") process_btn = st.button("โš™๏ธ Process & Index Documents", use_container_width=True) with col2: st.markdown("### Previously Indexed Files") if st.session_state.processed_docs: for doc in st.session_state.processed_docs: fname = Path(doc["filepath"]).name ext = doc["extension"] icon = {"csv":"๐Ÿ“‹","xlsx":"๐Ÿ“Š","xls":"๐Ÿ“Š","pdf":"๐Ÿ“„","docx":"๐Ÿ“"}.get(ext.lstrip("."), "๐Ÿ“") st.markdown( f'

{icon} {fname}

' f'
{ext.upper()} ยท Indexed โœ“
', unsafe_allow_html=True, ) else: st.info("No files indexed yet.") if process_btn: if not uploaded_files: st.warning("Please upload at least one file.") return consultant = _get_consultant() did_reset = False with st.spinner("Processing documentsโ€ฆ"): for uf in uploaded_files: try: saved_path = processor.save_uploaded_file(uf) result = processor.process(saved_path, manual_context=manual_context) n_chunks = consultant.index_documents(result["text"], reset=reset_index and not did_reset) did_reset = True if result["dataframes"]: spjimr = extract_spjimr_metrics_raw(result["filepath"]) if spjimr.get("waste_series") is not None: st.session_state.waste_df = spjimr["waste_series"] st.session_state.waste_full = spjimr.get("waste") else: wdf = extract_waste_series(result["dataframes"]) if wdf is not None: st.session_state.waste_df = wdf if spjimr.get("energy_series") is not None: st.session_state.energy_df = spjimr["energy_series"] st.session_state.energy_full = spjimr.get("energy") else: edf = extract_energy_series(result["dataframes"]) if edf is not None: st.session_state.energy_df = edf if spjimr.get("water") is not None: st.session_state.water_df = spjimr["water"] st.session_state.processed_docs.append(result) st.success(f"โœ… **{uf.name}** โ€” {n_chunks} chunks indexed") except Exception as exc: st.error(f"โŒ Failed to process **{uf.name}**: {exc}") logger.exception("Processing error for %s", uf.name) st.balloons() st.info(f"๐Ÿง  FAISS index now holds **{consultant.vector_count:,} vectors**.") # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # PAGE: ESG Dashboard # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def page_dashboard(): import plotly.graph_objects as go import pandas as pd import numpy as np st.markdown("# ๐Ÿ“Š ESG Strategic Dashboard") has_waste = st.session_state.get("waste_df") is not None has_energy = st.session_state.get("energy_df") is not None has_water = st.session_state.get("water_df") is not None has_waste_full = st.session_state.get("waste_full") is not None has_energy_full = st.session_state.get("energy_full") is not None k1, k2, k3, k4, k5 = st.columns(5) k1.metric("Documents Indexed", str(len(st.session_state.processed_docs))) k2.metric("RAG Vectors", f"{_get_consultant().vector_count:,}") k3.metric("Energy Data", "โœ…" if has_energy else "โ€”") k4.metric("Water Data", "โœ…" if has_water else "โ€”") k5.metric("Waste Data", "โœ…" if has_waste else "โ€”") def _hline(fig, y=100, text="100% Target"): fig.add_hline(y=y, line_dash="dot", line_color="#F67D31", annotation_text=text, annotation_font=dict(color="#F67D31")) # โ”€โ”€ 1. ENERGY โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("## โšก Energy Consumption") if has_energy_full: edf = st.session_state.energy_full.copy() periods = edf["period"].tolist() ec1, ec2 = st.columns(2, gap="large") with ec1: fig_e = go.Figure() if "solar_kwh" in edf: fig_e.add_trace(go.Bar(x=periods, y=edf["solar_kwh"], name="Solar (kWh)", marker_color="#F67D31")) if "adani_kwh" in edf: fig_e.add_trace(go.Bar(x=periods, y=edf["adani_kwh"], name="Adani Renewable (kWh)", marker_color="#FFA066")) if "nonrenewable_kwh" in edf: fig_e.add_trace(go.Bar(x=periods, y=edf["nonrenewable_kwh"],name="Non-Renewable (kWh)", marker_color="#E74C3C", opacity=0.7)) fig_e.update_layout(**_PLOT_LAYOUT, barmode="stack", height=320, title=dict(text="Energy by Source (kWh)", font=dict(color="#FFA066"))) st.plotly_chart(fig_e, use_container_width=True) with ec2: edf_clean = st.session_state.energy_df.dropna(subset=["renewable_pct"]) latest_pct = float(edf_clean["renewable_pct"].iloc[-1]) if not edf_clean.empty else 0 first_pct = float(edf_clean["renewable_pct"].iloc[0]) if len(edf_clean) > 1 else 0 fig_g = go.Figure(go.Indicator( mode="gauge+number+delta", value=latest_pct, delta={"reference": first_pct, "suffix": "%"}, title={"text": "Renewable Mix %", "font": {"color": "#C4A4D4", "size": 13}}, number={"suffix": "%", "font": {"color": "#FFA066", "size": 36}}, gauge={ "axis": {"range": [0, 100], "tickcolor": "#C4A4D4"}, "bar": {"color": "#F67D31"}, "bgcolor": "rgba(0,0,0,0)", "steps": [ {"range": [0, 33], "color": "rgba(231,76,60,0.12)"}, {"range": [33, 66], "color": "rgba(246,125,49,0.12)"}, {"range": [66,100], "color": "rgba(255,160,102,0.12)"}, ], "threshold": {"line": {"color": "#F67D31", "width": 3}, "value": 100}, }, )) fig_g.update_layout(paper_bgcolor="rgba(0,0,0,0)", font=dict(color="#C4A4D4"), height=280, margin=dict(l=20,r=20,t=20,b=0)) st.plotly_chart(fig_g, use_container_width=True) if latest_pct >= 100: st.success("๐Ÿ† 100% Renewable Energy Achieved!") else: st.info(f"๐ŸŒฑ {100 - latest_pct:.1f}% gap to 100% renewable target") edf_s = st.session_state.energy_df.copy() fig_el = go.Figure() fig_el.add_trace(go.Scatter(x=edf_s["period"], y=edf_s["renewable_pct"], mode="lines+markers", name="Renewable %", line=dict(color="#F67D31", width=2), marker=dict(size=6))) _hline(fig_el) fig_el.update_layout(**_PLOT_LAYOUT, height=260, title=dict(text="Renewable Energy % Over Time", font=dict(color="#FFA066"))) fig_el.update_yaxes(gridcolor="rgba(255,255,255,0.06)", title="Renewable %", range=[0, 115]) st.plotly_chart(fig_el, use_container_width=True) with st.expander("๐Ÿ“‹ Energy Data Table"): st.dataframe(edf, use_container_width=True, hide_index=True) elif has_energy: edf_s = st.session_state.energy_df.copy() fig_el = go.Figure() fig_el.add_trace(go.Scatter(x=edf_s["period"], y=edf_s["renewable_pct"], mode="lines+markers", line=dict(color="#F67D31", width=2))) _hline(fig_el) fig_el.update_layout(**_PLOT_LAYOUT, height=300, title=dict(text="Renewable %", font=dict(color="#FFA066"))) fig_el.update_yaxes(gridcolor="rgba(255,255,255,0.06)", range=[0, 115]) st.plotly_chart(fig_el, use_container_width=True) else: st.info("No energy data detected. Upload your SPJIMR Environmental Metrics XLSX.") # โ”€โ”€ 2. WATER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("## ๐Ÿ’ง Water Consumption") if has_water: wdf = st.session_state.water_df.copy() periods = wdf["period"].tolist() wc1, wc2 = st.columns(2, gap="large") with wc1: fig_w = go.Figure() if "municipal_kl" in wdf: fig_w.add_trace(go.Bar(x=periods, y=wdf["municipal_kl"], name="Municipal Corporation", marker_color="#3498DB")) if "tanker_kl" in wdf: fig_w.add_trace(go.Bar(x=periods, y=wdf["tanker_kl"], name="Tanker", marker_color="#E67E22")) if "rainwater_kl" in wdf: fig_w.add_trace(go.Bar(x=periods, y=wdf["rainwater_kl"], name="Rainwater Harvesting", marker_color="#F67D31")) fig_w.update_layout(**_PLOT_LAYOUT, barmode="stack", height=320, title=dict(text="Water by Source (Kilolitres)", font=dict(color="#FFA066"))) fig_w.update_yaxes(gridcolor="rgba(255,255,255,0.06)", title="kL") st.plotly_chart(fig_w, use_container_width=True) with wc2: src_cols = [c for c in ["municipal_kl","tanker_kl","rainwater_kl"] if c in wdf.columns] src_totals = [wdf[c].sum() for c in src_cols] src_labels = [c.replace("_kl","").replace("_"," ").title() for c in src_cols] fig_wp = go.Figure(go.Pie( labels=src_labels, values=src_totals, hole=0.5, marker=dict(colors=["#3498DB","#E67E22","#F67D31"]), textinfo="label+percent", textfont=dict(size=11), )) fig_wp.update_layout( paper_bgcolor="rgba(0,0,0,0)", font=dict(color="#C4A4D4"), title=dict(text="Source Mix (Total)", font=dict(color="#FFA066")), margin=dict(l=0,r=0,t=45,b=0), height=320, showlegend=False) st.plotly_chart(fig_wp, use_container_width=True) wk1, wk2, wk3 = st.columns(3) total_water = wdf["total_kl"].sum() if "total_kl" in wdf else 0 peak_month = wdf.loc[wdf["total_kl"].idxmax(), "period"] if "total_kl" in wdf else "โ€”" rain_pct = (wdf["rainwater_kl"].sum() / total_water * 100) if ("rainwater_kl" in wdf and total_water > 0) else 0 wk1.metric("Total Consumed", f"{total_water:,.0f} kL") wk2.metric("Peak Month", peak_month) wk3.metric("Rainwater %", f"{rain_pct:.1f}%") with st.expander("๐Ÿ“‹ Water Data Table"): st.dataframe(wdf, use_container_width=True, hide_index=True) else: st.info("No water data detected. Upload your SPJIMR Environmental Metrics XLSX.") # โ”€โ”€ 3. WASTE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("## โ™ป๏ธ Waste Management") if has_waste_full: wst = st.session_state.waste_full.copy() periods = wst["period"].tolist() wst1, wst2 = st.columns(2, gap="large") with wst1: fig_wst = go.Figure() if "recovered_kg" in wst: fig_wst.add_trace(go.Bar(x=periods, y=wst["recovered_kg"], name="Recovered / Recycled (kg)", marker_color="#F67D31")) if "disposed_kg" in wst: fig_wst.add_trace(go.Bar(x=periods, y=wst["disposed_kg"], name="Disposed (kg)", marker_color="#E74C3C", opacity=0.75)) fig_wst.update_layout(**_PLOT_LAYOUT, barmode="group", height=320, title=dict(text="Waste Recovered vs Disposed (kg)", font=dict(color="#FFA066"))) fig_wst.update_yaxes(gridcolor="rgba(255,255,255,0.06)", title="kg") st.plotly_chart(fig_wst, use_container_width=True) with wst2: if "recovered_pct" in wst: fig_rp = go.Figure() fig_rp.add_trace(go.Scatter( x=periods, y=wst["recovered_pct"], mode="lines+markers+text", text=[f"{v:.0f}%" for v in wst["recovered_pct"]], textposition="top center", textfont=dict(size=9, color="#C4A4D4"), line=dict(color="#F67D31", width=2), fill="tozeroy", fillcolor="rgba(246,125,49,0.1)", name="Recovery %", )) fig_rp.add_hline(y=50, line_dash="dot", line_color="#FFA066", annotation_text="50% Target", annotation_font=dict(color="#FFA066")) fig_rp.update_layout(**_PLOT_LAYOUT, height=320, title=dict(text="Waste Recovery Rate (%)", font=dict(color="#FFA066"))) fig_rp.update_yaxes(gridcolor="rgba(255,255,255,0.06)", title="%", range=[0, 110]) st.plotly_chart(fig_rp, use_container_width=True) k1, k2, k3 = st.columns(3) total_wst = wst["total_kg"].sum() if "total_kg" in wst else 0 total_rec = wst["recovered_kg"].sum() if "recovered_kg" in wst else 0 latest_rec_pct = float(wst["recovered_pct"].iloc[-1]) if "recovered_pct" in wst else 0 k1.metric("Total Waste Generated", f"{total_wst:,.0f} kg") k2.metric("Total Waste Recovered", f"{total_rec:,.0f} kg") k3.metric("Latest Recovery Rate", f"{latest_rec_pct:.1f}%", delta=f"{latest_rec_pct - float(wst['recovered_pct'].iloc[0]):.1f}% since start" if "recovered_pct" in wst and len(wst) > 1 else None) with st.expander("๐Ÿ“‹ Waste Data Table"): st.dataframe(wst, use_container_width=True, hide_index=True) elif has_waste: wdf_ = st.session_state.waste_df.copy() fig = go.Figure() for col, color, name in [("wet_kg","#F67D31","Recovered (kg)"),("dry_kg","#E74C3C","Disposed (kg)")]: if col in wdf_.columns: fig.add_trace(go.Bar(x=wdf_["period"], y=wdf_[col], name=name, marker_color=color, opacity=0.85)) fig.update_layout(**_PLOT_LAYOUT, barmode="group", height=300) fig.update_yaxes(gridcolor="rgba(255,255,255,0.06)", title="kg") st.plotly_chart(fig, use_container_width=True) else: st.info("No waste data detected. Upload your SPJIMR Environmental Metrics XLSX.") # โ”€โ”€ 4. SDG Alignment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("## ๐ŸŽฏ SDG Alignment Snapshot") latest_renewable = float(st.session_state.energy_df["renewable_pct"].iloc[-1]) if has_energy else 0 latest_waste_rec = 0 if has_waste_full and "recovered_pct" in st.session_state.waste_full.columns: latest_waste_rec = float(st.session_state.waste_full["recovered_pct"].iloc[-1]) elif has_waste and "wet_kg" in st.session_state.waste_df.columns: wdf_ = st.session_state.waste_df if "dry_kg" in wdf_.columns: denom = wdf_["wet_kg"].iloc[-1] + wdf_["dry_kg"].iloc[-1] latest_waste_rec = float(wdf_["wet_kg"].iloc[-1] / denom * 100) if denom > 0 else 0 sdg_data = { "SDG 4 โ€“ Quality Education": 85, "SDG 6 โ€“ Clean Water & Sanitation": ( min(100, int(100 - (st.session_state.water_df["tanker_kl"].sum() / st.session_state.water_df["total_kl"].sum() * 100))) if has_water else 65 ), "SDG 7 โ€“ Affordable & Clean Energy": min(100, int(latest_renewable)), "SDG 11 โ€“ Sustainable Cities": 70, "SDG 12 โ€“ Responsible Consumption": min(100, int(latest_waste_rec)), "SDG 13 โ€“ Climate Action": 75, } fig_sdg = go.Figure(go.Bar( x=list(sdg_data.values()), y=list(sdg_data.keys()), orientation="h", marker=dict(color=list(sdg_data.values()), colorscale=[[0,"#E74C3C"],[0.5,"#F67D31"],[1,"#FFA066"]], showscale=False), text=[f"{v}%" for v in sdg_data.values()], textposition="auto", )) fig_sdg.update_layout( paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", font=dict(color="#C4A4D4", family="DM Sans", size=11), xaxis=dict(range=[0,110], gridcolor="rgba(255,255,255,0.06)", title="Progress %"), yaxis=dict(gridcolor="rgba(255,255,255,0.06)"), margin=dict(l=0, r=0, t=10, b=0), height=300, ) st.plotly_chart(fig_sdg, use_container_width=True) st.caption("SDG 6, 7, 12 auto-populated from uploaded data. SDG 4, 11, 13 are baseline estimates.") # โ”€โ”€ 5. Predictive Analytics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("## ๐Ÿ”ฎ Predictive Analytics โ€” Forecast") def _poly_forecast(series, periods): y = __import__("pandas").to_numeric(__import__("pandas").Series(series), errors="coerce").dropna().values.astype(float) if len(y) < 3: return None, None, None x = np.arange(len(y), dtype=float) xf = np.arange(len(y), len(y) + periods, dtype=float) c = np.polyfit(x, y, min(2, len(y)-1)) err = np.std(y - np.polyval(c, x)) * 1.96 fc = np.polyval(c, xf) return fc, fc - err, fc + err horizon = st.slider("Forecast horizon (months)", 3, 12, 6, key="fc_horizon") fl = [f"M+{i+1}" for i in range(horizon)] has_fc = False def _add_fc(fig, hist_x, hist_y, future_x, color, name): r, g, b = int(color[1:3],16), int(color[3:5],16), int(color[5:7],16) fc, lo, hi = _poly_forecast(hist_y, len(future_x)) if fc is None: return fig.add_trace(go.Scatter(x=future_x, y=np.maximum(fc, 0), name=f"{name} (Forecast)", mode="lines+markers", line=dict(color=color, width=2, dash="dash"), marker=dict(size=7, symbol="diamond"))) fig.add_trace(go.Scatter( x=future_x + future_x[::-1], y=list(np.maximum(hi,0)) + list(np.maximum(lo,0))[::-1], fill="toself", fillcolor=f"rgba({r},{g},{b},0.1)", line=dict(color="rgba(0,0,0,0)"), showlegend=False)) if has_energy: has_fc = True edf_f = st.session_state.energy_df.dropna(subset=["renewable_pct"]).copy() fig_ef = go.Figure() fig_ef.add_trace(go.Scatter(x=list(edf_f["period"]), y=edf_f["renewable_pct"], name="Renewable % (Actual)", mode="lines+markers", line=dict(color="#F67D31", width=2), marker=dict(size=5))) _add_fc(fig_ef, list(edf_f["period"]), edf_f["renewable_pct"].values, fl, "#F67D31", "Renewable %") _hline(fig_ef) fig_ef.update_layout(**_PLOT_LAYOUT, height=320, title=dict(text=f"Renewable Energy % Forecast (next {horizon} months)", font=dict(color="#FFA066"))) fig_ef.update_yaxes(gridcolor="rgba(255,255,255,0.06)", title="%", range=[0, 115]) st.plotly_chart(fig_ef, use_container_width=True) if has_waste_full: has_fc = True wst_f = st.session_state.waste_full.copy() fig_wf = go.Figure() for col, color, name in [("recovered_kg","#F67D31","Recovered"),("disposed_kg","#E74C3C","Disposed")]: if col in wst_f.columns: fig_wf.add_trace(go.Scatter(x=list(wst_f["period"]), y=wst_f[col], name=f"{name} (Actual)", mode="lines+markers", line=dict(color=color, width=2), marker=dict(size=5))) _add_fc(fig_wf, list(wst_f["period"]), wst_f[col].values, fl, color, name) fig_wf.update_layout(**_PLOT_LAYOUT, height=320, title=dict(text=f"Waste Forecast (next {horizon} months)", font=dict(color="#FFA066"))) fig_wf.update_yaxes(gridcolor="rgba(255,255,255,0.06)", title="kg") st.plotly_chart(fig_wf, use_container_width=True) if has_water: has_fc = True wtr_f = st.session_state.water_df.copy() if "total_kl" in wtr_f.columns: fig_wtr = go.Figure() fig_wtr.add_trace(go.Scatter(x=list(wtr_f["period"]), y=wtr_f["total_kl"], name="Total Water (Actual)", mode="lines+markers", line=dict(color="#3498DB", width=2), marker=dict(size=5))) _add_fc(fig_wtr, list(wtr_f["period"]), wtr_f["total_kl"].values, fl, "#3498DB", "Total Water") fig_wtr.update_layout(**_PLOT_LAYOUT, height=300, title=dict(text=f"Water Consumption Forecast (next {horizon} months)", font=dict(color="#FFA066"))) fig_wtr.update_yaxes(gridcolor="rgba(255,255,255,0.06)", title="kL") st.plotly_chart(fig_wtr, use_container_width=True) if not has_fc: st.info("Upload the SPJIMR Environmental Metrics XLSX to enable forecasts.") st.caption("**Methodology:** Polynomial regression (degree โ‰ค 2) ยท 95% CI = ยฑ1.96ฯƒ of historical residuals.") # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # PAGE: AI Consultant # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def page_consultant(): st.markdown("# ๐Ÿค– AI ESG Consultant") st.markdown("Ask strategic sustainability questions. The AI retrieves relevant data from your uploaded documents and synthesises expert insights.") if not st.session_state.hf_token: st.error("๐Ÿ”‘ Please enter your Hugging Face API token in the sidebar.") return consultant = _get_consultant() if not consultant.is_ready: st.warning("โš ๏ธ Knowledge base is empty. Head to **๐Ÿ“ค Data Ingestion** to upload documents first.") return preset_questions = [ "Custom questionโ€ฆ", "What are the top 3 waste reduction opportunities for SPJIMR campus?", "How does our renewable energy progress compare to peer institutions?", "Which SDGs are we best and worst aligned with, and why?", "What initiatives should we launch to achieve net-zero by 2030?", "Summarise our ESG performance highlights for an annual report.", "What are the key risks and gaps in our current sustainability strategy?", ] col1, col2 = st.columns([2, 1], gap="large") with col1: selected = st.selectbox("๐Ÿ’ก Quick Insights", preset_questions) question = st.text_area("Your Strategic Question", value="" if selected == "Custom questionโ€ฆ" else selected, height=110, placeholder="e.g. What should be our top ESG priority for the next academic year?") with col2: st.markdown("### โš™๏ธ Query Settings") top_k = st.slider("Chunks to retrieve (top-k)", 2, 10, 5) max_tokens = st.slider("Max response tokens", 256, 2048, 1024, step=128) temperature = st.slider("Creativity (temperature)", 0.1, 1.0, 0.4, step=0.05) if st.button("๐Ÿ” Get Strategic Insight", use_container_width=True): if not question.strip(): st.warning("Please enter a question.") return with st.spinner("๐Ÿง  Consulting AI โ€” retrieving context and generating insightsโ€ฆ"): response = consultant.query(question, top_k=top_k, max_tokens=max_tokens, temperature=temperature) st.markdown("---") st.markdown("## ๐Ÿ“‹ Strategic Analysis") st.markdown(f'
{response["answer"]}
', unsafe_allow_html=True) st.markdown("---") with st.expander(f"๐Ÿ“š Retrieved Context Chunks ({response['chunks_used']} used)"): for i, chunk in enumerate(response["sources"], 1): st.markdown(f"**Chunk {i}:**") st.text(chunk) st.divider() st.markdown("---") st.caption("๐Ÿ’ก **Tip:** Upload multiple document types for richer, cross-referenced insights.") # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # PAGE: Creative Studio # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def page_creative_studio(): st.markdown("# ๐ŸŽจ Marketing Creative Studio") st.markdown("Multi-modal ESG content generation โ€” posters, videos, social copy, and audio narration.") if not st.session_state.hf_token: st.error("๐Ÿ”‘ Please enter your Hugging Face API token in the sidebar.") return consultant = _get_consultant() # Model badge strip badges = [ ("#F67D31", "โœ๏ธ Creative Text โ†’ Phi-3.5-mini"), ("#500073", "๐Ÿ–ผ Image/Poster โ†’ FLUX.1-Schnell"), ("#E74C3C", "๐ŸŽฌ Video โ†’ ModelScope text-to-video"), ("#3498DB", "๐Ÿ”Š Audio โ†’ SpeechT5 (local) + HF API fallback"), ] badge_html = '
' for color, label in badges: badge_html += ( f'
{label}
' ) badge_html += '
' st.markdown(badge_html, unsafe_allow_html=True) tab_poster, tab_video, tab_social, tab_audio = st.tabs( ["๐Ÿ–ผ Poster / Image", "๐ŸŽฌ Video Brief", "๐Ÿ“ฑ Social Media", "๐Ÿ”Š Audio Narration"] ) with tab_poster: st.markdown("### ๐Ÿ–ผ AI Poster Generator") st.caption("Phi-3.5 writes the optimised prompt โ†’ FLUX.1-Schnell generates the actual image") col_a, col_b = st.columns([3, 1], gap="large") with col_a: poster_brief = st.text_area("Poster Brief", value="Create a sustainability poster celebrating SPJIMR's zero-waste campus achievement.", height=100, key="brief_poster") with col_b: p_style = st.selectbox("Visual Style", ["Photorealistic","Cinematic","Minimalist","Bold Graphic","Watercolour"], key="p_style") p_size = st.selectbox("Aspect Ratio", ["1024ร—1024 (Square)","1024ร—576 (Landscape)","576ร—1024 (Portrait)"], key="p_size") p_mood = st.selectbox("Mood", ["Optimistic & Bright","Serene & Natural","Bold & Impactful","Warm & Human"], key="p_mood") size_map = {"1024ร—1024 (Square)":(1024,1024),"1024ร—576 (Landscape)":(1024,576),"576ร—1024 (Portrait)":(576,1024)} if st.button("โœ๏ธ Step 1 โ€” Generate Optimised Prompt", key="gen_poster_prompt", use_container_width=True): if poster_brief.strip(): enriched = f"{poster_brief}\nStyle: {p_style} | Mood: {p_mood}\nFor SPJIMR Mumbai sustainability campaign." with st.spinner("โœ๏ธ Phi-3.5 crafting the perfect image promptโ€ฆ"): result = consultant.creative_text(enriched, mode="poster", top_k=4) st.session_state["poster_prompt_text"] = result["answer"] st.success("โœ… Prompt ready โ€” review and edit below, then generate the image.") if "poster_prompt_text" in st.session_state: edited_prompt = st.text_area("๐Ÿ“ Optimised Prompt (edit before generating)", value=st.session_state["poster_prompt_text"], height=150, key="edited_poster_prompt") st.code(edited_prompt, language=None) if st.button("๐Ÿ–ผ Step 2 โ€” Generate Image (FLUX.1-Schnell)", key="gen_img", use_container_width=True): w, h = size_map[p_size] with st.spinner("๐ŸŽจ FLUX.1-Schnell generating your posterโ€ฆ (15โ€“30s)"): img_bytes = consultant.create_image(edited_prompt, width=w, height=h) if img_bytes: st.image(img_bytes, caption="Generated by FLUX.1-Schnell ยท SPJIMR ESG Campaign") st.download_button("โฌ‡๏ธ Download Poster (PNG)", data=img_bytes, file_name="spjimr_esg_poster.png", mime="image/png", use_container_width=True) else: st.error("โŒ Image generation failed. Try copying the prompt into Midjourney or DALL-E 3.") with tab_video: st.markdown("### ๐ŸŽฌ AI Video Generator") col_a, col_b = st.columns([3, 1], gap="large") with col_a: video_brief = st.text_area("Video Idea", value="A cinematic clip of SPJIMR campus โ€” solar panels, composting stations, students and sustainability.", height=110, key="brief_video") with col_b: v_style = st.selectbox("Style", ["Documentary","Cinematic","Social Reel","Timelapse"], key="v_style") v_tone = st.selectbox("Tone", ["Inspirational","Factual","Emotional","Energetic"], key="v_tone") v_frames = st.select_slider("Frames", options=[8, 16, 24], value=16, key="v_frames") if st.button("๐ŸŽฌ Write Brief & Generate Video", key="gen_video_full", use_container_width=True): if video_brief.strip(): enriched = f"{video_brief}\nStyle: {v_style} | Tone: {v_tone}\nFor SPJIMR sustainability campaign." with st.spinner("โœ๏ธ Step 1/3 โ€” Phi-3.5 writing video briefโ€ฆ"): result = consultant.creative_text(enriched, mode="video", top_k=4) full_brief = result["answer"] st.markdown(f'
{full_brief}
', unsafe_allow_html=True) with st.spinner("๐Ÿ” Step 2/3 โ€” Condensing briefโ€ฆ"): condensed = consultant._condense_for_video(full_brief) st.info(f"๐ŸŽฏ **Video model prompt:** {condensed}") prog = st.progress(0, text="โณ Step 3/3 โ€” Connecting to video modelโ€ฆ") status = st.empty() def _upd(msg): status.info(f"๐ŸŽฌ {msg}") if "Attempt 1" in msg: prog.progress(20) elif "Attempt 2" in msg: prog.progress(45) elif "Attempt 3" in msg: prog.progress(65) video_bytes = consultant.create_video(condensed, num_frames=v_frames, status_cb=_upd) prog.progress(100) if video_bytes: status.success(f"โœ… Done! ({len(video_bytes):,} bytes)") import tempfile, os as _os tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") tmp.write(video_bytes); tmp.close() st.video(tmp.name) st.download_button("โฌ‡๏ธ Download Video (MP4)", data=video_bytes, file_name="spjimr_esg_video.mp4", mime="video/mp4", use_container_width=True) _os.unlink(tmp.name) else: status.error("โŒ HF free-tier video generation unavailable.") st.code(condensed, language=None) with tab_social: st.markdown("### ๐Ÿ“ฑ Social Media Content Generator") col_a, col_b = st.columns([3, 1], gap="large") with col_a: social_brief = st.text_area("Social Brief", value="Celebrate SPJIMR reaching 70% renewable energy โ€” inspire peer institutions.", height=100, key="brief_social") with col_b: s_platform = st.selectbox("Platform", ["LinkedIn","Instagram","Twitter/X","All Platforms"], key="s_platform") s_format = st.selectbox("Format", ["Static Post","Carousel","Reel/Short Video","Story"], key="s_format") if st.button("๐Ÿ“ฑ Generate Social Content", key="gen_social", use_container_width=True): if social_brief.strip(): enriched = f"{social_brief}\nPlatform: {s_platform} | Format: {s_format}" with st.spinner("๐Ÿ“ฑ Phi-3.5 writing your social contentโ€ฆ"): result = consultant.creative_text(enriched, mode="social", top_k=4) st.markdown(f'
{result["answer"]}
', unsafe_allow_html=True) st.code(result["answer"], language=None) with tab_audio: st.markdown("### ๐Ÿ”Š Audio Narration Generator") col_a, col_b = st.columns([3, 1], gap="large") with col_a: audio_brief = st.text_area("Narration Brief", value="A 60-second podcast intro about SPJIMR's ESG progress and sustainability milestones.", height=100, key="brief_audio") with col_b: a_tone = st.selectbox("Script Tone", ["Authoritative & Warm","Energetic & Inspiring","Calm & Reflective","Conversational"], key="a_tone") a_dur = st.selectbox("Duration", ["30 seconds (~75 words)","60 seconds (~150 words)","90 seconds (~225 words)"], key="a_dur") a_mode = st.radio("Audio Mode", ["๐ŸŽ™๏ธ Single Speaker","๐ŸŽ™๏ธ๐ŸŽ™๏ธ Podcast (HOST / GUEST)"], key="a_mode") if st.button("โœ๏ธ Step 1 โ€” Write Narration Script", key="gen_script", use_container_width=True): if audio_brief.strip(): podcast_hint = ("\nWrite as podcast dialogue. [HOST] and [GUEST] tags per line." if "Podcast" in a_mode else "") enriched = f"{audio_brief}\nTone: {a_tone} | Duration: {a_dur}{podcast_hint}" with st.spinner("โœ๏ธ Phi-3.5 writing your narration scriptโ€ฆ"): result = consultant.creative_text(enriched, mode="audio_script", top_k=4) st.session_state["audio_script_text"] = result["answer"] st.success("โœ… Script ready.") if "audio_script_text" in st.session_state: edited_script = st.text_area("๐Ÿ“ Narration Script", value=st.session_state["audio_script_text"], height=180, key="edited_script") if st.button("๐Ÿ”Š Step 2 โ€” Generate Audio", key="gen_audio", use_container_width=True): with st.spinner("๐Ÿ”Š Generating audio narrationโ€ฆ"): try: audio_bytes = (consultant.create_podcast_audio(edited_script) if "Podcast" in a_mode else consultant.create_audio(edited_script)) st.audio(audio_bytes, format="audio/wav") st.download_button("โฌ‡๏ธ Download Narration (WAV)", data=audio_bytes, file_name="spjimr_esg_narration.wav", mime="audio/wav", use_container_width=True) except RuntimeError as e: st.error(f"โŒ Audio generation failed: {e}") st.markdown("---") st.markdown("### ๐Ÿ›  Model Summary") model_data = { "โœ๏ธ Creative Writing": ("Phi-3.5-mini", "Microsoft", "Chat / Creative"), "๐Ÿ–ผ Image Generation": ("FLUX.1-Schnell", "Black Forest Labs", "Textโ†’Image"), "๐ŸŽฌ Video Generation": ("text-to-video", "ModelScope / DAMO", "Textโ†’Video"), "๐Ÿ”Š Audio / TTS": ("SpeechT5 + HiFi-GAN","Microsoft", "Textโ†’Speech"), "๐Ÿง  Strategy / RAG": ("Qwen2.5-7B", "Alibaba / Qwen", "Chat / Reasoning"), } cols = st.columns(5) for col, (task, (model, org, task_type)) in zip(cols, model_data.items()): col.markdown( f'

{task}

' f'
{model}
' f'
{org} ยท {task_type}
', unsafe_allow_html=True, ) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Router โ€” CSS injected conditionally based on login state # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• if not st.session_state["logged_in"]: # Light theme for login page โ€” do NOT inject SPJIMR_CSS here render_login() else: # Dark theme for the main app โ€” inject SPJIMR_CSS only when logged in st.markdown(SPJIMR_CSS, unsafe_allow_html=True) page = render_sidebar() render_hero() if page == "๐Ÿ“ค Data Ingestion": page_ingestion() elif page == "๐Ÿ“Š ESG Dashboard": page_dashboard() elif page == "๐Ÿค– AI Consultant": page_consultant() elif page == "๐ŸŽจ Creative Studio": page_creative_studio() elif page == "๐Ÿ“ Data Entry": render_data_entry() elif page == "โ™ป๏ธ Waste Analytics": render_waste_analytics() elif page == "๐Ÿ† Gamification": render_gamification() elif page == "๐Ÿซ Peer Benchmarking": render_peer_benchmarking()