''',
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("""
""", 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()