Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import sys | |
| import os | |
| import html | |
| import re | |
| import json | |
| import uuid | |
| import streamlit.components.v1 as components | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from pipeline import run_query, PipelineResult | |
| from retrieval.retriever import RetrievedDocument | |
| from retrieval.refine import refine_with_user_query | |
| from evaluation.feedback import log_feedback | |
| st.set_page_config( | |
| page_title="Digital Commonwealth Β· BPL Search", | |
| page_icon="π", | |
| layout="wide", | |
| initial_sidebar_state="collapsed", | |
| ) | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&family=Source+Sans+3:wght@300;400;500;600&display=swap'); | |
| :root { | |
| --cream: #1A1410; | |
| --ink: #F7F3EC; | |
| --sepia: #C8A96E; | |
| --gold: #C8973A; | |
| --rust: #E07050; | |
| --muted: #A89880; | |
| --border: #3D3028; | |
| --card-bg: #221A12; | |
| --tag-bg: #2E2218; | |
| } | |
| html, body, [class*="css"], | |
| [data-testid="stApp"], | |
| [data-testid="stAppViewContainer"], | |
| [data-testid="stMain"], | |
| [data-testid="stBottom"], | |
| [data-testid="stHeader"], | |
| .stApp, .main { | |
| background-color: var(--cream) !important; | |
| color: var(--ink) !important; | |
| font-family: 'Source Sans 3', sans-serif; | |
| } | |
| .block-container { | |
| background-color: var(--cream) !important; | |
| padding-top: 0.5rem !important; | |
| padding-bottom: 3rem !important; | |
| max-width: 960px !important; | |
| } | |
| .block-container > div:first-child { | |
| margin-top: 0 !important; | |
| padding-top: 0 !important; | |
| } | |
| div[data-testid="stTextInput"] input, | |
| div[data-testid="stTextInput"] input:focus, | |
| div[data-testid="stTextInput"] input:active { | |
| background-color: #2E2218 !important; | |
| color: #F7F3EC !important; | |
| -webkit-text-fill-color: #F7F3EC !important; | |
| -webkit-box-shadow: none !important; | |
| box-shadow: none !important; | |
| outline: none !important; | |
| } | |
| div[data-testid="stVerticalBlock"] > div:has(div[data-testid="stTextInput"]) { | |
| padding-top: 0 !important; | |
| margin-top: 0 !important; | |
| } | |
| #MainMenu, footer, header { visibility: hidden; } | |
| html, body, [data-testid="stAppViewContainer"], .main, .block-container, | |
| section[data-testid="stMain"], div[data-testid="stAppViewBlockContainer"] { | |
| scroll-behavior: smooth !important; | |
| } | |
| /* ββ Full text expander ββ */ | |
| details.full-text-expander { | |
| margin-top: 0.6rem; | |
| border-top: 1px solid var(--border); | |
| padding-top: 0.6rem; | |
| } | |
| details.full-text-expander summary { | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| color: var(--sepia); | |
| cursor: pointer; | |
| user-select: none; | |
| list-style: none; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.4rem; | |
| } | |
| details.full-text-expander summary::-webkit-details-marker { display: none; } | |
| details.full-text-expander summary::before { | |
| content: "βΆ"; | |
| font-size: 0.6rem; | |
| transition: transform 0.2s; | |
| display: inline-block; | |
| } | |
| details.full-text-expander[open] summary::before { transform: rotate(90deg); } | |
| details.full-text-expander summary:hover { color: var(--gold); } | |
| .full-text-body { | |
| margin-top: 0.75rem; | |
| font-size: 0.85rem; | |
| color: var(--muted); | |
| line-height: 1.75; | |
| white-space: pre-wrap; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| padding-right: 0.5rem; | |
| } | |
| .full-text-body::-webkit-scrollbar { width: 4px; } | |
| .full-text-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } | |
| .result-card:target { | |
| border-color: #C8973A !important; | |
| box-shadow: 0 0 0 4px rgba(200,151,58,0.35) !important; | |
| transition: border-color 0.3s, box-shadow 0.3s; | |
| } | |
| /* ββ Primary button β NO width:100% so feedback buttons stay compact ββ */ | |
| div[data-testid="stButton"] button[kind="primary"] { | |
| background-color: var(--gold) !important; | |
| border: none !important; | |
| border-radius: 4px !important; | |
| color: #fff !important; | |
| font-family: 'Source Sans 3', sans-serif !important; | |
| font-weight: 600 !important; | |
| font-size: 0.92rem !important; | |
| white-space: nowrap !important; | |
| /* width is controlled per-button via use_container_width in Python */ | |
| } | |
| div[data-testid="stButton"] button[kind="primary"]:hover { | |
| background-color: var(--sepia) !important; | |
| } | |
| /* ββ Secondary button base β NO width:100% ββ */ | |
| div[data-testid="stButton"] button[kind="secondary"] { | |
| background: var(--tag-bg) !important; | |
| border: 1px solid var(--border) !important; | |
| border-radius: 20px !important; | |
| color: var(--sepia) !important; | |
| font-family: 'Source Sans 3', sans-serif !important; | |
| font-size: 0.78rem !important; | |
| font-style: italic !important; | |
| padding: 0.28rem 0.75rem !important; | |
| white-space: nowrap !important; | |
| } | |
| div[data-testid="stButton"] button[kind="secondary"]:hover { | |
| background: var(--gold) !important; | |
| border-color: var(--gold) !important; | |
| color: #fff !important; | |
| font-style: normal !important; | |
| } | |
| /* ββ Feedback buttons: compact pill styling ββ */ | |
| div[data-testid="stButton"] button[kind="secondary"].fb, | |
| div[data-testid="stButton"] button[kind="primary"].fb { | |
| width: auto !important; | |
| min-width: 0 !important; | |
| font-size: 0.72rem !important; | |
| font-style: normal !important; | |
| border-radius: 20px !important; | |
| padding: 0.18rem 0.8rem !important; | |
| letter-spacing: 0.03em !important; | |
| } | |
| div[data-testid="stButton"] button[kind="secondary"].fb { | |
| color: var(--muted) !important; | |
| } | |
| div[data-testid="stButton"] button[kind="secondary"].fb:hover { | |
| color: var(--gold) !important; | |
| border-color: var(--gold) !important; | |
| background: var(--tag-bg) !important; | |
| font-style: normal !important; | |
| } | |
| div[data-testid="stButton"] button[kind="primary"].fb { | |
| background: var(--gold) !important; | |
| color: #fff !important; | |
| border-radius: 20px !important; | |
| } | |
| /* ββ Masthead ββ */ | |
| .masthead { | |
| text-align: center; | |
| padding: 3.5rem 1rem 2rem; | |
| border-bottom: 2px solid var(--border); | |
| margin-bottom: 2.5rem; | |
| } | |
| .masthead-eyebrow { | |
| font-size: 0.72rem; font-weight: 600; | |
| letter-spacing: 0.22em; text-transform: uppercase; | |
| color: var(--sepia); margin-bottom: 0.6rem; | |
| } | |
| .masthead-title { | |
| font-family: 'Playfair Display', Georgia, serif; | |
| font-size: 3rem; font-weight: 400; | |
| color: var(--ink); line-height: 1.15; margin: 0 0 0.5rem; | |
| } | |
| .masthead-title em { font-style: italic; color: var(--gold); } | |
| .masthead-sub { | |
| font-size: 1rem; color: var(--muted); font-weight: 300; | |
| max-width: 540px; margin: 0 auto !important; | |
| display: block; text-align: center !important; line-height: 1.6; | |
| } | |
| .search-label { | |
| font-size: 0.72rem; font-weight: 600; | |
| letter-spacing: 0.18em; text-transform: uppercase; | |
| color: var(--sepia); margin-bottom: 0.4rem; | |
| } | |
| .divider { border: none; border-top: 1px solid var(--border); margin: 1.8rem 0; } | |
| /* ββ Context banner ββ */ | |
| .context-banner { | |
| background: #2A1E0E; | |
| border-left: 4px solid var(--gold); | |
| border-radius: 0 4px 4px 0; | |
| padding: 1.2rem 1.5rem; margin-bottom: 2rem; | |
| font-size: 0.95rem; line-height: 1.65; color: var(--ink); | |
| } | |
| .context-banner strong { color: var(--gold); } | |
| .citation-link { | |
| color: var(--gold); font-weight: 700; | |
| text-decoration: none; border-bottom: 1px dotted var(--gold); | |
| } | |
| .citation-link:hover { border-bottom-style: solid; color: var(--sepia); } | |
| /* ββ Results header ββ */ | |
| .results-header { | |
| display: flex; align-items: baseline; | |
| justify-content: space-between; margin-bottom: 1.2rem; | |
| } | |
| .results-count { font-family: 'Playfair Display', serif; font-size: 1.35rem; color: var(--ink); } | |
| .results-count span { color: var(--gold); font-style: italic; } | |
| .results-meta { font-size: 0.78rem; color: var(--muted); letter-spacing: 0.06em; } | |
| /* ββ Result card ββ */ | |
| .result-card { | |
| background: var(--card-bg); border: 1px solid var(--border); | |
| border-radius: 4px; padding: 1.4rem 1.6rem; | |
| margin-bottom: 0.4rem; scroll-margin-top: 80px; | |
| transition: border-color 0.3s, box-shadow 0.3s; | |
| } | |
| .result-card:hover { border-color: var(--gold); box-shadow: 0 4px 18px rgba(200,151,58,0.12); } | |
| .result-card.citation-highlight { | |
| border-color: var(--gold) !important; | |
| box-shadow: 0 0 0 4px rgba(200,151,58,0.4) !important; | |
| } | |
| .card-type-badge { | |
| display: inline-block; font-size: 0.65rem; font-weight: 600; | |
| letter-spacing: 0.18em; text-transform: uppercase; | |
| color: var(--rust); border: 1px solid var(--rust); | |
| border-radius: 2px; padding: 0.15rem 0.5rem; margin-bottom: 0.6rem; | |
| } | |
| .card-title { | |
| font-family: 'Playfair Display', serif; font-size: 1.1rem; | |
| font-weight: 600; color: var(--ink); margin-bottom: 0.3rem; line-height: 1.3; | |
| } | |
| .card-meta { font-size: 0.8rem; color: var(--muted); margin-bottom: 0.7rem; line-height: 1.5; } | |
| .card-snippet { font-size: 0.88rem; color: #C8B89A; line-height: 1.65; margin-bottom: 0.8rem; } | |
| .card-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.4rem; } | |
| .card-tag { | |
| background: var(--tag-bg); font-size: 0.72rem; color: var(--sepia); | |
| padding: 0.2rem 0.55rem; border-radius: 2px; border: 1px solid var(--border); | |
| } | |
| .card-link { | |
| font-size: 0.78rem; font-weight: 600; color: var(--gold); | |
| letter-spacing: 0.06em; text-transform: uppercase; | |
| text-decoration: none; border-bottom: 1px solid transparent; | |
| } | |
| .card-link:hover { border-bottom-color: var(--gold); } | |
| .score-row { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.6rem; } | |
| .score-label { font-size: 0.7rem; color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; } | |
| .score-bar-bg { flex:1; height:4px; background:var(--border); border-radius:2px; overflow:hidden; max-width:120px; } | |
| .score-bar-fill { height:100%; background:linear-gradient(90deg,var(--gold),var(--rust)); border-radius:2px; } | |
| .score-val { font-size: 0.7rem; color: var(--sepia); font-weight: 600; } | |
| /* ββ No results ββ */ | |
| .no-results { text-align:center; padding:4rem 2rem; color:var(--muted); } | |
| .no-results-icon { font-size:3rem; margin-bottom:1rem; } | |
| .no-results-title { font-family:'Playfair Display',serif; font-size:1.4rem; color:var(--ink); margin-bottom:0.5rem; } | |
| /* ββ Form alignment ββ */ | |
| div[data-testid="stForm"] div[data-testid="stHorizontalBlock"] { | |
| align-items: flex-end !important; | |
| } | |
| /* ββ Footer ββ */ | |
| .bpl-footer { | |
| text-align:center; padding:2.5rem 1rem 1rem; | |
| border-top:1px solid var(--border); margin-top:3rem; | |
| font-size:0.78rem; color:var(--muted); letter-spacing:0.05em; | |
| } | |
| .bpl-footer strong { color: var(--sepia); } | |
| /* ββ Hide Streamlit input hints ββ */ | |
| [data-testid="InputInstructions"], | |
| [data-testid="stTextAreaInstructions"], | |
| [data-testid="stWidgetInstructions"], | |
| div[data-testid="stTextArea"] small, | |
| div[data-testid="stTextInput"] small { | |
| display: none !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| EXAMPLE_QUERIES = [ | |
| "What happened in Boston in 1900?", | |
| "Find photographs of Greece", | |
| "Show me circus posters", | |
| "Victorian era correspondence", | |
| "Boston Traveler newspaper 1900", | |
| "Women's suffrage documents", | |
| ] | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def linkify_citations(text: str, num_docs: int) -> str: | |
| def replace(match): | |
| n = int(match.group(1)) | |
| if 1 <= n <= num_docs: | |
| return ( | |
| '<a class="citation-link" href="#result-' + str(n) + '">' | |
| '[' + str(n) + ']</a>' | |
| ) | |
| return match.group(0) | |
| return re.sub(r'\[(\d+)\]', replace, text) | |
| def format_card(doc: RetrievedDocument) -> dict: | |
| topics = doc.topics or [] | |
| title_lower = (doc.title or "").lower() | |
| if any(t.lower() in ["photograph","photography","photographs"] for t in topics): | |
| doc_type = "Photograph" | |
| elif any(t.lower() in ["map","maps","cartography"] for t in topics): | |
| doc_type = "Map" | |
| elif any(w in title_lower for w in ["traveler","globe","herald","gazette","journal","tribune"]): | |
| doc_type = "Newspaper" | |
| elif any(t.lower() in ["correspondence","manuscript","letter","papers"] for t in topics): | |
| doc_type = "Manuscript" | |
| else: | |
| doc_type = "Document" | |
| date_str = doc.issue_date or (str(doc.year[0]) if doc.year else "Date unknown") | |
| snippet = doc.best_chunk_text[:300] if doc.best_chunk_text else "" | |
| full_text = doc.best_chunk_text if doc.best_chunk_text else "" | |
| tags = list(set((doc.topics or []) + (doc.geography or [])))[:5] | |
| url = f"https://www.digitalcommonwealth.org/search/commonwealth:{doc.ark_id}" | |
| thumbnail_url = ( | |
| f"https://iiif.digitalcommonwealth.org/iiif/2/{doc.exemplary_image_id}/full/400,/0/default.jpg" | |
| if doc.exemplary_image_id and doc.exemplary_image_id.strip() else "" | |
| ) | |
| return { | |
| "type": doc_type, "title": doc.title or "Untitled", | |
| "date": date_str, "collection": doc.institution or "Boston Public Library", | |
| "snippet": snippet, "full_text": full_text, "tags": tags, | |
| "score": round(doc.final_score, 2), "url": url, "thumbnail": thumbnail_url, | |
| } | |
| def build_card_html(r: dict, i: int) -> str: | |
| score_pct = min(int(r["score"] * 100), 100) | |
| tags_html = "".join( | |
| '<span class="card-tag">' + html.escape(t) + '</span>' for t in r["tags"] | |
| ) | |
| thumb = ( | |
| '<img src="' + r["thumbnail"] + '" ' | |
| 'style="width:100%;max-height:200px;object-fit:cover;' | |
| 'border-radius:4px;margin-bottom:0.8rem;" />' | |
| if r.get("thumbnail", "").startswith("https://") else "" | |
| ) | |
| full_text = r.get("full_text", "") | |
| if full_text and len(full_text) > 300: | |
| expander = ( | |
| '<details class="full-text-expander">' | |
| '<summary>Full Text</summary>' | |
| '<div class="full-text-body">' + html.escape(full_text) + '</div>' | |
| '</details>' | |
| ) | |
| else: | |
| expander = "" | |
| return ( | |
| '<div class="result-card" id="result-' + str(i) + '">' | |
| + thumb | |
| + '<div class="card-type-badge">' + html.escape(r["type"]) + '</div>' | |
| + '<div class="card-title">' + html.escape(r["title"]) + '</div>' | |
| + '<div class="card-meta">' | |
| + html.escape(r["date"]) + ' Β· ' + html.escape(r["collection"]) | |
| + '</div>' | |
| + '<div class="card-snippet">' + html.escape(r["snippet"]) + '</div>' | |
| + expander | |
| + '<div class="card-tags">' + tags_html + '</div>' | |
| + '<div class="score-row">' | |
| + '<span class="score-label">Relevance</span>' | |
| + '<div class="score-bar-bg">' | |
| + '<div class="score-bar-fill" style="width:' + str(score_pct) + '%"></div>' | |
| + '</div>' | |
| + '<span class="score-val">' + str(score_pct) + '%</span>' | |
| + ' ' | |
| + '<a class="card-link" href="' + r["url"] + '" target="_blank">' | |
| + 'View in Digital Commonwealth β</a>' | |
| + '</div>' | |
| + '</div>' | |
| ) | |
| # ββ Session state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| for k, v in [ | |
| ("query", ""), ("results", None), ("searched", False), | |
| ("context", ""), ("latency_ms", 0), ("_last_ran", ""), ("page", 0), | |
| ("query_id", None), | |
| ("docs", []), | |
| ("thumbs", {}), | |
| ("missing_text", ""), | |
| ("refined_with", []), | |
| ("_scroll_to_top", False), | |
| ]: | |
| if k not in st.session_state: | |
| st.session_state[k] = v | |
| if "session_id" not in st.session_state: | |
| st.session_state["session_id"] = str(uuid.uuid4()) | |
| # ββ Masthead ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <div class="masthead"> | |
| <div class="masthead-eyebrow">Boston Public Library Β· Digital Commonwealth</div> | |
| <h1 class="masthead-title">Search the <em>Archive</em></h1> | |
| <p class="masthead-sub"> | |
| Ask anything in plain language. Explore photographs, maps, newspapers, | |
| manuscripts, and more from Massachusetts history. | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ββ Search box ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown('<div class="search-label">Natural Language Query</div>', unsafe_allow_html=True) | |
| with st.form(key="search_form", border=False): | |
| col_input, col_btn = st.columns([5, 1]) | |
| with col_input: | |
| typed = st.text_input( | |
| "Natural Language Query", | |
| value=st.session_state.query, | |
| placeholder='e.g. "Find photographs of Boston Harbor from the 1800s"', | |
| label_visibility="collapsed", | |
| key="text_input_box", | |
| ) | |
| with col_btn: | |
| search_clicked = st.form_submit_button( | |
| "Search β", type="primary", use_container_width=True | |
| ) | |
| st.markdown( | |
| '<div class="search-label" style="margin-top:0.5rem;margin-bottom:0.4rem;">' | |
| 'Try an example</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| pill_clicked = None | |
| row1 = st.columns(3) | |
| row2 = st.columns(3) | |
| for i, q in enumerate(EXAMPLE_QUERIES): | |
| col = row1[i] if i < 3 else row2[i - 3] | |
| with col: | |
| # use_container_width fills the column β this is what makes pills look good | |
| if st.button(q, key=f"pill_{i}", use_container_width=True): | |
| pill_clicked = q | |
| # ββ Determine active query ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if pill_clicked: | |
| st.session_state.query = pill_clicked | |
| elif search_clicked and typed.strip(): | |
| st.session_state.query = typed.strip() | |
| active_query = st.session_state.query.strip() | |
| # ββ Run pipeline ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if active_query and active_query != st.session_state["_last_ran"]: | |
| st.session_state["_last_ran"] = active_query | |
| st.session_state["page"] = 0 | |
| st.session_state["thumbs"] = {} | |
| st.session_state["missing_text"] = "" | |
| st.session_state["refined_with"] = [] | |
| with st.spinner("Searching the archiveβ¦"): | |
| try: | |
| result: PipelineResult = run_query( | |
| active_query, | |
| session_id=st.session_state["session_id"], | |
| ) | |
| cards = [format_card(doc) for doc in result.documents] | |
| st.session_state.results = cards | |
| st.session_state.docs = result.documents | |
| st.session_state.query_id = result.query_id | |
| st.session_state.context = result.generation.response | |
| st.session_state.latency_ms = result.latency_ms | |
| st.session_state.searched = True | |
| except Exception as e: | |
| st.error(f"Search failed: {e}") | |
| st.session_state.searched = False | |
| # ββ Results βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if st.session_state.searched and st.session_state.results is not None: | |
| results = st.session_state.results | |
| context = st.session_state.context | |
| latency = st.session_state.latency_ms | |
| st.markdown('<hr class="divider">', unsafe_allow_html=True) | |
| if results: | |
| if st.session_state["refined_with"]: | |
| chips = " Β· ".join(html.escape(q) for q in st.session_state["refined_with"]) | |
| st.markdown( | |
| '<div class="context-banner">' | |
| '<strong>Refined search.</strong> ' + chips + '</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| if st.session_state["_scroll_to_top"]: | |
| st.session_state["_scroll_to_top"] = False | |
| components.html( | |
| """ | |
| <script> | |
| (function() { | |
| const doc = window.parent.document; | |
| const main = doc.querySelector('section[data-testid="stMain"]') | |
| || doc.querySelector('[data-testid="stAppViewContainer"]') | |
| || doc.scrollingElement | |
| || doc.body; | |
| main.scrollTo({ top: 0, behavior: 'smooth' }); | |
| window.parent.scrollTo({ top: 0, behavior: 'smooth' }); | |
| })(); | |
| </script> | |
| """, | |
| height=0, | |
| ) | |
| context_with_links = linkify_citations(context, len(results)) | |
| st.markdown( | |
| '<div class="context-banner"><strong>About these results.</strong> ' | |
| + context_with_links + '</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown( | |
| '<div class="results-header">' | |
| + '<div class="results-count">Found <span>' + str(len(results)) | |
| + ' items</span> for “' + html.escape(st.session_state.query) | |
| + '”</div>' | |
| + '<div class="results-meta">Ranked by relevance Β· ' | |
| + str(latency) + 'ms Β· Digital Commonwealth BPL Subset</div>' | |
| + '</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| PAGE_SIZE = 10 | |
| total_pages = max(1, (len(results) + PAGE_SIZE - 1) // PAGE_SIZE) | |
| page = st.session_state["page"] | |
| start = page * PAGE_SIZE | |
| end = start + PAGE_SIZE | |
| page_results = results[start:end] | |
| page_docs = st.session_state.docs[start:end] | |
| for i, (r, doc) in enumerate(zip(page_results, page_docs), start=start + 1): | |
| with st.container(): | |
| st.markdown(build_card_html(r, i), unsafe_allow_html=True) | |
| # ββ Feedback row ββββββββββββββββββββββββββββββββββββββββββ | |
| # No use_container_width β buttons size naturally to their text. | |
| # The narrow columns prevent them from ever stretching wide. | |
| current = st.session_state["thumbs"].get(doc.ark_id) | |
| up_type = "primary" if current == "up" else "secondary" | |
| down_type = "primary" if current == "down" else "secondary" | |
| fb_col1, fb_col2, _ = st.columns([1, 1.6, 7]) | |
| with fb_col1: | |
| if st.button( | |
| "β Helpful", | |
| key=f"up_{i}_{doc.ark_id}", | |
| type=up_type, | |
| help="Mark as helpful", | |
| ): | |
| st.session_state["thumbs"][doc.ark_id] = "up" | |
| log_feedback( | |
| query_id = st.session_state["query_id"], | |
| ark_id = doc.ark_id, | |
| signal = "up", | |
| session_id = st.session_state["session_id"], | |
| raw_query = st.session_state["query"], | |
| ) | |
| st.rerun() | |
| with fb_col2: | |
| if st.button( | |
| "β Not relevant", | |
| key=f"down_{i}_{doc.ark_id}", | |
| type=down_type, | |
| help="Mark as not relevant", | |
| ): | |
| st.session_state["thumbs"][doc.ark_id] = "down" | |
| log_feedback( | |
| query_id = st.session_state["query_id"], | |
| ark_id = doc.ark_id, | |
| signal = "down", | |
| session_id = st.session_state["session_id"], | |
| raw_query = st.session_state["query"], | |
| ) | |
| st.rerun() | |
| st.markdown('<div style="height:0.5rem"></div>', unsafe_allow_html=True) | |
| # ββ Pagination controls βββββββββββββββββββββββββββββββββββββββββββ | |
| if total_pages > 1: | |
| st.markdown('<div style="height:0.5rem"></div>', unsafe_allow_html=True) | |
| pcol_prev, pcol_info, pcol_next = st.columns([1, 2, 1]) | |
| with pcol_prev: | |
| if page > 0: | |
| if st.button("β Previous", key="prev_page", use_container_width=True): | |
| st.session_state["page"] -= 1 | |
| st.rerun() | |
| with pcol_info: | |
| st.markdown( | |
| '<div style="text-align:center;padding-top:0.5rem;font-size:0.82rem;' | |
| 'color:var(--muted);font-family:sans-serif;">' | |
| 'Page ' + str(page + 1) + ' of ' + str(total_pages) | |
| + ' · ' + str(len(results)) + ' total results' | |
| + '</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| with pcol_next: | |
| if page < total_pages - 1: | |
| if st.button("Next β", key="next_page", use_container_width=True): | |
| st.session_state["page"] += 1 | |
| st.rerun() | |
| # ββ Human-in-the-loop refinement βββββββββββββββββββββββββββββββββ | |
| st.markdown('<hr class="divider">', unsafe_allow_html=True) | |
| st.markdown( | |
| '<div class="search-label">Didn\'t find any relevant results?</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown( | |
| '<div style="font-size:0.85rem;color:var(--muted);margin-bottom:0.6rem;">' | |
| 'Refine your search. Be more specific about what you want.' | |
| '</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| st.text_area( | |
| "Refine your search", | |
| key="missing_text", | |
| height=110, | |
| placeholder="e.g. photographs of JFK as a senator in 1958, not newspaper clippings", | |
| label_visibility="collapsed", | |
| ) | |
| if st.button("Refine search", key="refine_btn", use_container_width=True): | |
| user_text = st.session_state["missing_text"].strip() | |
| if not user_text: | |
| st.info("Type a refined query before clicking refine.") | |
| else: | |
| log_feedback( | |
| query_id = st.session_state["query_id"], | |
| ark_id = "", | |
| signal = "missing", | |
| comment = user_text, | |
| session_id = st.session_state["session_id"], | |
| raw_query = st.session_state["query"], | |
| ) | |
| with st.spinner("Searching with your refined queryβ¦"): | |
| try: | |
| merged, follow_ups, _child_ids = refine_with_user_query( | |
| original_query = st.session_state["query"], | |
| original_results = st.session_state.docs, | |
| user_query = user_text, | |
| top_k = max(50, len(st.session_state.docs)), | |
| session_id = st.session_state["session_id"], | |
| parent_query_id = st.session_state["query_id"], | |
| ) | |
| if follow_ups: | |
| st.session_state.docs = merged | |
| st.session_state.results = [format_card(d) for d in merged] | |
| st.session_state.refined_with = follow_ups | |
| st.session_state["page"] = 0 | |
| st.session_state["_scroll_to_top"] = True | |
| st.rerun() | |
| else: | |
| st.warning("Refinement search failed. Try again.") | |
| except Exception as e: | |
| st.error(f"Refine failed: {e}") | |
| else: | |
| context_with_links = linkify_citations(context, len(results)) | |
| st.markdown( | |
| '<div style="' | |
| 'background:#1E1A14;' | |
| 'border:1px solid #3D3028;' | |
| 'border-radius:6px;' | |
| 'padding:1.2rem 1.5rem;' | |
| 'margin-bottom:2rem;' | |
| 'font-size:0.95rem;line-height:1.65;color:#A89880;">' | |
| + context_with_links + | |
| '</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown( | |
| '<div class="no-results">' | |
| '<div class="no-results-icon">ποΈ</div>' | |
| '<div class="no-results-title">No matching materials found</div>' | |
| '<p>Try rephrasing your query, or use one of the example searches above.</p>' | |
| '</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| # ββ Footer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown( | |
| '<div class="bpl-footer">' | |
| '<strong>Boston Public Library</strong> Β· Digital Commonwealth Β· BPL RAG Search<br>' | |
| 'A natural language search prototype built with Retrieval-Augmented Generation.<br>' | |
| 'Results are drawn from digitized items in the BPL subset of Digital Commonwealth.' | |
| '</div>', | |
| unsafe_allow_html=True, | |
| ) |