WISE_Energy / src /app.py
ahanbose's picture
Update src/app.py (#1)
ef4c850
"""
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 = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600&family=DM+Sans:wght@300;400;500&display=swap');
/* ── Root Variables ── */
:root {
--orange: #F67D31;
--orange-light: #FFA066;
--orange-pale: rgba(246,125,49,0.12);
--purple: #500073;
--purple-deep: #1C0029;
--purple-mid: #350050;
--purple-light: #6b009a;
--cream: #FDF5FF;
--muted: #C4A4D4;
--glass-bg: rgba(255,255,255,0.04);
--border-o: rgba(246,125,49,0.22);
--border-p: rgba(80,0,115,0.35);
}
/* ── Global (dark theme — logged-in only) ── */
html, body, [class*="css"] {
font-family: 'DM Sans', sans-serif !important;
background-color: var(--purple-deep) !important;
color: var(--cream) !important;
}
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--purple-deep); }
::-webkit-scrollbar-thumb { background: var(--purple-mid); border-radius: 3px; }
/* ── Sidebar ── */
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #110018 0%, #1C0029 100%) !important;
border-right: 1px solid var(--border-p) !important;
}
[data-testid="stSidebar"] .stSelectbox label,
[data-testid="stSidebar"] .stTextInput label,
[data-testid="stSidebar"] p {
color: var(--muted) !important;
font-size: 0.78rem;
}
[data-testid="stSidebar"] .stRadio label { color: var(--cream) !important; font-size: 0.85rem; }
[data-testid="stSidebar"] .stRadio [data-testid="stMarkdownContainer"] p { color: var(--cream) !important; }
/* ── Headings ── */
h1 { font-family: 'Cormorant Garamond', serif !important; font-weight: 600 !important;
color: var(--orange) !important; letter-spacing: -0.3px; }
h2 { font-weight: 600 !important; color: var(--orange-light) !important; }
h3 { color: var(--muted) !important; font-size: 0.88rem !important;
text-transform: uppercase; letter-spacing: 1px; }
/* ── Cards ── */
.esg-card {
background: var(--glass-bg);
border: 1px solid var(--border-o);
border-radius: 12px;
padding: 1.4rem 1.6rem;
margin-bottom: 1rem;
}
.esg-card h4 { color: var(--orange); font-size: 0.78rem; text-transform: uppercase; margin: 0 0 0.3rem; }
.esg-card .big-num { font-size: 2.2rem; font-weight: 700; color: var(--orange-light); line-height: 1; }
.esg-card .sub { color: var(--muted); font-size: 0.75rem; margin-top: 0.2rem; }
/* ── Answer / AI response box ── */
.answer-box {
background: rgba(53,0,80,0.5);
border-left: 4px solid var(--orange);
border-radius: 0 12px 12px 0;
padding: 1.2rem 1.4rem;
font-size: 0.95rem;
line-height: 1.75;
white-space: pre-wrap;
color: var(--cream);
}
/* ── Creative prompt cards ── */
.prompt-card {
background: rgba(246,125,49,0.06);
border: 1px solid rgba(246,125,49,0.28);
border-radius: 12px;
padding: 1.2rem 1.4rem;
margin-bottom: 0.8rem;
font-size: 0.82rem;
line-height: 1.7;
color: var(--cream);
}
/* ── Buttons ── */
.stButton > button {
background: linear-gradient(135deg, var(--purple-mid), var(--purple-deep)) !important;
color: var(--orange) !important;
border: 1px solid var(--orange) !important;
border-radius: 8px !important;
font-size: 0.82rem !important;
letter-spacing: 0.5px;
padding: 0.55rem 1.4rem !important;
transition: all 0.2s ease;
font-family: 'DM Sans', sans-serif !important;
}
.stButton > button:hover {
background: var(--purple-mid) !important;
color: white !important;
border-color: var(--orange-light) !important;
box-shadow: 0 0 14px rgba(246,125,49,0.25) !important;
}
/* Primary button */
.stButton > button[kind="primary"] {
background: var(--purple) !important;
color: white !important;
border-color: var(--orange) !important;
}
.stButton > button[kind="primary"]:hover {
background: var(--purple-light) !important;
box-shadow: 0 0 14px rgba(246,125,49,0.3) !important;
}
/* ── Inputs ── */
.stTextArea textarea, .stTextInput input {
background: rgba(255,255,255,0.04) !important;
border: 1px solid var(--border-p) !important;
color: var(--cream) !important;
border-radius: 8px !important;
font-family: 'DM Sans', sans-serif !important;
}
.stTextArea textarea:focus, .stTextInput input:focus {
border-color: var(--orange) !important;
box-shadow: 0 0 0 3px rgba(246,125,49,0.12) !important;
}
.stSelectbox > div > div {
background: rgba(255,255,255,0.04) !important;
border: 1px solid var(--border-p) !important;
color: var(--cream) !important;
}
/* ── File uploader ── */
[data-testid="stFileUploader"] {
background: rgba(255,255,255,0.03);
border: 2px dashed var(--border-o) !important;
border-radius: 12px;
}
/* ── Metric tiles ── */
[data-testid="stMetric"] {
background: var(--glass-bg) !important;
border: 1px solid var(--border-o) !important;
border-radius: 10px;
padding: 0.8rem 1rem;
}
[data-testid="stMetricLabel"] { color: var(--muted) !important; font-size: 0.75rem; }
[data-testid="stMetricValue"] { color: var(--orange-light) !important; }
[data-testid="stMetricDelta"] { color: #a8e6c0 !important; }
/* ── Divider ── */
hr { border-color: var(--border-p) !important; }
/* ── Expander ── */
.stExpander { border: 1px solid var(--border-p) !important; border-radius: 8px !important; }
.stExpander > summary { color: var(--muted) !important; font-size: 0.78rem; }
/* ── Tabs ── */
[data-testid="stTabs"] button { font-size: 0.78rem !important; color: var(--muted) !important; }
[data-testid="stTabs"] [aria-selected="true"] {
color: var(--orange) !important;
border-bottom-color: var(--orange) !important;
}
/* ── Progress bar ── */
.stProgress > div > div { background: var(--orange) !important; }
/* ── Alerts ── */
.stAlert { border-radius: 8px !important; }
/* ── Slider ── */
[data-testid="stSlider"] [data-baseweb="slider"] [data-testid="stThumbValue"] {
color: var(--orange) !important;
}
</style>
"""
# ═══════════════════════════════════════════════════════════════════════════════
# 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 = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300&family=DM+Sans:wght@300;400;500&display=swap');
/* === 1. Full dark background everywhere === */
html, body,
section[data-testid="stAppViewContainer"],
div[data-testid="stAppViewBlockWrapper"],
div.appview-container,
div.main,
div.block-container,
div[class*="block-container"],
div[class*="stApp"],
div[class*="css"],
div[data-testid="stVerticalBlock"],
div[data-testid="stVerticalBlockBorderWrapper"],
div[data-testid="stForm"],
div[data-testid="column"],
div[data-testid="stHorizontalBlock"] {
background-color: #0D0014 !important;
background-image: none !important;
font-family: 'DM Sans', sans-serif !important;
}
/* === 2. Hide Streamlit chrome === */
[data-testid="stSidebar"],
[data-testid="stSidebarNav"],
[data-testid="collapsedControl"],
.stDeployButton,
#MainMenu, footer, header,
div[data-testid="stDecoration"],
div[data-testid="stStatusWidget"],
div[data-testid="stToolbar"] {
display: none !important;
visibility: hidden !important;
}
/* === 3. Block container: right half === */
div.block-container,
div[class*="block-container"] {
margin-left: 48% !important;
margin-right: 0 !important;
max-width: 480px !important;
padding: 0 2.5rem !important;
min-height: 100vh !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
box-sizing: border-box !important;
border-left: 1px solid rgba(246,125,49,0.15) !important;
}
/* === 4. Brand panel: fixed left === */
.spjimr-brand-panel {
position: fixed !important;
top: 0 !important; left: 0 !important;
width: 48vw !important;
height: 100vh !important;
background: linear-gradient(160deg, #1C0029 0%, #0D0014 55%, #150008 100%) !important;
border-right: 1px solid rgba(246,125,49,0.15) !important;
display: flex !important;
flex-direction: column !important;
justify-content: space-between !important;
padding: clamp(2rem,3.5vw,3.5rem) clamp(1.5rem,2.5vw,3rem) !important;
overflow: hidden !important;
z-index: 9999 !important;
box-sizing: border-box !important;
}
/* === 5. All text: light on dark === */
p, span, div, label { color: #e8d5f5 !important; }
a { color: #F67D31 !important; font-weight: 500 !important; text-decoration: none !important; }
a:hover { color: #FFA066 !important; }
/* === 6. Input labels === */
.stTextInput > label,
.stTextInput > label p,
.stTextInput label span {
font-size: 10px !important;
font-weight: 600 !important;
letter-spacing: 0.1em !important;
text-transform: uppercase !important;
color: #c4a4d4 !important;
display: block !important;
margin-bottom: 6px !important;
}
/* === 7. Input fields === */
.stTextInput > div > div > input {
height: 50px !important;
border-radius: 10px !important;
border: 1px solid rgba(196,164,212,0.25) !important;
background: rgba(255,255,255,0.05) !important;
color: #fdf5ff !important;
font-family: 'DM Sans', sans-serif !important;
font-size: 14px !important;
padding: 0 14px !important;
width: 100% !important;
box-sizing: border-box !important;
transition: border-color 0.2s, box-shadow 0.2s !important;
}
.stTextInput > div > div > input:focus {
border-color: #F67D31 !important;
box-shadow: 0 0 0 3px rgba(246,125,49,0.18) !important;
outline: none !important;
background: rgba(255,255,255,0.07) !important;
}
.stTextInput > div > div > input::placeholder { color: rgba(196,164,212,0.5) !important; }
/* Password toggle icon */
.stTextInput button { background: transparent !important; border: none !important; color: #c4a4d4 !important; }
/* === 8. Checkbox === */
.stCheckbox label p {
color: #c4a4d4 !important;
font-size: 13px !important;
text-transform: none !important;
letter-spacing: 0 !important;
}
.stCheckbox input[type="checkbox"] { accent-color: #F67D31 !important; }
/* Dark checkbox background */
.stCheckbox > label { background: transparent !important; }
/* === 9. Sign In button — full width orange/purple gradient === */
div[data-testid="stFormSubmitButton"] > button {
width: 100% !important;
height: 52px !important;
background: linear-gradient(135deg, #F67D31 0%, #c45a00 100%) !important;
color: #ffffff !important;
border: none !important;
border-radius: 10px !important;
font-family: 'DM Sans', sans-serif !important;
font-size: 15px !important;
font-weight: 500 !important;
letter-spacing: 0.04em !important;
margin-top: 0.5rem !important;
box-shadow: 0 4px 20px rgba(246,125,49,0.35) !important;
cursor: pointer !important;
transition: all 0.2s !important;
}
div[data-testid="stFormSubmitButton"] > button:hover {
background: linear-gradient(135deg, #FFA066 0%, #F67D31 100%) !important;
box-shadow: 0 6px 24px rgba(246,125,49,0.5) !important;
transform: translateY(-1px) !important;
}
/* === 10. SSO button === */
.stButton > button {
width: 100% !important;
height: 50px !important;
background: rgba(255,255,255,0.05) !important;
color: #fdf5ff !important;
border: 1px solid rgba(196,164,212,0.25) !important;
border-radius: 10px !important;
font-family: 'DM Sans', sans-serif !important;
font-size: 14px !important;
font-weight: 400 !important;
transition: all 0.2s !important;
}
.stButton > button:hover {
border-color: #F67D31 !important;
background: rgba(246,125,49,0.08) !important;
color: #fdf5ff !important;
box-shadow: 0 0 14px rgba(246,125,49,0.2) !important;
}
/* === 11. Divider line text === */
hr { border-color: rgba(196,164,212,0.15) !important; }
.stAlert { border-radius: 10px !important; }
/* === 12. Mobile === */
@media (max-width: 800px) {
.spjimr-brand-panel {
position: relative !important;
width: 100% !important; height: auto !important;
flex-direction: row !important; align-items: center !important;
padding: 1.2rem 1.5rem !important;
border-right: none !important;
border-bottom: 1px solid rgba(246,125,49,0.2) !important;
z-index: 100 !important;
}
div.block-container, div[class*="block-container"] {
margin-left: 0 !important;
width: 100% !important;
max-width: 100% !important;
padding: 2rem 1.5rem !important;
min-height: auto !important;
border-left: none !important;
}
}
</style>
"""
# ═══════════════════════════════════════════════════════════════════════════════
# 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("""
<div class="spjimr-brand-panel">
<svg style="position:absolute;bottom:-40px;left:-10px;width:320px;height:500px;opacity:0.07;pointer-events:none;" viewBox="0 0 320 500" fill="none">
<path d="M10 500 Q50 290 90 70" stroke="#F67D31" stroke-width="28" stroke-linecap="round"/>
<path d="M65 500 Q115 290 155 50" stroke="#F67D31" stroke-width="22" stroke-linecap="round"/>
<path d="M125 500 Q185 300 225 40" stroke="#F67D31" stroke-width="16" stroke-linecap="round"/>
<path d="M190 500 Q248 320 292 70" stroke="#F67D31" stroke-width="10" stroke-linecap="round"/>
</svg>
<div style="position:absolute;bottom:0;left:0;width:400px;height:400px;background:radial-gradient(ellipse at 20% 100%,rgba(246,125,49,0.13) 0%,transparent 60%);pointer-events:none;"></div>
<div style="position:relative;z-index:2;">
<div style="display:flex;align-items:center;gap:14px;">
<svg width="40" height="48" viewBox="0 0 46 54" fill="none">
<path d="M5 54 Q10 35 18 15 Q22 4 28 0 Q23 15 30 23 Q32 11 40 3 Q38 19 42 27 Q46 36 42 46 Q38 54 30 54" fill="none" stroke="#F67D31" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 54 Q14 37 22 22 Q26 11 32 5 Q28 19 34 27 Q36 17 42 9" fill="none" stroke="#F67D31" stroke-width="2.5" stroke-linecap="round" opacity="0.55"/>
<path d="M17 54 Q22 38 28 26 Q30 20 36 14" fill="none" stroke="#F67D31" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
</svg>
<div>
<div style="font-size:11px;font-weight:300;letter-spacing:0.05em;color:rgba(255,255,255,0.55);">Bharatiya Vidya Bhavan's</div>
<div style="font-size:28px;font-weight:500;line-height:1;margin-top:2px;"><span style="color:#F67D31;">SP</span><span style="color:#fff;">JIMR</span></div>
</div>
</div>
<div class="brand-tagline" style="margin-top:2.8rem;">
<div style="font-family:'Cormorant Garamond',serif;font-size:clamp(1.8rem,2.8vw,3rem);font-weight:300;line-height:1.1;color:#fff;margin-bottom:1.2rem;">
Where <em style="font-style:italic;color:#FFA066;">purpose</em><br>meets<br>management.
</div>
<p style="font-size:13px;font-weight:300;line-height:1.8;color:rgba(255,255,255,0.45);max-width:300px;margin:0;">
S.P. Jain Institute of Management and Research — shaping responsible leaders for a complex world.
</p>
</div>
</div>
<div class="brand-stats" style="position:relative;z-index:2;display:flex;gap:2.5rem;padding-top:2rem;border-top:1px solid rgba(255,255,255,0.1);">
<div>
<div style="font-family:'Cormorant Garamond',serif;font-size:2rem;font-weight:600;color:#F67D31;line-height:1;">62+</div>
<div style="font-size:10px;color:rgba(255,255,255,0.35);letter-spacing:0.08em;text-transform:uppercase;margin-top:4px;">Years</div>
</div>
<div>
<div style="font-family:'Cormorant Garamond',serif;font-size:2rem;font-weight:600;color:#F67D31;line-height:1;">10K+</div>
<div style="font-size:10px;color:rgba(255,255,255,0.35);letter-spacing:0.08em;text-transform:uppercase;margin-top:4px;">Alumni</div>
</div>
<div>
<div style="font-family:'Cormorant Garamond',serif;font-size:2rem;font-weight:600;color:#F67D31;line-height:1;">#1</div>
<div style="font-size:10px;color:rgba(255,255,255,0.35);letter-spacing:0.08em;text-transform:uppercase;margin-top:4px;">Social impact</div>
</div>
</div>
</div>
""", unsafe_allow_html=True)
# ── Form heading ─────────────────────────────────────────────────────────
st.markdown("""
<div style="padding-top:1.5rem;max-width:420px;">
<div style="display:inline-flex;align-items:center;gap:7px;
background:rgba(246,125,49,0.12);color:#F67D31;
font-family:'DM Sans',sans-serif;font-size:10px;font-weight:600;
letter-spacing:0.1em;text-transform:uppercase;padding:5px 14px;
border-radius:100px;margin-bottom:1.2rem;border:1px solid rgba(246,125,49,0.3);">
<span style="width:5px;height:5px;border-radius:50%;background:#F67D31;flex-shrink:0;display:inline-block;"></span>
ESG Sustainability Platform
</div>
<div style="font-family:'Cormorant Garamond',serif;font-size:2.2rem;font-weight:400;
color:#fdf5ff;line-height:1.15;margin-bottom:0.4rem;">
Welcome back
</div>
<div style="font-family:'DM Sans',sans-serif;font-size:13px;color:#c4a4d4;
font-weight:300;line-height:1.65;margin-bottom:1.8rem;">
Sign in with your SPJIMR email to continue.
</div>
</div>
""", 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(
'''<div style="text-align:right;padding-top:8px;">
<a href="#" style="font-size:12px;color:#F67D31;font-weight:500;">Forgot password?</a>
</div>''',
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("""
<div style="display:flex;align-items:center;gap:12px;margin:1.4rem 0;">
<div style="flex:1;height:1px;background:rgba(196,164,212,0.2);"></div>
<span style="font-size:12px;color:#c4a4d4;font-family:'DM Sans',sans-serif;">or continue with</span>
<div style="flex:1;height:1px;background:rgba(196,164,212,0.2);"></div>
</div>
""", 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("""
<div style="margin-top:1.6rem;text-align:center;font-family:'DM Sans',sans-serif;font-size:12px;color:#c4a4d4;line-height:2;">
New to the platform? <a href="#" style="color:#F67D31;font-weight:500;">Request access</a><br>
Having trouble? <a href="#" style="color:#F67D31;font-weight:500;">Contact IT support</a>
</div>
""", unsafe_allow_html=True)
# ═══════════════════════════════════════════════════════════════════════════════
# Sidebar (logged-in only)
# ═══════════════════════════════════════════════════════════════════════════════
def render_sidebar() -> str:
with st.sidebar:
# Brand mark
st.markdown("""
<div style="padding:0.8rem 0 1rem;">
<div style="font-size:10px;letter-spacing:0.06em;color:#C4A4D4;text-transform:uppercase;margin-bottom:4px;">
Bharatiya Vidya Bhavan's
</div>
<div style="font-size:24px;font-weight:500;line-height:1;margin-bottom:2px;">
<span style="color:#F67D31;">SP</span><span style="color:#FDF5FF;">JIMR</span>
</div>
<div style="font-size:11px;color:#C4A4D4;letter-spacing:0.03em;">
ESG Sustainability Platform
</div>
</div>
""", 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"""
<div style="display:flex;align-items:center;gap:10px;
background:rgba(246,125,49,0.08);border:1px solid rgba(246,125,49,0.2);
border-radius:10px;padding:0.55rem 0.8rem;margin-bottom:0.8rem;">
<div style="width:32px;height:32px;border-radius:50%;background:#500073;
display:flex;align-items:center;justify-content:center;
font-size:12px;font-weight:600;color:#FFA066;flex-shrink:0;">
{initials}
</div>
<div>
<div style="font-size:12px;font-weight:500;color:#FDF5FF;">{user.split("@")[0]}</div>
<div style="font-size:10px;color:#C4A4D4;">{user.split("@")[1] if "@" in user else ""}</div>
</div>
</div>
""", 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("""
<div style="font-size:10px;color:rgba(196,164,212,0.45);line-height:1.6;margin-top:0.5rem;">
Built for SPJIMR · Powered by Mistral AI + FAISS<br>All data stays local
</div>
""", 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"""
<div style="padding:1.4rem 0 0.6rem;
border-bottom:1px solid rgba(246,125,49,0.18);
margin-bottom:1.5rem;">
<span style="font-size:0.72rem;color:#F67D31;letter-spacing:2.5px;text-transform:uppercase;font-weight:500;">
SP Jain Institute of Management &amp; Research
</span>
<h1 style="margin:0.3rem 0 0;font-size:2rem;line-height:1.1;
font-family:'Cormorant Garamond',serif;font-weight:600;color:#F67D31;">
Strategic ESG Consultant
</h1>
<p style="color:#C4A4D4;font-size:0.85rem;margin-top:0.35rem;">
{greeting} · Local RAG Pipeline · Qwen2.5 + Phi-3.5 · FAISS · Zero Data Egress
</p>
</div>
""", 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'<div class="esg-card"><h4>{icon} {fname}</h4>'
f'<div class="sub">{ext.upper()} · Indexed ✓</div></div>',
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'<div class="answer-box">{response["answer"]}</div>', 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 = '<div style="display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:1.2rem;">'
for color, label in badges:
badge_html += (
f'<div style="background:rgba({int(color[1:3],16)},{int(color[3:5],16)},{int(color[5:7],16)},0.12);'
f'border:1px solid rgba({int(color[1:3],16)},{int(color[3:5],16)},{int(color[5:7],16)},0.35);'
f'border-radius:8px;padding:0.4rem 0.9rem;font-size:0.72rem;color:{color};">{label}</div>'
)
badge_html += '</div>'
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'<div class="prompt-card">{full_brief}</div>', 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'<div class="prompt-card">{result["answer"]}</div>', 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'<div class="esg-card"><h4>{task}</h4>'
f'<div class="big-num" style="font-size:0.92rem;color:#C4A4D4;">{model}</div>'
f'<div class="sub">{org} · {task_type}</div></div>',
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()