Files

1 line
59 KiB
JSON

{"repo": "anamicca23/resolvabot-llm", "files": {"app.py": {"23995428027237c868eacbb82f214e53f29caff1": {"t": 1785674394.1982868, "n": 54928, "h": "bf14938266734e29", "c": "\"\"\"\napp.py — ResolvaBot LLM (All 7 UI fixes applied)\n\"\"\"\nimport os, sys, re, html, base64\nimport streamlit as st\nimport streamlit.components.v1 as components\nfrom dotenv import load_dotenv\n\nSRC = os.path.join(os.path.dirname(__file__), \"src\")\nif SRC not in sys.path: sys.path.insert(0, SRC)\nload_dotenv()\n\nst.set_page_config(\n page_title=\"ResolvaBot LLM\", page_icon=\"📚\",\n layout=\"wide\", initial_sidebar_state=\"collapsed\",\n)\n\n# ── Themes ────────────────────────────────────────────────────────────────────\nTHEMES = {\n \"dark\": {\"name\":\"Dark\", \"ac\":\"#58a6ff\",\"ac2\":\"#8b5cf6\",\"rgb\":\"88,166,255\",\n \"page\":\"#0d1117\",\"card\":\"#161b22\",\"panel\":\"#1c2333\",\"inp\":\"#1f2937\",\n \"b1\":\"#30363d\",\"b2\":\"#21262d\",\"t1\":\"#e6edf3\",\"t2\":\"#8b949e\",\"t3\":\"#484f58\",\n \"green\":\"#3fb950\",\"gbg\":\"rgba(63,185,80,.12)\",\"shd\":\"0 8px 32px rgba(0,0,0,.5)\"},\n \"light\": {\"name\":\"Light\", \"ac\":\"#0969da\",\"ac2\":\"#7c3aed\",\"rgb\":\"9,105,218\",\n \"page\":\"#f6f8fa\",\"card\":\"#ffffff\",\"panel\":\"#f0f4f8\",\"inp\":\"#f8fafc\",\n \"b1\":\"#d0d7de\",\"b2\":\"#e2e8f0\",\"t1\":\"#0d1117\",\"t2\":\"#57606a\",\"t3\":\"#9ca3af\",\n \"green\":\"#1a7f37\",\"gbg\":\"rgba(26,127,55,.08)\",\"shd\":\"0 4px 16px rgba(0,0,0,.1)\"},\n \"indigo\": {\"name\":\"Indigo\", \"ac\":\"#7c6af7\",\"ac2\":\"#e86af7\",\"rgb\":\"124,106,247\",\n \"page\":\"#0f0e17\",\"card\":\"#16152a\",\"panel\":\"#1c1b33\",\"inp\":\"#211f3c\",\n \"b1\":\"#2e2b52\",\"b2\":\"#3a3760\",\"t1\":\"#fffffe\",\"t2\":\"#a7a9be\",\"t3\":\"#5c5a7e\",\n \"green\":\"#3ecf8e\",\"gbg\":\"rgba(62,207,142,.1)\",\"shd\":\"0 8px 32px rgba(0,0,0,.6)\"},\n \"teal\": {\"name\":\"Teal\", \"ac\":\"#2dd4bf\",\"ac2\":\"#22d3ee\",\"rgb\":\"45,212,191\",\n \"page\":\"#0a1628\",\"card\":\"#0f2237\",\"panel\":\"#122840\",\"inp\":\"#163048\",\n \"b1\":\"#1e3d55\",\"b2\":\"#254d6b\",\"t1\":\"#e2f4ff\",\"t2\":\"#7db3cc\",\"t3\":\"#3d7090\",\n \"green\":\"#34d399\",\"gbg\":\"rgba(52,211,153,.1)\",\"shd\":\"0 8px 32px rgba(0,0,0,.5)\"},\n \"rose\": {\"name\":\"Rose\", \"ac\":\"#f43f5e\",\"ac2\":\"#ec4899\",\"rgb\":\"244,63,94\",\n \"page\":\"#0f0a0c\",\"card\":\"#1a0f13\",\"panel\":\"#22141a\",\"inp\":\"#2a1820\",\n \"b1\":\"#3d1f28\",\"b2\":\"#4d2535\",\"t1\":\"#fff1f3\",\"t2\":\"#fca5a5\",\"t3\":\"#6b2737\",\n \"green\":\"#34d399\",\"gbg\":\"rgba(52,211,153,.1)\",\"shd\":\"0 8px 32px rgba(0,0,0,.6)\"},\n \"amber\": {\"name\":\"Amber\", \"ac\":\"#f59e0b\",\"ac2\":\"#ef4444\",\"rgb\":\"245,158,11\",\n \"page\":\"#0c0a04\",\"card\":\"#1a1504\",\"panel\":\"#221d06\",\"inp\":\"#2a2408\",\n \"b1\":\"#3d3410\",\"b2\":\"#4d4218\",\"t1\":\"#fefce8\",\"t2\":\"#fde68a\",\"t3\":\"#713f12\",\n \"green\":\"#34d399\",\"gbg\":\"rgba(52,211,153,.1)\",\"shd\":\"0 8px 32px rgba(0,0,0,.6)\"},\n \"mono\": {\"name\":\"Mono\", \"ac\":\"#94a3b8\",\"ac2\":\"#64748b\",\"rgb\":\"148,163,184\",\n \"page\":\"#0a0a0a\",\"card\":\"#111111\",\"panel\":\"#1a1a1a\",\"inp\":\"#222222\",\n \"b1\":\"#2d2d2d\",\"b2\":\"#3d3d3d\",\"t1\":\"#f8f8f8\",\"t2\":\"#a0a0a0\",\"t3\":\"#555555\",\n \"green\":\"#34d399\",\"gbg\":\"rgba(52,211,153,.1)\",\"shd\":\"0 8px 32px rgba(0,0,0,.7)\"},\n}\n\n# ── Session state defaults ─────────────────────────────────────────────────────\nfor k, v in dict(\n view=\"upload\", theme=\"dark\", sidebar_open=False,\n indexed=False, retriever=None, history=[],\n last_file=\"\", last_file_bytes=None,\n pages=0, chunks=0, nodes=0, file_size=\"\", step=0,\n cur_passages=[],\n groq_key=os.getenv(\"GROQ_API_KEY\",\"\"),\n openai_key=os.getenv(\"OPENAI_API_KEY\",\"\"),\n).items():\n if k not in st.session_state: st.session_state[k] = v\n\nif st.session_state.groq_key: os.environ[\"GROQ_API_KEY\"] = st.session_state.groq_key\nif st.session_state.openai_key:os.environ[\"OPENAI_API_KEY\"] = st.session_state.openai_key\nhas_groq = bool(os.getenv(\"GROQ_API_KEY\",\"\").strip())\nhas_openai = bool(os.getenv(\"OPENAI_API_KEY\",\"\").strip())\n\nT = THEMES[st.session_state.theme]\nac = T[\"ac\"]; ac2 = T[\"ac2\"]; rgb = T[\"rgb\"]\n\ndef go(v): st.session_state.view = v; st.rerun()\ndef detect_lang(t):\n if re.search(r'#include|cout|cin|vector<|int main', t): return \"cpp\"\n if re.search(r'\\bdef \\b|\\bimport \\b|print\\(', t): return \"python\"\n if re.search(r'public class|System\\.out', t): return \"java\"\n return \"text\"\ndef pill(text, cls=\"ps\"):\n return f'<span class=\"pill {cls}\">{text}</span>'\n\nst.markdown(f\"\"\"\n<style>\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');\n \n*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}\n \nhtml, body {{\n background: {T['page']} !important;\n height: 100vh !important;\n overflow: hidden !important;\n}}\n.stApp {{\n background: {T['page']} !important;\n font-family: 'Inter', -apple-system, sans-serif;\n color: {T['t1']};\n}}\n#MainMenu, footer, header,\n[data-testid=\"stDecoration\"],\n[data-testid=\"collapsedControl\"],\n.stDeployButton {{ display: none !important; }}\n \n/* Zero all Streamlit default padding */\n.block-container {{ padding: 0 !important; max-width: 100% !important; }}\nsection[data-testid=\"stSidebar\"] {{ display: none !important; }}\n \n[data-testid=\"stAppViewContainer\"] {{\n padding: 0 !important;\n padding-top: 56px !important;\n overflow-y: auto !important;\n overflow-x: hidden !important;\n height: 100vh !important;\n background: {T['page']} !important;\n scrollbar-width: thin;\n scrollbar-color: {T['b2']} transparent;\n}}\n[data-testid=\"stAppViewContainer\"]::-webkit-scrollbar {{ width: 4px; }}\n[data-testid=\"stAppViewContainer\"]::-webkit-scrollbar-thumb {{\n background: {T['b2']}; border-radius: 4px;\n}}\n \n[data-testid=\"stMain\"] {{\n padding: 0 !important;\n overflow: visible !important;\n background: {T['page']} !important;\n}}\n \n[data-testid=\"stMainBlockContainer\"] {{\n max-width: 860px !important;\n margin: 0 auto !important;\n padding: 1.5rem 1.5rem 4rem !important;\n min-width: 0 !important;\n background: {T['page']} !important;\n}}\n \n/* ── Fixed topbar ── */\n.rb-topbar {{\n position: fixed; top: 0; left: 0; right: 0; z-index: 1000;\n height: 56px; background: {T['card']}; border-bottom: 1px solid {T['b1']};\n display: flex; align-items: center; padding: 0 1.5rem;\n box-shadow: 0 1px 4px rgba(0,0,0,.15);\n}}\n.rb-topbar-inner {{\n display: flex; align-items: center;\n width: 100%; max-width: 900px; margin: 0 auto;\n}}\n.rb-brand {{ display: flex; align-items: center; gap: 9px; flex-shrink: 0; }}\n.rb-logo {{\n width: 30px; height: 30px; border-radius: 8px;\n background: linear-gradient(135deg, {ac}, {ac2});\n display: flex; align-items: center; justify-content: center;\n font-size: .9rem; box-shadow: 0 2px 8px rgba({rgb},.3);\n}}\n.rb-brand-name {{ font-size: .9rem; font-weight: 700; letter-spacing: -.02em; color: {T['t1']}; }}\n.rb-brand-name span {{ font-weight: 300; color: {T['t3']}; font-size: .8rem; }}\n.rb-breadcrumb {{ display: flex; align-items: center; gap: 6px; margin-left: 12px; }}\n.rb-sep {{ color: {T['t3']}; font-size: .8rem; }}\n.rb-page-name {{ font-size: .8rem; font-weight: 500; color: {T['t2']}; }}\n.rb-topbar-right {{ display: flex; align-items: center; gap: 8px; margin-left: auto; }}\n \n/* Pills */\n.pill {{ display: inline-flex; align-items: center; gap: 3px; padding: 2px 8px; border-radius: 20px; font-size: .63rem; font-weight: 600; }}\n.pg {{ background: {T['gbg']}; color: {T['green']}; border: 1px solid rgba(63,185,80,.25); }}\n.pb {{ background: rgba({rgb},.1); color: {ac}; border: 1px solid rgba({rgb},.25); }}\n.pp {{ background: rgba(139,92,246,.1); color: #8b5cf6; border: 1px solid rgba(139,92,246,.2); }}\n.py {{ background: rgba(245,158,11,.1); color: #d97706; border: 1px solid rgba(245,158,11,.2); }}\n.ps {{ background: {T['panel']}; color: {T['t2']}; border: 1px solid {T['b1']}; }}\n \n/* ── Sidebar overlay ── */\n.rb-sidebar-overlay {{\n position: fixed; inset: 0; top: 56px;\n background: rgba(0,0,0,.45); z-index: 900; backdrop-filter: blur(2px);\n}}\n.rb-sidebar {{\n position: fixed; top: 56px; left: 0; bottom: 0; width: 240px; z-index: 950;\n background: {T['card']}; border-right: 1px solid {T['b1']};\n display: flex; flex-direction: column; padding: 1rem .75rem; gap: 2px;\n overflow-y: auto; box-shadow: 4px 0 24px rgba(0,0,0,.3);\n animation: slideIn .2s ease;\n}}\n@keyframes slideIn {{ from{{transform:translateX(-20px);opacity:0}} to{{transform:translateX(0);opacity:1}} }}\n.sb-hdr {{ font-size:.62rem; font-weight:700; letter-spacing:.1em; color:{T['t3']}; text-transform:uppercase; padding:.5rem .4rem .25rem; margin-top:.3rem; }}\n.sb-link {{ display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:9px; font-size:.82rem; font-weight:500; color:{T['t2']}; cursor:pointer; transition:all .15s; width:100%; border:none; background:transparent; white-space:nowrap; text-align:left; }}\n.sb-link:hover {{ background:rgba({rgb},.1); color:{T['t1']}; }}\n.sb-link.active {{ background:rgba({rgb},.15); color:{ac}; font-weight:600; }}\n.sb-div {{ height:1px; background:{T['b1']}; margin:.6rem 0; }}\n \n/* Theme swatches */\n.sb-swatches {{ display:flex; gap:7px; padding:.3rem .4rem; flex-wrap:wrap; }}\n.swatch {{ width:22px; height:22px; border-radius:50%; cursor:pointer; border:2.5px solid transparent; transition:transform .15s; box-shadow:0 1px 4px rgba(0,0,0,.3); }}\n.swatch:hover {{ transform:scale(1.15); }}\n.swatch.sel {{ border-color:{T['t1']}; box-shadow:0 0 0 2px {T['page']},0 0 0 4px {ac}; }}\n \n/* ── Upload hero ── */\n.up-hero {{\n text-align:center; padding:2rem 1.5rem 1.6rem;\n background:{T['card']}; border:1px solid {T['b1']};\n border-radius:18px; margin-bottom:1.2rem;\n position:relative; overflow:hidden;\n}}\n.up-hero::before {{\n content:''; position:absolute; top:-60px; right:-60px;\n width:220px; height:220px;\n background:radial-gradient(circle,rgba({rgb},.07),transparent 70%);\n border-radius:50%; pointer-events:none;\n}}\n.up-hero h1 {{ font-size:1.5rem; font-weight:800; letter-spacing:-.03em; color:{T['t1']}; margin-bottom:.35rem; }}\n.up-hero h1 em {{ font-style:normal; color:{ac}; }}\n.up-hero p {{ font-size:.82rem; color:{T['t2']}; max-width:400px; margin:0 auto; line-height:1.65; }}\n.feat-row {{ display:flex; gap:5px; flex-wrap:wrap; justify-content:center; margin-top:.9rem; }}\n.ftag {{ display:inline-flex; align-items:center; gap:4px; padding:4px 10px; border-radius:20px; background:{T['panel']}; border:1px solid {T['b1']}; font-size:.67rem; font-weight:600; color:{T['t2']}; }}\n \n/* ── Upload cards ── */\n.up-card {{ background:{T['card']}; border:1px solid {T['b1']}; border-radius:18px; overflow:hidden; box-shadow:0 2px 10px rgba(0,0,0,.2); }}\n.up-card-head {{ display:flex; align-items:center; gap:9px; padding:.8rem 1rem; border-bottom:1px solid {T['b1']}; background:{T['panel']}; }}\n.up-card-ico {{ width:28px; height:28px; border-radius:7px; background:rgba({rgb},.15); border:1px solid rgba({rgb},.25); display:flex; align-items:center; justify-content:center; font-size:.85rem; }}\n.up-card-lbl {{ font-size:.75rem; font-weight:700; color:{T['t1']}; }}\n.up-card-sub {{ font-size:.62rem; color:{T['t3']}; margin-top:1px; }}\n.up-card-body {{ padding:1rem; }}\n \n/* ── Drop zone ── */\n.dz {{ border:2px dashed {T['b2']}; border-radius:14px; padding:1.8rem 1rem; text-align:center; background:{T['panel']}; transition:all .2s; }}\n.dz:hover {{ border-color:{ac}; background:rgba({rgb},.06); }}\n.dz-ico {{ font-size:2.1rem; margin-bottom:.6rem; filter:drop-shadow(0 2px 8px rgba({rgb},.25)); }}\n.dz-t {{ font-size:.9rem; font-weight:700; color:{T['t1']}; margin-bottom:.25rem; }}\n.dz-s {{ font-size:.73rem; color:{T['t2']}; }}\n.dz-br {{ color:{ac}; font-weight:600; }}\n.file-ok {{ display:flex; align-items:center; gap:10px; padding:10px 13px; background:{T['gbg']}; border:1px solid rgba(63,185,80,.25); border-radius:11px; margin-bottom:.8rem; }}\n.file-ok-n {{ font-size:.8rem; font-weight:700; color:{T['green']}; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }}\n.file-ok-m {{ font-size:.64rem; color:{T['t2']}; margin-top:1px; }}\n \n/* ── Pipeline steps ── */\n.pipe {{ display:flex; flex-direction:column; }}\n.pstep {{ display:flex; align-items:center; gap:10px; padding:9px 11px; border-radius:10px; border:1px solid {T['b1']}; background:{T['panel']}; transition:all .2s; }}\n.pstep.done {{ border-color:rgba(63,185,80,.3); background:{T['gbg']}; }}\n.pstep.actv {{ border-color:rgba({rgb},.4); background:rgba({rgb},.06); box-shadow:0 0 0 3px rgba({rgb},.1); }}\n.pconn {{ width:1.5px; height:7px; background:{T['b1']}; margin-left:17px; }}\n.pconn.done {{ background:rgba(63,185,80,.4); }}\n.pn {{ width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:.68rem; font-weight:700; flex-shrink:0; }}\n.pn-i {{ background:{T['panel']}; color:{T['t3']}; border:1.5px solid {T['b1']}; }}\n.pn-d {{ background:{T['gbg']}; color:{T['green']}; border:1.5px solid rgba(63,185,80,.4); }}\n.pn-a {{ background:rgba({rgb},.15); color:{ac}; border:1.5px solid rgba({rgb},.4); animation:pb 1s infinite; }}\n@keyframes pb {{ 0%,100%{{opacity:1}} 50%{{opacity:.35}} }}\n.pi {{ flex:1; min-width:0; }}\n.pn-lbl {{ font-size:.78rem; font-weight:600; color:{T['t1']}; }}\n.pn-lbl.dn {{ color:{T['green']}; }}\n.pn-lbl.av {{ color:{ac}; }}\n.pd {{ font-size:.62rem; color:{T['t3']}; margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }}\n.pbdg {{ font-size:.6rem; font-weight:700; padding:2px 8px; border-radius:20px; flex-shrink:0; }}\n.pbdg-i {{ background:{T['panel']}; color:{T['t3']}; }}\n.pbdg-d {{ background:{T['gbg']}; color:{T['green']}; }}\n.pbdg-a {{ background:rgba({rgb},.15); color:{ac}; }}\n \n/* Meta stats */\n.meta-g {{ display:grid; grid-template-columns:1fr 1fr; gap:7px; }}\n.meta-i {{ background:{T['panel']}; border:1px solid {T['b1']}; border-radius:10px; padding:10px 12px; text-align:center; }}\n.meta-v {{ font-size:1.25rem; font-weight:800; color:{ac}; font-family:'JetBrains Mono',monospace; line-height:1; }}\n.meta-k {{ font-size:.6rem; font-weight:700; color:{T['t3']}; letter-spacing:.07em; margin-top:4px; text-transform:uppercase; }}\n.meta-file {{ grid-column:1/-1; background:{T['panel']}; border:1px solid {T['b1']}; border-radius:10px; padding:9px 12px; display:flex; align-items:center; gap:9px; }}\n.mfn {{ font-size:.78rem; font-weight:600; color:{T['t1']}; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }}\n.mfm {{ font-size:.63rem; color:{T['t3']}; margin-top:1px; }}\n.ready-bar {{ display:flex; align-items:center; gap:12px; padding:12px 14px; background:linear-gradient(135deg,{T['gbg']},rgba(63,185,80,.04)); border:1.5px solid rgba(63,185,80,.3); border-radius:12px; margin-top:.75rem; }}\n.rdot {{ width:8px; height:8px; border-radius:50%; background:{T['green']}; box-shadow:0 0 0 3px rgba(63,185,80,.2); animation:pb 2s infinite; flex-shrink:0; }}\n.rtxt {{ font-size:.8rem; font-weight:700; color:{T['green']}; }}\n.rsub {{ font-size:.63rem; color:{T['t2']}; margin-top:2px; }}\n \n/* ── FIX 5: Chat page ── */\n.chat-outer {{\n position: fixed; top: 56px; left: 0; right: 0; bottom: 0;\n display: flex; flex-direction: column; align-items: center;\n background: {T['page']}; padding: .75rem 1.5rem 0;\n}}\n.chat-shell {{\n width: 100%; max-width: 860px;\n display: flex; flex-direction: column;\n flex: 1; min-height: 0;\n background: {T['card']}; border: 1px solid {T['b1']};\n border-radius: 18px 18px 0 0; overflow: hidden;\n box-shadow: {T['shd']};\n}}\n.chat-top {{\n display: flex; align-items: center; justify-content: space-between;\n padding: .65rem 1.2rem; border-bottom: 1px solid {T['b1']};\n background: {T['panel']}; flex-shrink: 0;\n}}\n.chat-tname {{ font-size: .84rem; font-weight: 700; color: {T['t1']}; }}\n.chat-tmeta {{ font-size: .67rem; color: {T['t2']}; margin-top: 1px; }}\n\n.chat-msgs {{\n flex: 1; overflow-y: auto; overflow-x: hidden;\n padding: 1.5rem 2rem 1rem;\n display: flex; flex-direction: column; gap: 1.5rem;\n scroll-behavior: smooth; min-height: 0;\n scrollbar-width: thin; scrollbar-color: {T['b2']} transparent;\n}}\n.chat-msgs::-webkit-scrollbar {{ width: 4px; }}\n.chat-msgs::-webkit-scrollbar-thumb {{ background: {T['b2']}; border-radius: 4px; }}\n\n.mrow {{ display: flex; gap: 12px; animation: fadeUp .2s ease; }}\n@keyframes fadeUp {{ from{{opacity:0;transform:translateY(6px)}} to{{opacity:1;transform:translateY(0)}} }}\n.mrow.user {{ flex-direction: row-reverse; }}\n.mav {{ width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: .8rem; flex-shrink: 0; margin-top: 2px; }}\n.mav-u {{ background: linear-gradient(135deg,{ac},{ac2}); box-shadow: 0 2px 8px rgba({rgb},.3); }}\n.mav-b {{ background: {T['panel']}; border: 1px solid {T['b1']}; }}\n.mbody {{ display: flex; flex-direction: column; max-width: 80%; }}\n.mrow.user .mbody {{ align-items: flex-end; }}\n\n.bub-u {{\n background: linear-gradient(135deg,{ac},{ac2}); color: #fff;\n border-radius: 18px 4px 18px 18px; padding: 10px 15px;\n font-size: .87rem; line-height: 1.65;\n box-shadow: 0 2px 10px rgba({rgb},.25);\n}}\n.bub-b {{\n background: {T['card']}; border: 1px solid {T['b1']};\n border-radius: 4px 18px 18px 18px; padding: 14px 16px;\n font-size: .875rem; line-height: 1.82;\n box-shadow: 0 1px 4px rgba(0,0,0,.1);\n}}\n.bub-b h2 {{ font-size: .9rem; font-weight: 700; color: {T['t1']}; margin: .9em 0 .35em; padding-bottom: .3em; border-bottom: 1px solid {T['b1']}; }}\n.bub-b h3 {{ font-size: .84rem; font-weight: 600; color: {ac}; margin: .75em 0 .25em; }}\n.bub-b p {{ margin: 0 0 .65em; line-height: 1.82; color: {T['t1']}; }}\n.bub-b p:last-child {{ margin-bottom: 0; }}\n.bub-b ul, .bub-b ol {{ padding-left: 1.4em; margin: .3em 0 .7em; }}\n.bub-b li {{ margin: .28em 0; line-height: 1.75; color: {T['t1']}; }}\n.bub-b strong {{ font-weight: 600; color: {T['t1']}; }}\n.bub-b code {{ background: {T['panel']}; border: 1px solid {T['b1']}; padding: 1px 6px; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: .78em; color: {ac}; }}\n.bub-b pre {{ background: #0d1117; border: 1px solid {T['b2']}; border-radius: 10px; padding: 14px 16px; overflow-x: auto; margin: .6em 0 .9em; }}\n.bub-b pre code {{ background: none; border: none; padding: 0; color: #7dd3fc; font-size: .78rem; line-height: 1.7; }}\n.bub-b blockquote {{ border-left: 3px solid {ac}; padding-left: 12px; color: {T['t2']}; margin: .5em 0; font-style: italic; }}\n.mmeta {{ display: flex; gap: 5px; margin-top: 6px; flex-wrap: wrap; }}\n\n.chat-empty {{ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .6rem; opacity: .5; text-align: center; padding: 2rem; }}\n.ce-ico {{ font-size: 2.8rem; margin-bottom: .2rem; }}\n.ce-t {{ font-size: .95rem; font-weight: 700; color: {T['t1']}; }}\n.ce-s {{ font-size: .78rem; color: {T['t2']}; max-width: 320px; line-height: 1.65; }}\n\n/* ── FIX 6: Sources page ── */\n.src-col {{\n background: {T['card']}; border: 1px solid {T['b1']};\n border-radius: 16px; overflow: hidden;\n box-shadow: {T['shd']};\n height: calc(100vh - 110px);\n display: flex; flex-direction: column;\n}}\n.src-head {{ display: flex; align-items: center; gap: 8px; padding: .7rem 1rem; border-bottom: 1px solid {T['b1']}; background: {T['panel']}; flex-shrink: 0; }}\n.src-htitle {{ font-size: .7rem; font-weight: 700; letter-spacing: .09em; color: {T['t3']}; text-transform: uppercase; }}\n.src-body {{ flex: 1; overflow-y: auto; padding: .75rem; min-height: 0; scrollbar-width: thin; scrollbar-color: {T['b2']} transparent; }}\n.src-body::-webkit-scrollbar {{ width: 3px; }}\n.src-body::-webkit-scrollbar-thumb {{ background: {T['b2']}; border-radius: 4px; }}\n.pcard {{ background: {T['panel']}; border: 1px solid {T['b1']}; border-radius: 10px; overflow: hidden; margin-bottom: 8px; transition: box-shadow .15s; }}\n.pcard:hover {{ box-shadow: 0 4px 16px rgba(0,0,0,.25); }}\n.pcard-h {{ display: flex; align-items: center; justify-content: space-between; padding: 6px 11px; background: {T['card']}; border-bottom: 1px solid {T['b1']}; }}\n.pcard-n {{ font-size: .67rem; font-weight: 700; color: {ac}; font-family: 'JetBrains Mono', monospace; }}\n.pcard-s {{ font-size: .62rem; color: {T['t3']}; font-family: 'JetBrains Mono', monospace; }}\n.pcard-b {{ padding: 10px 11px; font-size: .78rem; line-height: 1.75; color: {T['t2']}; }}\n\n/* FIX 7 — Button styles */\n.stButton > button {{\n background: {T['panel']} !important; border: 1.5px solid {T['b2']} !important;\n color: {T['t2']} !important; border-radius: 9px !important;\n font-family: 'Inter', sans-serif !important; font-size: .79rem !important;\n font-weight: 500 !important; transition: all .15s !important;\n padding: 7px 16px !important;\n}}\n.stButton > button:hover {{\n border-color: {ac} !important; color: {ac} !important;\n background: rgba({rgb},.08) !important;\n}}\n.stButton > button[kind=\"primary\"] {{\n background: linear-gradient(135deg,{ac},{ac2}) !important;\n border-color: transparent !important; color: #fff !important;\n box-shadow: 0 2px 10px rgba({rgb},.3) !important;\n}}\n.stButton > button[kind=\"primary\"]:hover {{ opacity: .9 !important; color: #fff !important; }}\n\n/* Streamlit input overrides */\n[data-testid=\"stFileUploader\"] {{ background: transparent !important; border: none !important; padding: 0 !important; }}\n.stProgress > div > div {{ background: linear-gradient(90deg,{ac},{ac2}) !important; border-radius: 4px !important; height: 3px !important; }}\n[data-testid=\"stProgressBarMessage\"] {{ font-size: .72rem !important; color: {T['t2']} !important; }}\n[data-testid=\"stChatInput\"] {{ padding: 0 !important; background: transparent !important; }}\n[data-testid=\"stChatInput\"] > div {{ background: transparent !important; border: none !important; box-shadow: none !important; }}\n[data-testid=\"stChatInput\"] textarea {{\n background: {T['inp']} !important; border: 1.5px solid {T['b2']} !important;\n border-radius: 12px !important; color: {T['t1']} !important;\n font-family: 'Inter', sans-serif !important; font-size: .86rem !important;\n padding: 10px 14px !important; resize: none !important;\n}}\n[data-testid=\"stChatInput\"] textarea:focus {{ border-color: {ac} !important; box-shadow: 0 0 0 3px rgba({rgb},.12) !important; }}\n[data-testid=\"stChatInput\"] textarea::placeholder {{ color: {T['t3']} !important; }}\ndetails summary {{ background: {T['panel']} !important; border: 1px solid {T['b1']} !important; border-radius: 9px !important; font-size: .77rem !important; color: {T['t2']} !important; padding: 7px 12px !important; }}\ndetails > div {{ background: {T['panel']} !important; border: 1px solid {T['b1']} !important; border-top: none !important; border-radius: 0 0 9px 9px !important; padding: 10px !important; }}\n.stTextInput input {{ background: {T['inp']} !important; border: 1.5px solid {T['b2']} !important; border-radius: 9px !important; color: {T['t1']} !important; font-family: 'JetBrains Mono', monospace !important; font-size: .77rem !important; padding: 8px 11px !important; }}\n.stTextInput input:focus {{ border-color: {ac} !important; }}\nlabel {{ color: {T['t2']} !important; font-size: .77rem !important; font-weight: 500 !important; }}\n.stSelectbox [data-baseweb=\"select\"] > div {{ background: {T['inp']} !important; border: 1.5px solid {T['b2']} !important; border-radius: 9px !important; font-size: .8rem !important; color: {T['t1']} !important; }}\n.stAlert {{ border-radius: 10px !important; font-size: .78rem !important; }}\n.stSuccess {{ background: {T['gbg']} !important; border: 1px solid rgba(63,185,80,.3) !important; color: {T['green']} !important; }}\n.stError {{ background: rgba(248,113,113,.08) !important; border: 1px solid rgba(248,113,113,.25) !important; color: #f87171 !important; }}\n[data-testid=\"stChatMessage\"] {{ background: transparent !important; border: none !important; padding: 0 !important; }}\n</style>\n\"\"\", unsafe_allow_html=True)\n\n# ── Helpers ────────────────────────────────────────────────────────────────────\ndef mpill(model, src):\n if model.startswith(\"groq/\"): return pill(f\"🟢 {model.split('/')[1][:20]}\", \"pg\")\n if model.startswith(\"openai/\"): return pill(\"🤖 GPT-3.5\", \"pb\")\n if \"wiki\" in src: return pill(\"🌐 Wikipedia\", \"py\")\n return pill(\"📄 Context\", \"ps\")\ndef spill(src):\n return pill(\"📗 Textbook\", \"pp\") if \"textbook\" in src else pill(\"🌐 Wikipedia\", \"py\")\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# TOP BAR\n# ═══════════════════════════════════════════════════════════════════════════════\nv = st.session_state.view\nidx = st.session_state.indexed\npage_labels = {\"upload\":\"📂 Upload & Index\",\"chat\":\"💬 Chat\",\"sources\":\"🔍 Sources & PDF\"}\nllm_badge = (pill(\"🟢 Groq\",\"pg\") if has_groq\n else pill(\"🤖 OpenAI\",\"pb\") if has_openai\n else pill(\"⚠ No LLM\",\"py\"))\nidx_badge = pill(\"● Indexed\",\"pg\") if idx else pill(\"○ Not Indexed\",\"ps\")\n\nst.markdown(f\"\"\"\n<div class=\"rb-topbar\">\n <div class=\"rb-topbar-inner\">\n <div class=\"rb-brand\">\n <div class=\"rb-logo\">📚</div>\n <div class=\"rb-brand-name\">ResolvaBot <span>LLM</span></div>\n </div>\n <div class=\"rb-breadcrumb\">\n <span class=\"rb-sep\">·</span>\n <span class=\"rb-page-name\">{page_labels.get(v,'')}</span>\n </div>\n <div class=\"rb-topbar-right\">\n {llm_badge}&nbsp;{idx_badge}\n </div>\n </div>\n</div>\n\"\"\", unsafe_allow_html=True)\n\n# ── Hamburger toggle ──────────────────────────────────────────────────────────\n# FIX: marker div so CSS :has() can target ONLY the hamburger stHorizontalBlock\n# (not the upload columns), avoiding the :first-of-type collision bug\nst.markdown(\n '<div id=\"rb-hamb-before\" style=\"display:none;height:0;overflow:hidden;'\n 'margin:0;padding:0;line-height:0;font-size:0\"></div>',\n unsafe_allow_html=True,\n)\nhamb_col = st.columns([1, 20])[0]\nwith hamb_col:\n hamb_label = \"✕\" if st.session_state.sidebar_open else \"☰\"\n if st.button(hamb_label, key=\"hamburger\"):\n st.session_state.sidebar_open = not st.session_state.sidebar_open\n st.rerun()\n\nst.markdown(f\"\"\"\n<style>\n/*\n HAMBURGER FIX:\n Streamlit wraps every widget in its own elementContainer div, which is a\n direct child of stVerticalBlock. So stMarkdownContainer and stHorizontalBlock\n are never direct siblings — the :has(marker) + stHorizontalBlock selector\n always fails. Correct approach: target the elementContainer SIBLINGS inside\n stVerticalBlock, then descend into the stHorizontalBlock inside.\n*/\n[data-testid=\"stVerticalBlock\"] > div:has(#rb-hamb-before) + div [data-testid=\"stHorizontalBlock\"] {{\n position: fixed; top: 0; left: 0; z-index: 1100;\n height: 56px; display: flex; align-items: center;\n padding: 0 1rem; background: transparent;\n pointer-events: auto;\n}}\n[data-testid=\"stVerticalBlock\"] > div:has(#rb-hamb-before) + div [data-testid=\"stHorizontalBlock\"] .stButton > button {{\n background: transparent !important; border: none !important;\n color: {T['t2']} !important; font-size: 1.1rem !important;\n padding: 4px 8px !important; border-radius: 6px !important;\n height: 32px !important; width: 36px !important;\n min-width: 0 !important;\n}}\n[data-testid=\"stVerticalBlock\"] > div:has(#rb-hamb-before) + div [data-testid=\"stHorizontalBlock\"] .stButton > button:hover {{\n background: rgba({rgb},.1) !important; color: {T['t1']} !important;\n border-color: transparent !important;\n}}\n</style>\n\"\"\", unsafe_allow_html=True)\n\n\n\n# ── Sidebar overlay ────────────────────────────────────────────────────────────\n\n# ── Sidebar overlay ────────────────────────────────────────────────────────────\n\nif st.session_state.sidebar_open:\n\n cur = st.session_state.theme\n\n SWATCH_DATA = [\n (\"dark\", \"\\U0001f535\", \"Dark\"),\n (\"light\", \"\\u2b1c\", \"Light\"),\n (\"indigo\", \"\\U0001f7e3\", \"Indigo\"),\n (\"teal\", \"\\U0001f7e2\", \"Teal\"),\n (\"rose\", \"\\U0001f534\", \"Rose\"),\n (\"amber\", \"\\U0001f7e0\", \"Amber\"),\n (\"mono\", \"\\u26ab\", \"Mono\"),\n ]\n _theme_list = list(t for t, *_ in SWATCH_DATA)\n _active_i = _theme_list.index(cur) + 1 # 1-based nth-child\n\n st.markdown('<div class=\"rb-sidebar-overlay\"></div>', unsafe_allow_html=True)\n\n st.markdown(f\"\"\"\n<style>\n/* ── Panel shell ── */\n.rb-sidebar {{\n position:fixed;top:56px;left:0;bottom:0;width:240px;z-index:950;\n background:{T['card']};border-right:1px solid {T['b1']};\n box-shadow:10px 0 30px rgba(0,0,0,.45);\n animation:sbIn .18s cubic-bezier(.4,0,.2,1);pointer-events:none;\n}}\n@keyframes sbIn{{from{{transform:translateX(-240px);opacity:0}}to{{transform:translateX(0);opacity:1}}}}\n\n/* ── Widget container ── */\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) {{\n position:fixed !important;top:56px !important;left:0 !important;\n width:240px !important;height:calc(100vh - 56px) !important;\n z-index:960 !important;padding:0 !important;margin:0 !important;\n background:{T['card']} !important;\n overflow-y:auto !important;overflow-x:hidden !important;\n scrollbar-width:thin;scrollbar-color:{T['b2']} transparent;\n pointer-events:auto !important;\n}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root)::-webkit-scrollbar{{width:3px;}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root)::-webkit-scrollbar-thumb{{\n background:{T['b2']};border-radius:4px;\n}}\n\n/* ── Section label ── */\n.sb-sec {{\n display:block;font-size:.58rem;font-weight:800;letter-spacing:.14em;\n text-transform:uppercase;color:{T['t3']};padding:.85rem 12px .3rem;\n}}\n/* ── Divider ── */\n.sb-hr{{height:1px;background:{T['b1']};margin:.3rem 12px;}}\n\n/* ── Navigation buttons ── */\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-nav .stButton>button {{\n width:calc(100% - 16px) !important;\n margin:4px 8px !important;\n padding:0 12px !important;\n border-radius:8px !important;\n height:36px !important;\n font-size:.82rem !important;\n font-weight:500 !important;\n text-align:left !important;\n justify-content:flex-start !important;\n border:1px solid {T['b1']} !important;\n background:transparent !important;\n color:{T['t2']} !important;\n transition:all .14s !important;\n line-height:1 !important;\n}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-nav .stButton>button:hover{{\n background:rgba({rgb},.1) !important;\n color:{ac} !important;\n border-color:rgba({rgb},.3) !important;\n}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-nav .stButton>button[kind=\"primary\"]{{\n background:linear-gradient(135deg,{ac},{ac2}) !important;\n color:#fff !important;\n font-weight:600 !important;\n border-color:transparent !important;\n box-shadow:0 2px 10px rgba({rgb},.3) !important;\n}}\n\n/* ── Swatch row: 7 circles, all inside 240px ── */\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-sw [data-testid=\"stHorizontalBlock\"] {{\n display:flex !important;\n flex-wrap:nowrap !important;\n justify-content:space-between !important;\n align-items:center !important;\n padding:4px 12px 8px !important;\n margin:0 !important;\n gap:0 !important;\n max-width:240px !important;\n overflow:hidden !important;\n}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-sw [data-testid=\"column\"] {{\n flex:0 0 26px !important;\n width:26px !important;\n min-width:0 !important;\n max-width:26px !important;\n padding:0 !important;\n overflow:hidden !important;\n}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-sw .stButton>button {{\n width:22px !important;\n height:22px !important;\n padding:0 !important;\n min-width:0 !important;\n border-radius:50% !important;\n font-size:.85rem !important;\n border:2px solid transparent !important;\n background:transparent !important;\n line-height:22px !important;\n transition:transform .12s !important;\n display:flex !important;\n align-items:center !important;\n justify-content:center !important;\n overflow:hidden !important;\n}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-sw .stButton>button > div {{\n display:flex !important;\n align-items:center !important;\n justify-content:center !important;\n padding:0 !important;\n margin:0 !important;\n line-height:1 !important;\n}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-sw .stButton>button p {{\n margin:0 !important;\n padding:0 !important;\n line-height:1 !important;\n font-size:.85rem !important;\n}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-sw .stButton>button:hover{{\n transform:scale(1.25) !important;\n}}\n/* Active swatch ring */\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-sw [data-testid=\"column\"]:nth-child({_active_i}) .stButton>button {{\n border-color:{T['t1']} !important;\n box-shadow:0 0 0 2px {T['page']},0 0 0 4px {ac} !important;\n transform:scale(1.15) !important;\n}}\n\n/* ── File info card ── */\n.sb-file {{\n margin:.2rem 8px .35rem;\n padding:8px 11px;\n background:rgba({rgb},.07);\n border:1px solid rgba({rgb},.18);\n border-radius:9px;\n font-size:.7rem;\n color:{T['t2']};\n line-height:1.55;\n}}\n.sb-file b{{color:{T['t1']};font-weight:600;}}\n\n/* ── Action buttons (Unload / Clear Chat) ── */\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-act .stButton>button {{\n width:calc(100% - 16px) !important;\n margin:4px 8px !important;\n padding:0 12px !important;\n border-radius:8px !important;\n height:32px !important;\n font-size:.76rem !important;\n font-weight:500 !important;\n text-align:left !important;\n justify-content:flex-start !important;\n border:1px solid {T['b1']} !important;\n background:{T['panel']} !important;\n color:{T['t2']} !important;\n transition:all .12s !important;\n line-height:1 !important;\n}}\ndiv[data-testid=\"stVerticalBlock\"]:has(.rb-sb-root) .sb-act .stButton>button:hover{{\n background:rgba(248,113,113,.1) !important;\n border-color:rgba(248,113,113,.35) !important;\n color:#f87171 !important;\n}}\n\n/* ── Bottom padding ── */\n.sb-bottom-pad{{height:1rem;}}\n</style>\n<div class=\"rb-sidebar\"></div>\n\"\"\", unsafe_allow_html=True)\n\n # ── Widget layer ──────────────────────────────────────────────────────────\n with st.container():\n st.markdown('<div class=\"rb-sb-root\"></div>', unsafe_allow_html=True)\n\n # Small top gap so content doesn't touch the very top of the panel\n st.markdown('<div style=\"height:.5rem\"></div>', unsafe_allow_html=True)\n\n # Navigation\n st.markdown('<span class=\"sb-sec\">Navigation</span>', unsafe_allow_html=True)\n st.markdown('<div class=\"sb-nav\">', unsafe_allow_html=True)\n\n if st.button(\"\\U0001f4c2 Upload & Index\", key=\"sb_upload\",\n use_container_width=True,\n type=\"primary\" if v==\"upload\" else \"secondary\"):\n st.session_state.sidebar_open = False; go(\"upload\")\n\n _cdot = \" \\u25cf\" if v == \"chat\" else \"\"\n if st.button(f\"\\U0001f4ac Chat{_cdot}\", key=\"sb_chat\",\n use_container_width=True,\n type=\"primary\" if v==\"chat\" else \"secondary\",\n disabled=not idx):\n st.session_state.sidebar_open = False; go(\"chat\")\n\n if st.button(\"\\U0001f50d Sources & PDF\", key=\"sb_src\",\n use_container_width=True,\n type=\"primary\" if v==\"sources\" else \"secondary\",\n disabled=not idx):\n st.session_state.sidebar_open = False; go(\"sources\")\n\n st.markdown('</div>', unsafe_allow_html=True)\n\n # Theme swatches\n st.markdown('<div class=\"sb-hr\"></div>', unsafe_allow_html=True)\n st.markdown('<span class=\"sb-sec\">Color Theme</span>', unsafe_allow_html=True)\n st.markdown('<div class=\"sb-sw\">', unsafe_allow_html=True)\n\n _swcols = st.columns(7, gap=\"small\")\n for _i, (_tk, _emoji, _name) in enumerate(SWATCH_DATA):\n with _swcols[_i]:\n if st.button(_emoji, key=f\"sw_{_tk}\", help=_name):\n st.session_state.theme = _tk\n st.session_state.sidebar_open = False\n st.rerun()\n\n st.markdown('</div>', unsafe_allow_html=True)\n\n # Document info + action buttons\n if idx:\n st.markdown('<div class=\"sb-hr\"></div>', unsafe_allow_html=True)\n st.markdown('<span class=\"sb-sec\">Document</span>', unsafe_allow_html=True)\n\n _fn = st.session_state.last_file[:22]+(\"\\u2026\" if len(st.session_state.last_file)>22 else \"\")\n st.markdown(\n f'<div class=\"sb-file\">'\n f'<b>\\U0001f4c4 {_fn}</b><br>'\n f'{st.session_state.pages}p \\u00b7 {st.session_state.chunks} chunks \\u00b7 {st.session_state.file_size}'\n f'</div>',\n unsafe_allow_html=True\n )\n\n # Space between file card and first action button\n st.markdown('<div style=\"height:.25rem\"></div>', unsafe_allow_html=True)\n\n st.markdown('<div class=\"sb-act\">', unsafe_allow_html=True)\n if st.button(\"\\U0001f4c2 Unload PDF\", key=\"sb_unload\",\n use_container_width=True):\n for _k in (\"indexed\",\"step\",\"history\",\"cur_passages\"):\n st.session_state[_k] = False if _k==\"indexed\" else 0 if _k==\"step\" else []\n st.session_state.last_file_bytes=None\n st.session_state.view=\"upload\"\n st.session_state.sidebar_open=False\n st.rerun()\n st.markdown('</div>', unsafe_allow_html=True)\n\n if st.session_state.history:\n st.markdown('<div class=\"sb-act\">', unsafe_allow_html=True)\n if st.button(\"\\U0001f5d1 Clear Chat\", key=\"sb_clear\",\n use_container_width=True):\n st.session_state.history=[]\n st.session_state.cur_passages=[]\n st.session_state.sidebar_open=False\n st.rerun()\n st.markdown('</div>', unsafe_allow_html=True)\n\n st.markdown('<div class=\"sb-bottom-pad\"></div>', unsafe_allow_html=True)\n\n\n# ╔══════════════════════════════════════════════════════════╗\n# ║ VIEW 1: UPLOAD ║\n# ║ FIX: Removed rb-page/rb-center wrapper divs. ║\n# ║ stMainBlockContainer already centers to 860px. ║\n# ║ All content now flows top-to-bottom in the same DOM. ║\n# ╚══════════════════════════════════════════════════════════╝\nif v == \"upload\":\n\n # Hero — rendered as pure HTML block, flows first\n st.markdown(f\"\"\"\n<div class=\"up-hero\">\n <h1>Upload your <em>Textbook</em></h1>\n <p>Drop any PDF — algorithms, CS, math, science — and ResolvaBot will extract, chunk, and index it for AI-powered Q&amp;A.</p>\n <div class=\"feat-row\">\n <span class=\"ftag\">⚡ RAPTOR Index</span>\n <span class=\"ftag\">🔍 BM25 + FAISS</span>\n <span class=\"ftag\">🤖 Llama 3.3 70B</span>\n <span class=\"ftag\">🌲 Hierarchical</span>\n <span class=\"ftag\">📖 Wikipedia fallback</span>\n </div>\n</div>\n\"\"\", unsafe_allow_html=True)\n\n # API keys (compact, only when no key)\n if not (has_groq or has_openai):\n with st.expander(\"🔑 Set LLM API Key (required for answers)\", expanded=True):\n k1, k2 = st.columns(2)\n with k1:\n gk = st.text_input(\"Groq Key (free)\", value=st.session_state.groq_key,\n type=\"password\", placeholder=\"gsk_...\", help=\"console.groq.com\")\n if gk != st.session_state.groq_key:\n st.session_state.groq_key=gk; os.environ[\"GROQ_API_KEY\"]=gk; st.rerun()\n with k2:\n ok = st.text_input(\"OpenAI Key\", value=st.session_state.openai_key,\n type=\"password\", placeholder=\"sk-...\")\n if ok != st.session_state.openai_key:\n st.session_state.openai_key=ok; os.environ[\"OPENAI_API_KEY\"]=ok; st.rerun()\n\n # Two-column layout — placed AFTER hero in the same flow\n lc, rc = st.columns([1.1, 0.9], gap=\"medium\")\n\n with lc:\n st.markdown(\"\"\"<div class=\"up-card\">\n <div class=\"up-card-head\">\n <div class=\"up-card-ico\">📄</div>\n <div><div class=\"up-card-lbl\">PDF Textbook</div><div class=\"up-card-sub\">Drag & drop or browse</div></div>\n </div>\n <div class=\"up-card-body\">\"\"\", unsafe_allow_html=True)\n\n if st.session_state.indexed:\n st.markdown(f\"\"\"<div class=\"file-ok\">\n <div style=\"font-size:1.4rem\">📄</div>\n <div style=\"flex:1;min-width:0\">\n <div class=\"file-ok-n\">{st.session_state.last_file}</div>\n <div class=\"file-ok-m\">{st.session_state.file_size} · {st.session_state.pages} pages</div>\n </div>\n <div style=\"font-size:1.2rem\">✅</div>\n</div>\"\"\", unsafe_allow_html=True)\n else:\n st.markdown(\"\"\"<div class=\"dz\">\n <div class=\"dz-ico\">☁️</div>\n <div class=\"dz-t\">Drop PDF here</div>\n <div class=\"dz-s\">or <span class=\"dz-br\">browse files</span></div>\n</div>\"\"\", unsafe_allow_html=True)\n\n uploaded = st.file_uploader(\"\", type=[\"pdf\"],\n label_visibility=\"collapsed\",\n key=\"pdf_uploader\")\n st.markdown('</div></div>', unsafe_allow_html=True)\n\n if st.session_state.indexed:\n if st.button(\"💬 Start Chatting →\", type=\"primary\", use_container_width=True):\n go(\"chat\")\n\n with rc:\n STEPS = [\n (\"📝\",\"Extract Text\",\"PyMuPDF page reader\"),\n (\"✂️\",\"Chunk Text\",\"NLTK sentence chunking\"),\n (\"🌲\",\"RAPTOR Index\",\"GMM clustering hierarchy\"),\n (\"⚡\",\"FAISS Store\",\"Dense vector embeddings\"),\n (\"📑\",\"BM25 Index\",\"Keyword inverted index\"),\n ]\n s = st.session_state.step\n\n st.markdown(\"\"\"<div class=\"up-card\">\n <div class=\"up-card-head\">\n <div class=\"up-card-ico\">⚙️</div>\n <div><div class=\"up-card-lbl\">Indexing Pipeline</div><div class=\"up-card-sub\">5-step workflow</div></div>\n </div>\n <div class=\"up-card-body\"><div class=\"pipe\">\"\"\", unsafe_allow_html=True)\n\n step_placeholders = []\n for i, (icon, name, desc) in enumerate(STEPS, 1):\n is_last = (i == len(STEPS))\n if s >= i: sc,nc,bc,bl,nl = \"done\",\"pn-d\",\"pbdg-d\",\"✓ Done\",\"dn\"\n elif s==i-1 and uploaded and not st.session_state.indexed:\n sc,nc,bc,bl,nl = \"actv\",\"pn-a\",\"pbdg-a\",\"Running…\",\"av\"\n else: sc,nc,bc,bl,nl = \"\",\"pn-i\",\"pbdg-i\",str(i),\"\"\n conn = \"\" if is_last else f'<div class=\"pconn {\"done\" if s>=i else \"\"}\"></div>'\n ph = st.empty()\n ph.markdown(f\"\"\"<div class=\"pstep {sc}\">\n <div class=\"pn {nc}\">{icon if s>=i else str(i)}</div>\n <div class=\"pi\"><div class=\"pn-lbl {nl}\">{name}</div><div class=\"pd\">{desc}</div></div>\n <div class=\"pbdg {bc}\">{bl}</div>\n</div>{conn}\"\"\", unsafe_allow_html=True)\n step_placeholders.append(ph)\n\n st.markdown('</div></div></div>', unsafe_allow_html=True)\n\n # Meta stats card\n if st.session_state.indexed:\n fname = st.session_state.last_file[:36]+(\"…\" if len(st.session_state.last_file)>36 else \"\")\n st.markdown(f\"\"\"\n<div class=\"up-card\" style=\"margin-top:.9rem\">\n <div class=\"up-card-head\">\n <div class=\"up-card-ico\">📊</div>\n <div style=\"flex:1\"><div class=\"up-card-lbl\">Document Summary</div><div class=\"up-card-sub\">Indexing complete</div></div>\n {pill(\"✓ Ready to Chat\",\"pg\")}\n </div>\n <div class=\"up-card-body\">\n <div class=\"meta-g\">\n <div class=\"meta-i\"><div class=\"meta-v\">{st.session_state.pages}</div><div class=\"meta-k\">Pages</div></div>\n <div class=\"meta-i\"><div class=\"meta-v\">{st.session_state.chunks}</div><div class=\"meta-k\">Chunks</div></div>\n <div class=\"meta-i\"><div class=\"meta-v\">{st.session_state.nodes}</div><div class=\"meta-k\">Nodes</div></div>\n <div class=\"meta-i\"><div class=\"meta-v\">{st.session_state.file_size}</div><div class=\"meta-k\">Size</div></div>\n <div class=\"meta-file\">\n <div style=\"font-size:1.3rem\">📄</div>\n <div style=\"flex:1;min-width:0\">\n <div class=\"mfn\">{fname}</div>\n <div class=\"mfm\">SBERT all-MiniLM-L6-v2 · 384-dim</div>\n </div>\n </div>\n </div>\n <div class=\"ready-bar\">\n <div class=\"rdot\"></div>\n <div><div class=\"rtxt\">Ready to chat!</div><div class=\"rsub\">{st.session_state.chunks} chunks · {st.session_state.pages} pages</div></div>\n </div>\n </div>\n</div>\n\"\"\", unsafe_allow_html=True)\n\n # Indexing with real-time step updates\n if uploaded and not st.session_state.indexed:\n def update_step(i, placeholders, steps_data, current_step):\n for j, (icon, name, desc) in enumerate(steps_data, 1):\n is_last = (j == len(steps_data))\n if current_step >= j: sc,nc,bc,bl,nl = \"done\",\"pn-d\",\"pbdg-d\",\"✓ Done\",\"dn\"\n elif current_step==j-1: sc,nc,bc,bl,nl = \"actv\",\"pn-a\",\"pbdg-a\",\"Running…\",\"av\"\n else: sc,nc,bc,bl,nl = \"\",\"pn-i\",\"pbdg-i\",str(j),\"\"\n conn = \"\" if is_last else f'<div class=\"pconn {\"done\" if current_step>=j else \"\"}\"></div>'\n placeholders[j-1].markdown(f\"\"\"<div class=\"pstep {sc}\">\n <div class=\"pn {nc}\">{icon if current_step>=j else str(j)}</div>\n <div class=\"pi\"><div class=\"pn-lbl {nl}\">{name}</div><div class=\"pd\">{desc}</div></div>\n <div class=\"pbdg {bc}\">{bl}</div>\n</div>{conn}\"\"\", unsafe_allow_html=True)\n\n prog = st.progress(0)\n try:\n from extraction import extract_text_from_pdf_bytes\n from chunking import chunk_pages\n from raptor_index import build_raptor_index\n from vector_store import VectorStore\n from retrieval import BM25Index, HybridRetriever\n\n raw = uploaded.read()\n sz = f\"{len(raw)/1024:.0f} KB\" if len(raw)<1024*1024 else f\"{len(raw)/1024/1024:.1f} MB\"\n st.session_state.file_size=sz; st.session_state.last_file_bytes=raw\n\n st.session_state.step=0; update_step(0,step_placeholders,STEPS,0)\n prog.progress(5, text=\"📄 Extracting text…\")\n try: pages = extract_text_from_pdf_bytes(raw)\n except ValueError as e: st.error(str(e)); st.stop()\n if not any(pg[\"text\"].strip() for pg in pages):\n st.error(\"No readable text found.\"); st.stop()\n\n st.session_state.step=1; update_step(0,step_placeholders,STEPS,1)\n prog.progress(22, text=\"✂️ Chunking text…\")\n chunks = chunk_pages(pages, chunk_size=100)\n if not chunks: st.error(\"Chunking returned 0 results.\"); st.stop()\n\n st.session_state.step=2; update_step(0,step_placeholders,STEPS,2)\n prog.progress(42, text=\"🌲 Building RAPTOR…\")\n nodes = build_raptor_index(chunks, max_levels=2, use_llm=False)\n\n st.session_state.step=3; update_step(0,step_placeholders,STEPS,3)\n prog.progress(65, text=\"⚡ Indexing FAISS…\")\n vs = VectorStore(); vs.add_nodes(nodes)\n\n st.session_state.step=4; update_step(0,step_placeholders,STEPS,4)\n prog.progress(83, text=\"📑 Building BM25…\")\n bm25 = BM25Index()\n bm25.build([{\"chunk_id\":n.node_id,\"page\":n.page,\"level\":n.level,\"text\":n.text} for n in nodes])\n\n st.session_state.step=5; prog.progress(100, text=\"✅ Done!\")\n from retrieval import HybridRetriever\n st.session_state.retriever = HybridRetriever(bm25, vs)\n st.session_state.indexed = True\n st.session_state.last_file = uploaded.name\n st.session_state.pages = len(pages)\n st.session_state.chunks = len(chunks)\n st.session_state.nodes = len(nodes)\n st.rerun()\n except Exception as e:\n import traceback; st.error(f\"Error: {e}\")\n with st.expander(\"Details\"): st.code(traceback.format_exc())\n\n# ╔══════════════════════════════════════════════════════════╗\n# ║ VIEW 2: CHAT (FIX 5) ║\n# ╚══════════════════════════════════════════════════════════╝\nelif v == \"chat\":\n if not st.session_state.indexed:\n st.warning(\"Upload and index a PDF first.\")\n if st.button(\"← Go to Upload\"): go(\"upload\")\n else:\n llm_lbl = (\"Groq · Llama 3.3 70B\" if has_groq\n else \"OpenAI · GPT-3.5\" if has_openai\n else \"Wikipedia fallback\")\n n = st.session_state.chunks\n\n st.markdown(f'<div class=\"chat-outer\">', unsafe_allow_html=True)\n st.markdown(f\"\"\"\n<div class=\"chat-shell\">\n <div class=\"chat-top\">\n <div>\n <div class=\"chat-tname\">💬 {st.session_state.last_file}</div>\n <div class=\"chat-tmeta\">{n} chunks indexed · {llm_lbl}</div>\n </div>\n <div style=\"display:flex;gap:6px;align-items:center\">\n {pill(\"🟢 Groq\",\"pg\") if has_groq else pill(\"⚠ No LLM\",\"py\")}\n {pill(f\"{n} chunks\",\"ps\")}\n </div>\n </div>\n <div class=\"chat-msgs\" id=\"chatbox\">\n\"\"\", unsafe_allow_html=True)\n\n if not st.session_state.history:\n st.markdown(\"\"\"<div class=\"chat-empty\">\n <div class=\"ce-ico\">💬</div>\n <div class=\"ce-t\">Start a conversation</div>\n <div class=\"ce-s\">Ask anything about your textbook. I'll retrieve the most relevant passages and synthesize a complete, structured answer.</div>\n</div>\"\"\", unsafe_allow_html=True)\n\n for msg in st.session_state.history:\n if msg[\"role\"] == \"user\":\n st.markdown(f\"\"\"\n<div class=\"mrow user\">\n <div class=\"mav mav-u\">🙋</div>\n <div class=\"mbody\">\n <div class=\"bub-u\">{html.escape(msg[\"content\"])}</div>\n </div>\n</div>\n\"\"\", unsafe_allow_html=True)\n else:\n err = msg.get(\"llm_error\")\n if err:\n st.markdown(f'<div style=\"background:rgba(248,113,113,.08);border:1px solid rgba(248,113,113,.2);border-radius:8px;padding:8px 12px;font-size:.72rem;color:#f87171;margin-bottom:6px\">⚠ {html.escape(str(err))}</div>', unsafe_allow_html=True)\n sp = spill(msg.get(\"source\",\"\")); mp = mpill(msg.get(\"model\",\"\"),msg.get(\"source\",\"\"))\n\n st.markdown('<div class=\"mrow\"><div class=\"mav mav-b\">🤖</div><div class=\"mbody\" style=\"max-width:88%\"><div class=\"bub-b\">', unsafe_allow_html=True)\n st.markdown(msg[\"content\"])\n st.markdown(f'</div><div class=\"mmeta\">{sp}&nbsp;{mp}</div></div></div>', unsafe_allow_html=True)\n\n if msg.get(\"passages\"):\n with st.expander(f\"📋 {len(msg['passages'])} source passages\", expanded=False):\n for j, r in enumerate(msg[\"passages\"][:6], 1):\n pg = f\"Page {r['page']}\" if r.get(\"page\",-1)>0 else \"Summary\"\n scr = r.get(\"rrf_score\",0)\n txt = r[\"text\"]\n cr = sum(1 for c in txt if c in \"{}()<>;=\") / max(len(txt),1)\n st.markdown(f'<div class=\"pcard\"><div class=\"pcard-h\"><span class=\"pcard-n\">#{j} · {pg}</span><span class=\"pcard-s\">{scr:.4f}</span></div>', unsafe_allow_html=True)\n if cr > 0.06: st.code(txt, language=detect_lang(txt))\n else: st.markdown(f'<div class=\"pcard-b\">{html.escape(txt)}</div>', unsafe_allow_html=True)\n st.markdown(\"</div>\", unsafe_allow_html=True)\n\n st.markdown('</div>', unsafe_allow_html=True)\n\n st.markdown(f\"\"\"\n<div style=\"border-top:1px solid {T['b1']};padding:.85rem 1.2rem;background:{T['card']};flex-shrink:0;\">\n\"\"\", unsafe_allow_html=True)\n\n question = st.chat_input(\"Ask anything about the textbook…\")\n\n st.markdown(f'<div style=\"font-size:.64rem;color:{T[\"t3\"]};text-align:center;margin-top:.3rem\">Enter to send</div>', unsafe_allow_html=True)\n st.markdown('</div></div></div>', unsafe_allow_html=True)\n\n if question:\n st.session_state.history.append({\"role\":\"user\",\"content\":question})\n with st.spinner(\"\"):\n from question_answering import answer_question\n results = st.session_state.retriever.retrieve(question, top_k=8)\n result = answer_question(question, results, use_llm=True)\n st.session_state.cur_passages = results\n st.session_state.history.append({\n \"role\":\"assistant\",\"content\":result[\"answer\"],\n \"source\":result[\"source\"],\"model\":result[\"model\"],\n \"llm_error\":result.get(\"llm_error\"),\"passages\":results,\n })\n st.rerun()\n\n# ╔══════════════════════════════════════════════════════════╗\n# ║ VIEW 3: SOURCES & PDF (FIX 6) ║\n# ║ FIX: Removed fixed-position outer container. ║\n# ║ Content now flows directly in stMainBlockContainer. ║\n# ║ PDF iframe uses full column width and tall height. ║\n# ╚══════════════════════════════════════════════════════════╝\nelif v == \"sources\":\n if not st.session_state.indexed:\n st.warning(\"Upload and index a PDF first.\")\n if st.button(\"← Go to Upload\"): go(\"upload\")\n else:\n passages = (\n st.session_state.history[-1].get(\"passages\",[])\n if st.session_state.history and st.session_state.history[-1][\"role\"]==\"assistant\"\n else st.session_state.cur_passages\n )\n fname = st.session_state.last_file or \"document.pdf\"\n n_pages = st.session_state.pages\n\n # Sources page: PDF viewer takes full width, passages shown below\n st.markdown(f\"\"\"\n<div style=\"background:{T['card']};border:1px solid {T['b1']};border-radius:16px;\n overflow:hidden;box-shadow:{T['shd']};\">\n <div class=\"src-head\">\n <span style=\"font-size:1rem\">📄</span>\n <span class=\"src-htitle\">PDF Viewer · {n_pages} pages</span>\n <span style=\"margin-left:auto;font-size:.63rem;color:{T['t3']};\n font-family:'JetBrains Mono',monospace\">{fname}</span>\n </div>\n\"\"\", unsafe_allow_html=True)\n\n if st.session_state.last_file_bytes:\n b64 = base64.b64encode(st.session_state.last_file_bytes).decode()\n st.markdown(f\"\"\"\n<iframe\n src=\"data:application/pdf;base64,{b64}#toolbar=1&navpanes=0&scrollbar=1&view=FitH\"\n width=\"100%\"\n height=\"720\"\n style=\"display:block;border:none;width:100%;\"\n></iframe>\n\"\"\", unsafe_allow_html=True)\n else:\n st.markdown(\n f'<div style=\"display:flex;align-items:center;justify-content:center;'\n f'padding:4rem;flex-direction:column;gap:.5rem;opacity:.4\">'\n f'<div style=\"font-size:2.5rem\">📄</div>'\n f'<div style=\"font-size:.82rem;color:{T[\"t3\"]}\">PDF not available</div></div>',\n unsafe_allow_html=True\n )\n st.markdown('</div>', unsafe_allow_html=True)"}}}}