{"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'{text}'\n\nst.markdown(f\"\"\"\n\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
\n
\n
\n
πŸ“š
\n
ResolvaBot LLM
\n
\n
\n Β·\n {page_labels.get(v,'')}\n
\n
\n {llm_badge} {idx_badge}\n
\n
\n
\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 '
',\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\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('
', unsafe_allow_html=True)\n\n st.markdown(f\"\"\"\n\n
\n\"\"\", unsafe_allow_html=True)\n\n # ── Widget layer ──────────────────────────────────────────────────────────\n with st.container():\n st.markdown('
', unsafe_allow_html=True)\n\n # Small top gap so content doesn't touch the very top of the panel\n st.markdown('
', unsafe_allow_html=True)\n\n # Navigation\n st.markdown('Navigation', unsafe_allow_html=True)\n st.markdown('
', 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('
', unsafe_allow_html=True)\n\n # Theme swatches\n st.markdown('
', unsafe_allow_html=True)\n st.markdown('Color Theme', unsafe_allow_html=True)\n st.markdown('
', 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('
', unsafe_allow_html=True)\n\n # Document info + action buttons\n if idx:\n st.markdown('
', unsafe_allow_html=True)\n st.markdown('Document', 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'
'\n f'\\U0001f4c4 {_fn}
'\n f'{st.session_state.pages}p \\u00b7 {st.session_state.chunks} chunks \\u00b7 {st.session_state.file_size}'\n f'
',\n unsafe_allow_html=True\n )\n\n # Space between file card and first action button\n st.markdown('
', unsafe_allow_html=True)\n\n st.markdown('
', 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('
', unsafe_allow_html=True)\n\n if st.session_state.history:\n st.markdown('
', 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('
', unsafe_allow_html=True)\n\n st.markdown('
', 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
\n

Upload your Textbook

\n

Drop any PDF β€” algorithms, CS, math, science β€” and ResolvaBot will extract, chunk, and index it for AI-powered Q&A.

\n
\n ⚑ RAPTOR Index\n πŸ” BM25 + FAISS\n πŸ€– Llama 3.3 70B\n 🌲 Hierarchical\n πŸ“– Wikipedia fallback\n
\n
\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(\"\"\"
\n
\n
πŸ“„
\n
PDF Textbook
Drag & drop or browse
\n
\n
\"\"\", unsafe_allow_html=True)\n\n if st.session_state.indexed:\n st.markdown(f\"\"\"
\n
πŸ“„
\n
\n
{st.session_state.last_file}
\n
{st.session_state.file_size} Β· {st.session_state.pages} pages
\n
\n
βœ…
\n
\"\"\", unsafe_allow_html=True)\n else:\n st.markdown(\"\"\"
\n
☁️
\n
Drop PDF here
\n
or browse files
\n
\"\"\", unsafe_allow_html=True)\n\n uploaded = st.file_uploader(\"\", type=[\"pdf\"],\n label_visibility=\"collapsed\",\n key=\"pdf_uploader\")\n st.markdown('
', 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(\"\"\"
\n
\n
βš™οΈ
\n
Indexing Pipeline
5-step workflow
\n
\n
\"\"\", 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'
=i else \"\"}\">
'\n ph = st.empty()\n ph.markdown(f\"\"\"
\n
{icon if s>=i else str(i)}
\n
{name}
{desc}
\n
{bl}
\n
{conn}\"\"\", unsafe_allow_html=True)\n step_placeholders.append(ph)\n\n st.markdown('
', 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
\n
\n
πŸ“Š
\n
Document Summary
Indexing complete
\n {pill(\"βœ“ Ready to Chat\",\"pg\")}\n
\n
\n
\n
{st.session_state.pages}
Pages
\n
{st.session_state.chunks}
Chunks
\n
{st.session_state.nodes}
Nodes
\n
{st.session_state.file_size}
Size
\n
\n
πŸ“„
\n
\n
{fname}
\n
SBERT all-MiniLM-L6-v2 Β· 384-dim
\n
\n
\n
\n
\n
\n
Ready to chat!
{st.session_state.chunks} chunks Β· {st.session_state.pages} pages
\n
\n
\n
\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'
=j else \"\"}\">
'\n placeholders[j-1].markdown(f\"\"\"
\n
{icon if current_step>=j else str(j)}
\n
{name}
{desc}
\n
{bl}
\n
{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'
', unsafe_allow_html=True)\n st.markdown(f\"\"\"\n
\n
\n
\n
πŸ’¬ {st.session_state.last_file}
\n
{n} chunks indexed Β· {llm_lbl}
\n
\n
\n {pill(\"🟒 Groq\",\"pg\") if has_groq else pill(\"⚠ No LLM\",\"py\")}\n {pill(f\"{n} chunks\",\"ps\")}\n
\n
\n
\n\"\"\", unsafe_allow_html=True)\n\n if not st.session_state.history:\n st.markdown(\"\"\"
\n
πŸ’¬
\n
Start a conversation
\n
Ask anything about your textbook. I'll retrieve the most relevant passages and synthesize a complete, structured answer.
\n
\"\"\", unsafe_allow_html=True)\n\n for msg in st.session_state.history:\n if msg[\"role\"] == \"user\":\n st.markdown(f\"\"\"\n
\n
πŸ™‹
\n
\n
{html.escape(msg[\"content\"])}
\n
\n
\n\"\"\", unsafe_allow_html=True)\n else:\n err = msg.get(\"llm_error\")\n if err:\n st.markdown(f'
⚠ {html.escape(str(err))}
', unsafe_allow_html=True)\n sp = spill(msg.get(\"source\",\"\")); mp = mpill(msg.get(\"model\",\"\"),msg.get(\"source\",\"\"))\n\n st.markdown('
πŸ€–
', unsafe_allow_html=True)\n st.markdown(msg[\"content\"])\n st.markdown(f'
{sp} {mp}
', 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'
#{j} Β· {pg}{scr:.4f}
', unsafe_allow_html=True)\n if cr > 0.06: st.code(txt, language=detect_lang(txt))\n else: st.markdown(f'
{html.escape(txt)}
', unsafe_allow_html=True)\n st.markdown(\"
\", unsafe_allow_html=True)\n\n st.markdown('
', unsafe_allow_html=True)\n\n st.markdown(f\"\"\"\n
\n\"\"\", unsafe_allow_html=True)\n\n question = st.chat_input(\"Ask anything about the textbook…\")\n\n st.markdown(f'
Enter to send
', unsafe_allow_html=True)\n st.markdown('
', 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
\n
\n πŸ“„\n PDF Viewer Β· {n_pages} pages\n {fname}\n
\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\n\"\"\", unsafe_allow_html=True)\n else:\n st.markdown(\n f'
'\n f'
πŸ“„
'\n f'
PDF not available
',\n unsafe_allow_html=True\n )\n st.markdown('
', unsafe_allow_html=True)"}}}}