Spaces:
Sleeping
Sleeping
File size: 74,091 Bytes
339726b c0f5d67 339726b 38a6b9a c0f5d67 339726b c0f5d67 1566616 c0f5d67 339726b ae41df5 339726b c0f5d67 339726b 38a6b9a 40cd131 38a6b9a 1b5bb17 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 ef4c850 c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b ef4c850 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 ef4c850 c0f5d67 ef4c850 c0f5d67 ef4c850 c0f5d67 ef4c850 c0f5d67 ef4c850 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a 339726b 38a6b9a 339726b 38a6b9a 339726b ef4c850 339726b c0f5d67 339726b c0f5d67 ef4c850 c0f5d67 ef4c850 9a0a3d3 ef4c850 c0f5d67 01cfb7e 524476d 01cfb7e 524476d ef4c850 01cfb7e ef4c850 01cfb7e ef4c850 01cfb7e c0f5d67 01cfb7e c0f5d67 ef4c850 c0f5d67 ef4c850 01cfb7e ef4c850 c0f5d67 ef4c850 c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a 339726b 38a6b9a 339726b c0f5d67 339726b 40cd131 339726b ef4c850 339726b c0f5d67 ef4c850 c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 38a6b9a 339726b c0f5d67 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 38a6b9a 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 38a6b9a 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 38a6b9a 1b5bb17 339726b 38a6b9a 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 38a6b9a 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 38a6b9a 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a 339726b 38a6b9a 339726b 38a6b9a 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 38a6b9a 339726b c0f5d67 339726b c0f5d67 38a6b9a 339726b c0f5d67 339726b 38a6b9a 339726b 38a6b9a 339726b c0f5d67 38a6b9a 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 38a6b9a 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 38a6b9a 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a c0f5d67 339726b c0f5d67 339726b 38a6b9a 339726b c0f5d67 339726b 38a6b9a 339726b 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 339726b c0f5d67 38a6b9a c0f5d67 38a6b9a c0f5d67 38a6b9a c0f5d67 38a6b9a c0f5d67 38a6b9a c0f5d67 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a c0f5d67 38a6b9a 339726b c0f5d67 339726b c0f5d67 339726b 38a6b9a c0f5d67 339726b 38a6b9a c0f5d67 38a6b9a c0f5d67 339726b c0f5d67 1566616 c0f5d67 1566616 c0f5d67 1566616 c0f5d67 1566616 339726b c0f5d67 339726b 38a6b9a c0f5d67 38a6b9a 339726b ef4c850 339726b c0f5d67 ef4c850 c0f5d67 ef4c850 c0f5d67 ef4c850 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 | """
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 & 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() |