Rxrohans/PayLens-Dev
0
1"""2app.py — Phase 4 (v2): ChargeClarity Streamlit UI3---------------------------------------------------4Run: streamlit run src/app.py5"""6 7import sys8import re9from pathlib import Path10sys.path.insert(0, str(Path(__file__).parent))11from datetime_parser import parse_datetime_query12 13import streamlit as st14from dotenv import load_dotenv15load_dotenv()16 17# ── Page config ───────────────────────────────────────────18st.set_page_config(19 page_title="PayLens",20 page_icon="💳",21 layout="centered",22 initial_sidebar_state="collapsed"23)24 25# ── CSS ───────────────────────────────────────────────────26st.markdown("""27<style>28@import url('https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=DM+Sans:wght@300;400;500&display=swap');29 30html, body, [class*="css"] { font-family: 'DM Sans', sans-serif; background:#0d0d14; color:#e2e2ee; }31.stApp { background:#0d0d14; }32#MainMenu, footer, header { visibility:hidden; }33.block-container { padding-top:2.5rem; padding-bottom:3rem; max-width:760px; }34 35/* hero */36.hero { text-align:center; padding:2.5rem 1rem 0.5rem; }37.hero-eyebrow {38 display:inline-block; background:rgba(139,92,246,0.12);39 border:1px solid rgba(139,92,246,0.25); color:#a78bfa;40 font-size:0.7rem; letter-spacing:0.14em; text-transform:uppercase;41 padding:0.28rem 0.9rem; border-radius:999px; margin-bottom:1.1rem;42}43.hero-title {44 font-family:'Syne',sans-serif; font-size:2.9rem; font-weight:800;45 background:linear-gradient(130deg,#fff 40%,#a78bfa);46 -webkit-background-clip:text; -webkit-text-fill-color:transparent;47 line-height:1.08; letter-spacing:-0.03em; margin-bottom:0.7rem;48}49.hero-sub { color:#52526e; font-size:0.95rem; font-weight:300; max-width:440px; margin:0 auto 1.8rem; line-height:1.65; }50 51/* chips */52.stButton>button {53 background:rgba(255,255,255,0.04) !important; border:1px solid rgba(255,255,255,0.09) !important;54 color:#7b7b9a !important; font-size:0.78rem !important; border-radius:999px !important;55 padding:0.3rem 0.9rem !important; transition:all 0.18s !important; font-family:'DM Sans' !important;56}57.stButton>button:hover { background:rgba(139,92,246,0.12) !important; color:#c4b5fd !important; border-color:rgba(139,92,246,0.35) !important; }58 59/* ask button override */60div[data-testid="column"]:last-child .stButton>button {61 background:linear-gradient(135deg,#6d28d9,#7c3aed) !important;62 color:white !important; border:none !important;63 border-radius:10px !important; font-weight:500 !important; font-size:0.95rem !important;64 padding:0.65rem 1.2rem !important;65}66 67/* input */68.stTextInput input {69 background:rgba(255,255,255,0.04) !important; border:1px solid rgba(255,255,255,0.1) !important;70 border-radius:10px !important; color:#e2e2ee !important; font-size:0.97rem !important;71 padding:0.8rem 1.1rem !important; font-family:'DM Sans' !important;72}73.stTextInput input:focus { border-color:rgba(139,92,246,0.55) !important; box-shadow:0 0 0 3px rgba(139,92,246,0.1) !important; }74.stTextInput input::placeholder { color:#3a3a52 !important; }75.stTextInput label { display:none !important; }76 77/* answer card */78.ans-card {79 background:rgba(255,255,255,0.025); border:1px solid rgba(255,255,255,0.07);80 border-radius:14px; padding:1.6rem 1.8rem; margin-top:1.4rem;81}82.ans-card p { margin:0 0 0.75rem; line-height:1.75; color:#c8c8e0; font-size:0.96rem; }83.ans-card p:last-child { margin-bottom:0; }84.ans-card ul { padding-left:1.2rem; margin:0.3rem 0 0.75rem; }85.ans-card li { color:#c8c8e0; font-size:0.96rem; line-height:1.75; margin-bottom:0.25rem; }86.ans-card strong { color:#e8e8f8; font-weight:500; }87 88/* badges */89.badge-row { display:flex; flex-wrap:wrap; gap:0.5rem; margin-top:1.1rem; align-items:center; }90.badge {91 font-size:0.72rem; font-weight:500; letter-spacing:0.04em;92 padding:0.22rem 0.7rem; border-radius:999px;93}94.b-high { background:rgba(16,185,129,0.12); color:#6ee7b7; border:1px solid rgba(16,185,129,0.2); }95.b-medium { background:rgba(245,158,11,0.12); color:#fcd34d; border:1px solid rgba(245,158,11,0.2); }96.b-low { background:rgba(239,68,68,0.12); color:#fca5a5; border:1px solid rgba(239,68,68,0.2); }97.b-none { background:rgba(100,100,120,0.12); color:#9ca3af; border:1px solid rgba(100,100,120,0.2); }98.b-web { background:rgba(59,130,246,0.12); color:#93c5fd; border:1px solid rgba(59,130,246,0.2); }99.b-src { background:rgba(139,92,246,0.1); color:#c4b5fd; border:1px solid rgba(139,92,246,0.18); }100.b-time { background:rgba(255,255,255,0.04); color:#52526e; border:1px solid rgba(255,255,255,0.07); }101 102/* links section */103.links-section { margin-top:1.1rem; }104.links-label { font-size:0.72rem; color:#3a3a52; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:0.5rem; }105.links-row { display:flex; flex-wrap:wrap; gap:0.45rem; }106.ext-link {107 font-size:0.78rem; color:#7c6faa; text-decoration:none;108 background:rgba(139,92,246,0.07); border:1px solid rgba(139,92,246,0.15);109 padding:0.22rem 0.75rem; border-radius:6px; transition:all 0.15s;110}111.ext-link:hover { background:rgba(139,92,246,0.15); color:#c4b5fd; }112 113/* blocked / warn */114.blocked { background:rgba(239,68,68,0.08); border:1px solid rgba(239,68,68,0.18); border-radius:10px; padding:1rem 1.2rem; color:#fca5a5; font-size:0.9rem; margin-top:1rem; }115.warn { background:rgba(245,158,11,0.08); border:1px solid rgba(245,158,11,0.18); border-radius:10px; padding:0.7rem 1rem; color:#fcd34d; font-size:0.82rem; margin-top:0.7rem; }116 117/* history */118.hist-wrap { margin-top:2.5rem; }119.hist-label { font-size:0.7rem; color:#2e2e42; letter-spacing:0.1em; text-transform:uppercase; margin-bottom:0.9rem; }120.hist-item { border-left:2px solid rgba(139,92,246,0.2); padding:0.5rem 0.9rem; margin-bottom:0.7rem; }121.hist-q { font-size:0.86rem; color:#6b6b8a; font-weight:500; margin-bottom:0.2rem; }122.hist-a { font-size:0.82rem; color:#3a3a52; line-height:1.55; }123 124hr { border-color:rgba(255,255,255,0.05) !important; margin:2rem 0 !important; }125.stSpinner>div { border-top-color:#7c3aed !important; }126/* Hide "Press Enter to submit form" text */127.stForm small { display: none !important; }128.stForm [data-testid="InputInstructions"] { display: none !important; }129</style>130""", unsafe_allow_html=True)131 132 133# ── Cached resources ──────────────────────────────────────134@st.cache_resource(show_spinner=False)135def load_chain():136 from pathlib import Path137 import subprocess, sys138 139 # Build index if it doesn't exist (first run on HuggingFace)140 index_path = Path(__file__).parent.parent / "data" / "processed" / "faiss_index.bin"141 142 if not index_path.exists():143 with st.spinner("First run — building knowledge base index (2-3 mins)..."):144 subprocess.run([sys.executable, str(Path(__file__).parent / "chunker.py")], check=True)145 subprocess.run([sys.executable, str(Path(__file__).parent / "embedder.py")], check=True)146 147 from chain import ChargeChain148 return ChargeChain()149 150@st.cache_resource(show_spinner=False)151def get_guardrail_fn():152 from guardrails import run_with_guardrails153 return run_with_guardrails154 155 156# ── Session state ─────────────────────────────────────────157if "history" not in st.session_state:158 st.session_state.history = []159if "result" not in st.session_state:160 st.session_state.result = None161if "last_q" not in st.session_state:162 st.session_state.last_q = ""163 164# ── Tabs ──────────────────────────────────────────────────165tab_main, tab_eval = st.tabs(["💳 PayLens", "📊 Eval Dashboard"])166 167with tab_main:168 # ── Hero ──────────────────────────────────────────────────169 st.markdown("""170 <div class="hero">171 <div class="hero-eyebrow">AI · RAG · Fintech</div>172 <div class="hero-title">PayLens</div>173 <div class="hero-sub">Ask anything about payment fees, currency charges,174 UPI, taxes, or fintech — get a plain-English answer with sources.</div>175 </div>176 """, unsafe_allow_html=True)177 178 # ── Example chips ─────────────────────────────────────────179 EXAMPLES = [180 "Why did PayPal charge me so much?",181 "How does currency conversion work?",182 "Is UPI free for merchants?",183 "What is a chargeback?",184 "Do I pay GST on foreign income?",185 ]186 187 chip_cols = st.columns(len(EXAMPLES))188 for i, ex in enumerate(EXAMPLES):189 if chip_cols[i].button(ex, key=f"chip_{i}"):190 st.session_state["chip_query"] = ex191 192 st.markdown("<div style='height:0.8rem'></div>", unsafe_allow_html=True)193 194 # ── Input ─────────────────────────────────────────────────195 with st.form(key="query_form", clear_on_submit=False):196 col_in, col_btn = st.columns([5, 1])197 with col_in:198 # Use chip query if clicked, otherwise empty199 default_val = st.session_state.pop("chip_query", "")200 user_query = st.text_input(201 "q", label_visibility="collapsed",202 placeholder="e.g. Why did PayPal deduct 7% from my payment?",203 value=default_val, key="main_input"204 )205 with col_btn:206 ask_clicked = st.form_submit_button("Ask →", width='stretch')207 208 209 # ── Helper: render answer text as clean HTML ──────────────210 def render_answer(text: str) -> str:211 """212 Converts LLM markdown-ish output → clean HTML for the answer card.213 Handles: **bold**, bullet lists, numbered lists, paragraphs.214 """215 # Strip confidence tag216 text = re.sub(r'\s*\[(High|Medium|Low|None)\]\s*$', '', text, flags=re.IGNORECASE).strip()217 218 lines = text.split("\n")219 html = ""220 in_ul = False221 in_ol = False222 223 for line in lines:224 line = line.strip()225 if not line:226 if in_ul: html += "</ul>"; in_ul = False227 if in_ol: html += "</ol>"; in_ol = False228 continue229 230 # Bold231 line = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', line)232 233 # Bullet list234 if re.match(r'^[-•*] ', line):235 if in_ol: html += "</ol>"; in_ol = False236 if not in_ul: html += "<ul>"; in_ul = True237 html += f"<li>{line[2:].strip()}</li>"238 continue239 240 # Numbered list241 if re.match(r'^\d+\. ', line):242 if in_ul: html += "</ul>"; in_ul = False243 if not in_ol: html += "<ol>"; in_ol = True244 html += f"<li>{re.sub(r'^\d+\. ', '', line)}</li>"245 continue246 247 # Close open lists248 if in_ul: html += "</ul>"; in_ul = False249 if in_ol: html += "</ol>"; in_ol = False250 251 html += f"<p>{line}</p>"252 253 if in_ul: html += "</ul>"254 if in_ol: html += "</ol>"255 return html256 257 258 # ── Process query ─────────────────────────────────────────259 # ── Process query ─────────────────────────────────────────260 if ask_clicked and user_query.strip():261 # Parse datetime info BEFORE spinner (so it shows immediately)262 dt_info = parse_datetime_query(user_query.strip())263 264 # Show datetime detection if present265 if dt_info['has_temporal']:266 st.info(f"📅 **Temporal context detected:** {', '.join(dt_info['temporal_refs'])}")267 with st.expander("ℹ️ Date Context", expanded=False):268 st.caption(dt_info['date_context'])269 270 with st.spinner("Thinking..."):271 try:272 chain = load_chain()273 guardrail_fn = get_guardrail_fn()274 result = guardrail_fn(user_query.strip(), chain.ask)275 st.session_state.result = result276 except Exception as e:277 st.error(f"Error: {str(e)}")278 st.stop()279 st.session_state.last_q = user_query.strip()280 281 if not result["blocked"]:282 clean_a = re.sub(283 r'\s*\[(High|Medium|Low|None)\]\s*$', '',284 result["answer"], flags=re.IGNORECASE285 ).strip()286 st.session_state.history.insert(0, {287 "q": user_query.strip(),288 "a": clean_a[:160] + "..." if len(clean_a) > 160 else clean_a,289 "confidence": result["confidence"]290 })291 292 293 # ── Render result ─────────────────────────────────────────294 result = st.session_state.result295 296 if result:297 if result["blocked"]:298 reason = result["guardrail_warnings"][0] if result["guardrail_warnings"] else "Request not supported."299 st.markdown(f'<div class="blocked">🚫 {reason}</div>', unsafe_allow_html=True)300 301 else:302 answer = result["answer"]303 confidence = result.get("confidence", "medium")304 sources = result.get("sources", [])305 latency = result.get("latency_ms", 0)306 warnings = result.get("guardrail_warnings", [])307 web_used = result.get("web_search_used", False)308 links = result.get("official_links", [])309 310 # ── Answer card ───────────────────────────────────311 rendered = render_answer(answer)312 st.markdown(f'<div class="ans-card">{rendered}</div>', unsafe_allow_html=True)313 314 # ── Badges ────────────────────────────────────────315 conf_map = {316 "high": ("b-high", "High Confidence"),317 "medium": ("b-medium", "Medium Confidence"),318 "low": ("b-low", "Low Confidence"),319 "none": ("b-none", "No Data"),320 }321 cls, label = conf_map.get(confidence.lower(), ("b-none", confidence))322 323 badges = f'<span class="badge {cls}">{label}</span>'324 325 if web_used:326 badges += ' <span class="badge b-web">🌐 Live Search</span>'327 328 clean_sources = [329 s for s in sources330 if s not in ("live_web_search",)331 ]332 for s in clean_sources:333 readable = s.replace("_", " ").replace("manual", "").strip().title()334 badges += f' <span class="badge b-src">📄 {readable}</span>'335 336 badges += f' <span class="badge b-time">⚡ {latency:.0f}ms</span>'337 338 st.markdown(f'<div class="badge-row">{badges}</div>', unsafe_allow_html=True)339 340 # ── Warnings ──────────────────────────────────────341 for w in warnings:342 st.markdown(f'<div class="warn">⚠️ {w}</div>', unsafe_allow_html=True)343 344 # ── Official links ────────────────────────────────345 if links:346 link_items = ""347 for url in links[:5]:348 domain = url.replace("https://","").replace("http://","").split("/")[0]349 link_items += f'<a class="ext-link" href="{url}" target="_blank">↗ {domain}</a>'350 st.markdown(f"""351 <div class="links-section">352 <div class="links-label">Verify at official sources</div>353 <div class="links-row">{link_items}</div>354 </div>355 """, unsafe_allow_html=True)356 357 358 # ── History ───────────────────────────────────────────────359 if st.session_state.history:360 st.markdown("<hr>", unsafe_allow_html=True)361 st.markdown('<div class="hist-label">Recent Questions</div>', unsafe_allow_html=True)362 for item in st.session_state.history[:5]:363 dot_color = {"high":"#6ee7b7","medium":"#fcd34d","low":"#fca5a5"}.get(item["confidence"],"#52526e")364 st.markdown(f"""365 <div class="hist-item">366 <div class="hist-q"><span style="color:{dot_color};margin-right:6px">●</span>{item['q']}</div>367 <div class="hist-a">{item['a']}</div>368 </div>""", unsafe_allow_html=True)369 370 371 # ── Footer ────────────────────────────────────────────────372 st.markdown("""373 <div style="text-align:center;margin-top:3rem;border-top:1px solid rgba(255,255,255,0.04);padding-top:1.5rem;">374 <p style="color:#2a2a3a;font-size:0.72rem;letter-spacing:0.06em;">375 PAYLENS · RAG + LIVE SEARCH · 376 LLAMA 3.1 via GROQ · FAISS · LANGCHAIN377 </p>378 </div>379 """, unsafe_allow_html=True)380 381# end of tab_main content ↑382 383with tab_eval:384 sys.path.insert(0, str(Path(__file__).parent.parent / "eval"))385 from metrics_dashboard import render_dashboard386 render_dashboard()