# app.py β€” DocQuest Β· Personal Knowledge RAG with Self-Healing import re from collections import Counter import streamlit as st import streamlit.components.v1 as components from core.ingestion import DocumentLoader from core.chunking import ChunkingPipeline from core.embeddings import EmbeddingStore from core.vector_store import VectorStoreManager from core.generator import ChatGenerator from config import ( check_ollama_server, DEFAULT_MODEL, AVAILABLE_MODELS, EMBED_MODEL, IS_HF_SPACES, ) from core.self_healing import run_healing_pipeline, FeedbackStore, FeedbackStore st.set_page_config( page_title="DocQuest Β· Personal Knowledge RAG", page_icon="πŸ“š", layout="wide", initial_sidebar_state="expanded", ) check_ollama_server() # ── Styles ────────────────────────────────────────────────────────────────────── def _inject_styles(): st.markdown( """ """, unsafe_allow_html=True, ) # ── Session state ──────────────────────────────────────────────────────────────── def _initialize_state(): defaults = { "documents": [], "vector_store": None, "chat_history": [], "processing_status": None, "processing_message": None, "processing_message_type": None, "selected_model": DEFAULT_MODEL, "selected_vector_db": "Chroma", "uploaded_files": [], "clear_counter": 0, "starter_questions": [], # shown when chat is empty "_saved_starter_questions": [], # mirror so New Chat can restore them "pending_question": None, # set by chip click, consumed by input handler "feedback_store": None, # FeedbackStore instance (Cap. 6) } for key, value in defaults.items(): if key not in st.session_state: st.session_state[key] = value def _clear_all(): """Full reset β€” wipes documents, vector store, chat, and uploads.""" st.session_state.documents = [] st.session_state.vector_store = None st.session_state.chat_history = [] st.session_state.processing_status = None st.session_state.processing_message = None st.session_state.processing_message_type = None st.session_state.uploaded_files = [] # Wipe both the visible list and the saved mirror st.session_state.starter_questions = [] st.session_state["_saved_starter_questions"] = [] st.session_state.pending_question = None st.session_state.selected_model = DEFAULT_MODEL st.session_state.selected_vector_db = "Chroma" st.session_state.feedback_store = None # reset feedback on full clear st.session_state.clear_counter += 1 def _rerun(): if hasattr(st, "rerun"): st.rerun() elif hasattr(st, "experimental_rerun"): st.experimental_rerun() # ── Helpers ────────────────────────────────────────────────────────────────────── def _excerpt_text(text: str, max_chars: int = 260) -> str: cleaned = re.sub(r"[#\*`\_\[\]()>~\-]+", "", text) cleaned = re.sub(r"\s+", " ", cleaned).strip() return cleaned[:max_chars].rstrip() + "…" if len(cleaned) > max_chars else cleaned def _score_to_pct(score: float) -> int: pct = round((1.0 - score / 2.0) * 100) return max(0, min(100, pct)) def _get_model_display_name(model_id: str) -> str: """Return a short friendly name for a model ID.""" return AVAILABLE_MODELS.get(model_id, model_id) if isinstance(AVAILABLE_MODELS, dict) else model_id def _generate_starter_questions(vector_store, generator) -> list[str]: """ Generate 3 questions grounded exclusively in the *just-indexed* content. Pulls diverse chunks via multiple seed queries so questions reflect the actual document, not any prior session. """ try: seed_queries = [ "main topic subject overview", "key details facts data", "conclusions findings purpose", ] seen_ids: set = set() excerpts: list[str] = [] for seed in seed_queries: results = vector_store.search(seed, k=2) for doc, _ in results: cid = doc.metadata.get("chunk_id") or doc.metadata.get("source") or id(doc) if cid not in seen_ids: seen_ids.add(cid) cleaned = re.sub(r"\s+", " ", doc.page_content).strip() excerpts.append(cleaned[:500]) if len(excerpts) >= 5: break if not excerpts: return [] combined = "\n\n---\n\n".join(excerpts[:5]) system = ( "You generate short, specific questions that a user might ask about a document. " "You must base your questions ONLY on the excerpts provided. " "Do NOT ask generic questions about document systems, file formats, or software. " "Every question must be answerable from the excerpt text." ) user = ( f"Based ONLY on these excerpts from the document, generate exactly 3 short " f"questions a user might ask about the content. " f"Return only the 3 questions, one per line, no numbering, no bullet points.\n\n" f"{combined}" ) result = ( generator._call_hf(system=system, user=user) if IS_HF_SPACES else generator._call_ollama(system=system, user=user) ) lines = [ q.strip().lstrip("β€’-123456789. ") for q in result.strip().split("\n") if q.strip() ] return [q for q in lines if len(q) > 10][:3] except Exception: return [] # ── Document processing ────────────────────────────────────────────────────────── def _load_documents(uploaded_files): loader = DocumentLoader() chunker = ChunkingPipeline() all_chunks = [] progress = st.progress(0.0) for idx, uploaded_file in enumerate(uploaded_files): result = loader.load_file(uploaded_file) chunks = chunker.split(result["text"], metadata=result["metadata"]) all_chunks.extend(chunks) progress.progress((idx + 1) / len(uploaded_files)) return all_chunks def _create_vector_store(documents, vector_db): embedder = EmbeddingStore(model_name=EMBED_MODEL) return VectorStoreManager.create_store( vector_db, documents=documents, embedding_function=embedder.embeddings, ) # ── Sidebar ────────────────────────────────────────────────────────────────────── def _render_sidebar(): with st.sidebar: # Global sidebar styling st.markdown(""" """, unsafe_allow_html=True) # Header st.markdown("""
DocQuest
""", unsafe_allow_html=True) # Subtitle st.markdown( "", unsafe_allow_html=True ) # ── Model selector with friendly names ──────────────────────────────── st.markdown("#### Language Model") if isinstance(AVAILABLE_MODELS, dict): friendly_names = list(AVAILABLE_MODELS.keys()) model_ids = list(AVAILABLE_MODELS.values()) if st.session_state.selected_model not in model_ids: st.session_state.selected_model = model_ids[0] current_idx = model_ids.index(st.session_state.selected_model) chosen_name = st.selectbox( "Language model", friendly_names, index=current_idx, help="Model used for answer generation.", label_visibility="collapsed", ) st.session_state.selected_model = AVAILABLE_MODELS[chosen_name] st.markdown( f"

{st.session_state.selected_model}

", unsafe_allow_html=True, ) else: if st.session_state.selected_model not in AVAILABLE_MODELS: st.session_state.selected_model = DEFAULT_MODEL st.session_state.selected_model = st.selectbox( "Language model", AVAILABLE_MODELS, index=AVAILABLE_MODELS.index(st.session_state.selected_model), help="Model used for answer generation.", label_visibility="collapsed", ) # ── Vector DB (hidden in HF Spaces) ────────────────────────────────── if not IS_HF_SPACES: st.markdown("#### Vector Database") st.session_state.selected_vector_db = st.selectbox( "Vector database", ["Chroma", "Pinecone"], index=["Chroma", "Pinecone"].index(st.session_state.selected_vector_db), help="Where embeddings are stored.", label_visibility="collapsed", ) # ── File uploader ───────────────────────────────────────────────────── st.markdown("#### Upload Documents") if st.session_state.processing_status != "success": if not st.session_state.uploaded_files: st.markdown( """ """, unsafe_allow_html=True, ) uploaded = st.file_uploader( "Upload documents", type=["pdf", "docx", "pptx", "txt", "md"], accept_multiple_files=True, help="PDF, DOCX, PPTX, TXT, MD β€” max 25 MB each", label_visibility="collapsed", key=f"file_uploader_{st.session_state.clear_counter}", ) if uploaded: st.session_state.uploaded_files = uploaded _rerun() # ── Show selected-file pills when files chosen but not yet processed ── if st.session_state.uploaded_files and st.session_state.processing_status != "success": for f in st.session_state.uploaded_files: ext = f.name.rsplit(".", 1)[-1].upper() if "." in f.name else "FILE" size_kb = round(f.size / 1024, 1) st.markdown( f"
" f"
" f"
" f"
" f"
" f"
" f"
{f.name}
" f"
" f"{ext} Β· {size_kb} KB
" f"
" f"
Ready
" f"
", unsafe_allow_html=True, ) # ── Process button ──────────────────────────────────────────────────── st.markdown("
", unsafe_allow_html=True) if st.button("β–Ά Process & Index", type="primary", use_container_width=True): if not st.session_state.uploaded_files: st.warning("Upload at least one document first.") else: # Wipe everything doc-specific BEFORE processing so stale # questions never survive into the new session even briefly st.session_state.starter_questions = [] st.session_state.chat_history = [] st.session_state.vector_store = None st.session_state.documents = [] st.session_state.pending_question = None with st.spinner("Chunking and embedding…"): try: chunks = _load_documents(st.session_state.uploaded_files) store = _create_vector_store( chunks, st.session_state.selected_vector_db ) st.session_state.vector_store = store st.session_state.documents = chunks st.session_state.processing_status = "success" st.session_state.processing_message = ( f"βœ… Indexed {len(chunks)} chunks from " f"{len(st.session_state.uploaded_files)} file(s)." ) st.session_state.processing_message_type = "success" st.session_state.chat_history = [] st.session_state.pending_question = None # Always wipe stale questions BEFORE generating # new ones β€” if generation throws or returns [], # the old doc's questions are already gone. st.session_state.starter_questions = [] st.session_state["_saved_starter_questions"] = [] generator = ChatGenerator(model=st.session_state.selected_model) questions = _generate_starter_questions(store, generator) # Store in both the visible list and the saved mirror. # The mirror lets _new_chat() restore chips without # re-querying the LLM. st.session_state.starter_questions = questions st.session_state["_saved_starter_questions"] = list(questions) except Exception as exc: st.session_state.processing_status = "error" st.session_state.processing_message = f"Processing failed: {exc}" st.session_state.processing_message_type = "error" # Ensure stale questions are never shown on error st.session_state.starter_questions = [] st.session_state["_saved_starter_questions"] = [] # Clear all ────────────────────────────────────────────── if st.button("Clear all", type="secondary", use_container_width=True): _clear_all() _rerun() # ── Indexed file cards ──────────────────────────────────────────────── # Guard: skip during chip-click reruns to prevent double render. # pending_question is set by chip click and consumed in _render_chat_interface. if ( st.session_state.uploaded_files and st.session_state.processing_status == "success" and not st.session_state.get("pending_question") ): st.markdown( "
" "Indexed Files
", unsafe_allow_html=True, ) chunk_counts = Counter( doc.metadata.get("filename", "unknown") for doc in st.session_state.documents ) seen: set = set() for file in st.session_state.uploaded_files: if file.name in seen: continue seen.add(file.name) n_chunks = chunk_counts.get(file.name, "β€”") size_kb = round(file.size / 1024, 1) ext = file.name.rsplit(".", 1)[-1].upper() if "." in file.name else "FILE" st.markdown( f"
" f"
" f"
" f"
" f"
" f"
" f"
{file.name}
" f"
" f"{ext} Β· {n_chunks} chunks Β· {size_kb} KB
" f"
" f"
βœ“
" f"
", unsafe_allow_html=True, ) # ── Demo banner ────────────────────────────────────────────────────────────────── def _render_demo_banner(): if IS_HF_SPACES: st.markdown( """
Demo mode β€” documents held in memory for this session only. First embedding may take ~30s.
""", unsafe_allow_html=True, ) # ── Step indicator ──────────────────────────────────────────────────────────────── def _render_steps(): # Suppress during chip-click intermediate rerun to avoid visible flash if st.session_state.get("pending_question"): return has_files = bool(st.session_state.uploaded_files) has_index = st.session_state.processing_status == "success" has_chat = bool(st.session_state.chat_history) upload_cls = "done" if has_files else ("active" if not has_files else "") process_cls = "done" if has_index else ("active" if has_files else "") chat_cls = "done" if has_chat else ("active" if has_index else "") st.markdown( f"""
1 Β· UploadAdd your docs
2 Β· ProcessChunk & embed
3 Β· ChatAsk questions
""", unsafe_allow_html=True, ) # ── Hero (shown only before indexing) ──────────────────────────────────────────── def _render_hero(): if st.session_state.processing_status == "success": return st.markdown( """
DQ
DocQuest

Transform documents into intelligent conversations

Upload PDFs, DOCX, Markdown, and TXT files to create a searchable knowledge base β€” then chat with cited answers.

""", unsafe_allow_html=True, ) col1, col2, col3, col4 = st.columns(4) cards = [ ("πŸ“€", "Upload & index", "PDF, DOCX, MD, TXT β€” up to 25 MB each"), ("πŸ”", "Vector search", "Semantic embeddings for accurate retrieval"), ("πŸ’¬", "Chat with sources", "Answers with inline citations and excerpts"), ("πŸ”’", "Private by default", "Session-only β€” nothing stored permanently"), ] for col, (icon, title, desc) in zip([col1, col2, col3, col4], cards): with col: st.markdown( f"""
{icon}

{title}

{desc}

""", unsafe_allow_html=True, ) # ── Indexed summary (shown instead of hero after indexing) ─────────────────────── def _render_index_summary(): if st.session_state.processing_status != "success": return # Suppress during chip-click intermediate rerun to avoid visible flash if st.session_state.get("pending_question"): return n_docs = len(st.session_state.uploaded_files) n_chunks = len(st.session_state.documents) names = ", ".join(f.name for f in st.session_state.uploaded_files[:2]) if n_docs > 2: names += f" +{n_docs - 2} more" # Topbar breadcrumb row st.markdown( f"""
Knowledge base β€Ί {names}
{n_docs} doc{'s' if n_docs > 1 else ''} indexed Β· {n_chunks} chunks
""", unsafe_allow_html=True, ) # ── Chat interface ──────────────────────────────────────────────────────────────── def _render_source_card(source: dict): pct = source["match_pct"] chunk_raw = source.get("chunk_id", "β€”") # Chunk label is now secondary β€” rendered after filename in muted style chunk_label = f"chunk {chunk_raw}" if chunk_raw not in ("β€”", "", None) else "" st.markdown( f"""
{source['filename']} {f"{chunk_label}" if chunk_label else ""}

{source['text']}

{pct}%
match
""", unsafe_allow_html=True, ) # Helper β€” detect queries where low retrieval scores are structurally expected _SUMMARY_PATTERNS = re.compile( r"^\s*(summarize|summarise|give me a summary|overview|what is this (doc|document|about)|" r"tldr|tl;dr|brief me|describe this)", re.IGNORECASE, ) def _is_retrieval_limited_query(query: str, pct: int) -> bool: """True when low confidence is caused by retrieval structure, not a model failure.""" if pct <= 40: # Anything at or below 40% is retrieval-limited by definition β€” # the model found weak matches regardless of query type. return True if _SUMMARY_PATTERNS.match(query.strip()): return True return False def _render_confidence(trace, query: str = ""): """Capability 5 β€” always render confidence bar below the answer card.""" if not trace: return pct = round(trace.confidence * 100) tier = trace.confidence_label # 'high' | 'medium' | 'low' if tier == "low" and _is_retrieval_limited_query(query, pct): tier_text = "Retrieval limited" bar_class = "conf-bar-medium" pct_class = "conf-pct-medium" else: tier_text = { "high": "High confidence", "medium": "Medium confidence", "low": "Low confidence", }[tier] bar_class = f"conf-bar-{tier}" pct_class = f"conf-pct-{tier}" st.markdown( f"
" f"{tier_text}" f"
" f"
" f"
" f"{pct}%" f"
", unsafe_allow_html=True, ) def _render_thumbs(turn_idx: int): """Capability 6 β€” thumbs feedback rendered as a clean HTML inline row.""" turn = st.session_state.chat_history[turn_idx] existing = turn.get("feedback") if existing: icon = "πŸ‘" if existing == "up" else "πŸ‘Ž" st.markdown( f"
" f"" f"{icon} Feedback recorded
", unsafe_allow_html=True, ) return col_up, col_dn, col_rest = st.columns([1, 1, 22]) with col_up: if st.button("πŸ‘", key=f"tb_up_{turn_idx}", help="Helpful"): _record_feedback(turn_idx, thumbs_up=True) with col_dn: if st.button("πŸ‘Ž", key=f"tb_dn_{turn_idx}", help="Not helpful"): _record_feedback(turn_idx, thumbs_up=False) def _record_feedback(turn_idx: int, thumbs_up: bool): """ Capability 6 β€” write feedback into session state FeedbackStore and annotate the chat_history turn so buttons become static. """ turn = st.session_state.chat_history[turn_idx] turn["feedback"] = "up" if thumbs_up else "down" # Lazily initialise FeedbackStore if st.session_state.feedback_store is None: st.session_state.feedback_store = FeedbackStore() trace = turn.get("healing_trace") chunk_ids = trace.retrieved_chunk_ids if trace else [] st.session_state.feedback_store.record( query=turn["user"], chunk_ids=chunk_ids, thumbs_up=thumbs_up, ) _rerun() def _render_self_healing(trace, always_show: bool = False): """ Render self-healing badges + collapsible audit panel. When always_show=True, renders even if no healing actions fired (shows a minimal 'Pipeline ran cleanly' state for UI consistency). """ if not trace: return any_fired = trace.any_healing_fired badge_text = trace.summary_badge() if trace else "" # ── Badge row ────────────────────────────────────────────────────────── badges_html = ( "
" "
" "Faithfulness verified Β· no hallucination detected" "
" ) if badge_text: badges_html += ( f"
" f"
" f"{badge_text}" f"
" ) st.markdown(f"
{badges_html}
", unsafe_allow_html=True) # ── Collapsible audit panel ──────────────────────────────────────────── steps = trace.steps if trace else [] # If no healing fired and we're in always_show mode, synthesise a clean-run step if not steps: if not always_show: return steps = ["Pipeline ran cleanly Β· no interventions required"] uid = abs(hash(str(steps))) st.markdown( f"""""", unsafe_allow_html=True, ) rows_html = "" for step in steps: s = step.lower() if "pipeline ran cleanly" in s: dot_color, text_color = "#1D9E75", "#1D9E75" elif any(w in s for w in ("complete", "success", "passed", "indexed")): dot_color, text_color = "#1D9E75", "#1D9E75" elif any(w in s for w in ("failed", "error", "invalid")): dot_color, text_color = "#A32D2D", "#A32D2D" elif any(w in s for w in ("filtered", "low-relevance", "inferential", "re-answer", "threshold", "fallback", "faithfulness check")): dot_color, text_color = "#BA7517", "#BA7517" elif "confidence:" in s: dot_color, text_color = "#7F77DD", "#afa9ec" elif any(w in s for w in ("feedback:", "rerank", "rewrite skipped", "threshold adjusted")): dot_color, text_color = "#5b8dd9", "#8ab4f8" else: dot_color, text_color = "#4e4d60", "#6b6980" rows_html += ( f"
" f"" f"{step}" f"
" ) st.markdown( f"
" f"
" f"" f"" f"" f"
", unsafe_allow_html=True, ) # ── Factual query detector ──────────────────────────────────────────────────────── def _is_factual_query(query: str) -> bool: factual_starters = ("who", "what", "when", "where", "which", "how many", "how much") return len(query.split()) <= 8 and query.lower().startswith(factual_starters) def _render_chat_interface(): if st.session_state.vector_store is None: if st.session_state.processing_status != "success": st.markdown( "

" "Upload and process documents to start chatting.

", unsafe_allow_html=True, ) return # ── Consume pending_question FIRST before any rendering ────────────────── # This prevents the double-render: if a chip was clicked, we capture the # question and clear state now, so the render below sees the final state. pending = st.session_state.get("pending_question") if pending: st.session_state.pending_question = None st.session_state.starter_questions = [] # Snapshot history BEFORE appending the new placeholder turn history_for_context = list(st.session_state.chat_history) st.session_state.chat_history.append({"user": pending, "assistant": "…"}) generator = ChatGenerator(model=st.session_state.selected_model) generator._conversation_history = history_for_context # picked up by generate_answer with st.spinner("Searching your documents…"): try: answer, relevant_docs, healing_trace = run_healing_pipeline( query=pending, vector_store=st.session_state.vector_store, generator=generator, k=6, relevance_threshold=0.15 if _is_factual_query(pending) else 0.30, feedback_store=st.session_state.feedback_store, ) except RuntimeError as e: err_msg = ( "⚠️ The demo's free API quota is temporarily exhausted. " "Please try again later or run the app locally with Ollama." if ("402" in str(e) or "Payment Required" in str(e) or "credits" in str(e).lower()) else f"⚠️ Something went wrong: {e}" ) st.session_state.chat_history[-1] = { "user": pending, "assistant": err_msg, "sources": [], "healing_trace": None, } _rerun() return sources = [] for idx, (doc, score) in enumerate(relevant_docs, start=1): meta = doc.metadata sources.append({ "num": f"Source {idx}", "filename": meta.get("filename") or meta.get("source") or f"Document {idx}", "chunk_id": meta.get("chunk_id", "β€”"), "match_pct": _score_to_pct(score), "text": _excerpt_text(doc.page_content, max_chars=260), "title": f"Source {idx}", "match_label": f"{_score_to_pct(score)}% match", }) st.session_state.chat_history[-1] = { "user": pending, "assistant": answer, "sources": sources, "healing_trace": healing_trace, } _rerun() return # ── Suggested question cards (shown when chat is empty) ─────────────────── if not st.session_state.chat_history and st.session_state.starter_questions: questions_snapshot = list(st.session_state.starter_questions) # Render the beautiful card UI via st.markdown (in parent frame, no iframe, no bg issues) def _html_escape(s): return s.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) cards_html = "" for i, q in enumerate(questions_snapshot): q_safe = _html_escape(q) # data-idx used by the JS click handler below cards_html += f"""
0{i+1}
{q_safe}
Ask β†’
""" st.markdown( f"""
✦ Suggested questions
{cards_html}
""", unsafe_allow_html=True, ) # Hidden native st.buttons β€” visually suppressed via CSS but Streamlit-clickable. # The components.html script below wires each .sq-card click to its matching button. btn_cols = st.columns(len(questions_snapshot)) for i, (col, q) in enumerate(zip(btn_cols, questions_snapshot)): with col: if st.button(q, key=f"sq_{i}", use_container_width=True): st.session_state.pending_question = q st.session_state.starter_questions = [] _rerun() return # Wire card clicks β†’ hidden buttons via parent-frame JS. # Buttons are found by matching their visible text to the question text. # The same JS also hides the native buttons so only the styled cards show. import json as _json questions_json = _json.dumps(questions_snapshot) components.html( f""" """, height=0, ) # ── Render chat history ─────────────────────────────────────────────────── for turn_idx, turn in enumerate(st.session_state.chat_history): with st.chat_message("user"): st.markdown(turn["user"]) with st.chat_message("assistant"): n_src = len(turn.get("sources", [])) src_count_html = ( f"({n_src} source{'s' if n_src > 1 else ''})" if n_src else "" ) st.markdown( f"
" f"" f"{turn['assistant']}{src_count_html}" f"
", unsafe_allow_html=True, ) trace = turn.get("healing_trace") _render_confidence(trace, query=turn.get("user", "")) _render_thumbs(turn_idx) _render_self_healing(trace, always_show=True) if turn.get("sources"): st.markdown("
Sources
", unsafe_allow_html=True) for src in turn["sources"]: _render_source_card(src) # ── Accept typed input ──────────────────────────────────────────────────── user_question = st.chat_input("Ask anything about your documents…") if not user_question: return # Snapshot history BEFORE appending the new placeholder turn history_for_context = list(st.session_state.chat_history) st.session_state.chat_history.append({"user": user_question, "assistant": "…"}) generator = ChatGenerator(model=st.session_state.selected_model) generator._conversation_history = history_for_context # picked up by generate_answer with st.spinner("Searching your documents…"): try: answer, relevant_docs, healing_trace = run_healing_pipeline( query=user_question, vector_store=st.session_state.vector_store, generator=generator, k=6, relevance_threshold=0.15 if _is_factual_query(user_question) else 0.30, feedback_store=st.session_state.feedback_store, ) except RuntimeError as e: if "402" in str(e) or "Payment Required" in str(e) or "credits" in str(e).lower(): st.session_state.chat_history[-1] = { "user": user_question, "assistant": "⚠️ The demo's free API quota is temporarily exhausted. " "Please try again later or run the app locally with Ollama.", "sources": [], "healing_trace": None, } else: st.session_state.chat_history[-1] = { "user": user_question, "assistant": f"⚠️ Something went wrong: {e}", "sources": [], "healing_trace": None, } _rerun() return sources = [] for idx, (doc, score) in enumerate(relevant_docs, start=1): meta = doc.metadata sources.append({ "num": f"Source {idx}", "filename": meta.get("filename") or meta.get("source") or f"Document {idx}", "chunk_id": meta.get("chunk_id", "β€”"), "match_pct": _score_to_pct(score), "text": _excerpt_text(doc.page_content, max_chars=260), "title": f"Source {idx}", "match_label": f"{_score_to_pct(score)}% match", }) st.session_state.chat_history[-1] = { "user": user_question, "assistant": answer, "sources": sources, "healing_trace": healing_trace, } _rerun() # ── Border fix (components.html injection) ─────────────────────────────────────── def _inject_border_fix(): components.html( """ """, height=0, ) # ── Entry point ────────────────────────────────────────────────────────────────── _inject_styles() _initialize_state() _render_sidebar() _render_demo_banner() _render_steps() _render_hero() _render_index_summary() _render_chat_interface() _inject_border_fix()