CoolFace
Apppublic

Area51base/cc-chatbot

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
streamlit_app.py382 linesDownload Raw Back to ui
1"""2Credit Card AI Chatbot — Streamlit UI3 4Run with:5  streamlit run ui/streamlit_app.py6 7Connects to the FastAPI backend at http://localhost:80008Falls back to direct import if the API is not running.9"""10 11import os12import sys13import uuid14import json15import time16from typing import Optional17 18import streamlit as st19import httpx20 21# ── Path setup for direct import fallback ─────────────────────────────────────22sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))23 24API_BASE = os.getenv("CC_API_URL", "http://localhost:8000")25DIRECT_MODE = False   # switched to True if API is unreachable26 27# ── Page config ───────────────────────────────────────────────────────────────28st.set_page_config(29    page_title="CC AI Assistant",30    page_icon="💳",31    layout="centered",32    initial_sidebar_state="collapsed",33)34 35# ── Custom CSS ────────────────────────────────────────────────────────────────36st.markdown("""37<style>38/* Chat bubbles */39.user-bubble {40    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);41    color: white;42    padding: 12px 16px;43    border-radius: 18px 18px 4px 18px;44    margin: 8px 0 8px 20%;45    max-width: 80%;46    float: right;47    clear: both;48    font-size: 0.95rem;49    box-shadow: 0 2px 8px rgba(0,0,0,0.15);50}51.assistant-bubble {52    background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf0 100%);53    color: #1a1a2e;54    padding: 14px 18px;55    border-radius: 18px 18px 18px 4px;56    margin: 8px 20% 8px 0;57    max-width: 80%;58    float: left;59    clear: both;60    font-size: 0.95rem;61    box-shadow: 0 2px 8px rgba(0,0,0,0.1);62    border-left: 3px solid #667eea;63}64.chat-container {65    display: flex;66    flex-direction: column;67    gap: 4px;68    padding: 10px 0;69}70.meta-badge {71    display: inline-block;72    background: #e8f4fd;73    color: #0366d6;74    padding: 2px 8px;75    border-radius: 10px;76    font-size: 0.75rem;77    margin: 2px;78}79.suggestion-btn {80    background: #f0f7ff;81    border: 1px solid #c6ddf0;82    border-radius: 8px;83    padding: 6px 12px;84    margin: 4px;85    cursor: pointer;86    font-size: 0.85rem;87    color: #0366d6;88}89.header-gradient {90    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);91    color: white;92    padding: 20px;93    border-radius: 12px;94    text-align: center;95    margin-bottom: 20px;96}97.clearfix { clear: both; }98/* Hide sidebar and its toggle button completely */99[data-testid="stSidebar"] { display: none !important; }100[data-testid="collapsedControl"] { display: none !important; }101pre {102    background: #f6f8fa;103    border: 1px solid #e1e4e8;104    border-radius: 6px;105    padding: 12px;106    white-space: pre-wrap;107    word-wrap: break-word;108    font-size: 0.9rem;109}110</style>111""", unsafe_allow_html=True)112 113 114# ── Session state init ────────────────────────────────────────────────────────115if "session_id" not in st.session_state:116    st.session_state.session_id = str(uuid.uuid4())117if "messages" not in st.session_state:118    st.session_state.messages = []119if "suggestions" not in st.session_state:120    st.session_state.suggestions = []121if "last_intent" not in st.session_state:122    st.session_state.last_intent = None123 124 125api_url = API_BASE126 127# ── Warm-up banner (shown on first load while ingestion runs in background) ────128def _api_ready() -> bool:129    try:130        r = httpx.get(f"{api_url}/health", timeout=2)131        return r.status_code == 200132    except Exception:133        return False134 135if not _api_ready():136    st.info(137        "⏳ **First-time setup in progress** — loading credit card data and "138        "starting AI engine (~2 min on first launch). "139        "You can explore the UI; the first query may take a moment longer.",140        icon="🚀",141    )142 143# ── Header ────────────────────────────────────────────────────────────────────144col_h, col_btn = st.columns([5, 1])145with col_h:146    st.markdown("""147<div class="header-gradient">148    <h2 style="margin:0">💳 Credit Card AI Assistant</h2>149    <p style="margin:4px 0 0 0; opacity:0.9; font-size:0.9rem">150        Find the best cards · Compare rewards · Plan redemptions151    </p>152</div>153""", unsafe_allow_html=True)154with col_btn:155    st.markdown("<div style='padding-top:18px'></div>", unsafe_allow_html=True)156    if st.button("🔄 New", help="Start a new conversation", use_container_width=True):157        st.session_state.session_id = str(uuid.uuid4())158        st.session_state.messages = []159        st.session_state.suggestions = []160        st.session_state.last_intent = None161        st.rerun()162 163# ── Example questions (shown only when chat is empty) ─────────────────────────164if not st.session_state.messages:165    st.markdown("**💡 Try asking:**")166    example_questions = [167        "Best card for Swiggy food orders?",168        "Which card for international travel?",169        "I have 20,000 HDFC Infinia points, want to go to Spain",170        "Compare HDFC Infinia vs Diners Club Black",171        "Best no-fee credit card with good rewards",172        "Which card gives unlimited lounge access?",173        "Best cashback card for Amazon shopping",174        "HDFC Regalia rewards and benefits",175    ]176    cols = st.columns(2)177    for i, eq in enumerate(example_questions):178        if cols[i % 2].button(eq, key=f"ex_{i}", use_container_width=True):179            st.session_state.pending_question = eq180 181 182# ── Chat history display ───────────────────────────────────────────────────────183def render_message(role: str, content: str, meta: Optional[dict] = None):184    if role == "user":185        st.markdown(186            f'<div class="user-bubble">{content}</div><div class="clearfix"></div>',187            unsafe_allow_html=True,188        )189    else:190        # Format assistant answer nicely191        formatted = _format_answer(content)192        badges = ""193        if meta:194            if meta.get("intent"):195                badges += f'<span class="meta-badge">🎯 {meta["intent"]}</span>'196            if meta.get("category"):197                badges += f'<span class="meta-badge">📂 {meta["category"]}</span>'198            if meta.get("cache_hit"):199                badges += f'<span class="meta-badge">⚡ Cached ({meta.get("similarity", 0):.2f})</span>'200        st.markdown(201            f'<div class="assistant-bubble">{badges}<br>{formatted}</div><div class="clearfix"></div>',202            unsafe_allow_html=True,203        )204 205 206def _format_answer(text: str) -> str:207    """Convert plain text answer to styled HTML."""208    import html209    import re210    # Escape HTML211    safe = html.escape(text)212    # Bold **text**213    safe = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", safe)214    # Markdown table rows: | col | col | → render as an HTML table215    if "|" in safe:216        lines = safe.split("\n")217        result_lines = []218        in_table = False219        table_rows = []220        for line in lines:221            stripped = line.strip()222            if stripped.startswith("|") and stripped.endswith("|"):223                if not in_table:224                    in_table = True225                    table_rows = []226                # Skip separator rows like |---|---|227                if re.match(r"^\|[\s\-|:]+\|$", stripped):228                    continue229                cells = [c.strip() for c in stripped.strip("|").split("|")]230                table_rows.append(cells)231            else:232                if in_table:233                    # Flush table234                    if table_rows:235                        html_table = '<table style="border-collapse:collapse;width:100%;margin:8px 0;font-size:0.88rem">'236                        for i, row in enumerate(table_rows):237                            tag = "th" if i == 0 else "td"238                            style_row = ' style="background:#f0f4f8"' if i == 0 else (' style="background:#fafafa"' if i % 2 == 0 else "")239                            html_table += f"<tr{style_row}>"240                            for cell in row:241                                html_table += f'<{tag} style="border:1px solid #ddd;padding:6px 10px">{cell}</{tag}>'242                            html_table += "</tr>"243                        html_table += "</table>"244                        result_lines.append(html_table)245                    in_table = False246                    table_rows = []247                result_lines.append(line)248        if in_table and table_rows:249            html_table = '<table style="border-collapse:collapse;width:100%;margin:8px 0;font-size:0.88rem">'250            for i, row in enumerate(table_rows):251                tag = "th" if i == 0 else "td"252                style_row = ' style="background:#f0f4f8"' if i == 0 else (' style="background:#fafafa"' if i % 2 == 0 else "")253                html_table += f"<tr{style_row}>"254                for cell in row:255                    html_table += f'<{tag} style="border:1px solid #ddd;padding:6px 10px">{cell}</{tag}>'256                html_table += "</tr>"257            html_table += "</table>"258            result_lines.append(html_table)259        safe = "\n".join(result_lines)260    # Numbered list items261    safe = re.sub(r"^(\d+)\.\s+", r"<br><strong>\1.</strong> ", safe, flags=re.MULTILINE)262    # Bullet / checkmark lines263    safe = re.sub(r"^✅\s+", r"<br>✅ ", safe, flags=re.MULTILINE)264    safe = re.sub(r"^[-•]\s+", r"<br>• ", safe, flags=re.MULTILINE)265    # Section headers with ──266    safe = re.sub(r"^(──.+)$", r"<br><strong>\1</strong>", safe, flags=re.MULTILINE)267    # Bold "Verdict:" line268    safe = re.sub(r"^(Verdict:)", r"<br><strong>Verdict:</strong>", safe, flags=re.MULTILINE)269    safe = re.sub(r"^(Pro tip:)", r"<br><strong>💡 Pro tip:</strong>", safe, flags=re.MULTILINE)270    # Section headers with emoji (🎁 ✈️ etc)271    safe = re.sub(r"^([🎁✈️🍽💳⭐🏦💰🛍⛽🏥🎬][^\n]+)$", r"<br><strong>\1</strong>", safe, flags=re.MULTILINE)272    # Newlines273    safe = safe.replace("\n", "<br>")274    return safe275 276 277# Render existing messages278with st.container():279    for msg in st.session_state.messages:280        render_message(281            role=msg["role"],282            content=msg["content"],283            meta=msg.get("meta"),284        )285 286# ── Follow-up suggestions ──────────────────────────────────────────────────────287if st.session_state.suggestions:288    st.markdown("**💬 You might also ask:**")289    cols = st.columns(len(st.session_state.suggestions[:3]))290    for i, suggestion in enumerate(st.session_state.suggestions[:3]):291        if cols[i].button(suggestion, key=f"sug_{i}_{suggestion[:20]}"):292            st.session_state.pending_question = suggestion293 294 295# ── Handle pending question (from sidebar or suggestions) ─────────────────────296if "pending_question" in st.session_state:297    pending = st.session_state.pop("pending_question")298    st.session_state.pending_send = pending299 300 301# ── Chat input ────────────────────────────────────────────────────────────────302user_input = st.chat_input(303    "Ask about credit cards — rewards, travel, dining, comparisons...",304    key="chat_input",305)306 307# Resolve input (typed or from pending)308question = user_input309if hasattr(st.session_state, "pending_send"):310    question = st.session_state.pop("pending_send")311 312 313def _send_question(q: str):314    """Send the question to API or direct service and update state."""315    # Show user message immediately316    st.session_state.messages.append({"role": "user", "content": q})317    st.session_state.suggestions = []318 319    with st.spinner("Thinking..."):320        try:321            response = httpx.post(322                f"{api_url}/api/chat",323                json={"question": q, "session_id": st.session_state.session_id},324                timeout=60,325            )326            response.raise_for_status()327            data = response.json()328 329            answer = data.get("answer", "Sorry, I couldn't generate an answer.")330            meta = {331                "intent": data.get("intent"),332                "category": data.get("category"),333                "cache_hit": data.get("cache_hit", False),334                "similarity": data.get("similarity", 0),335            }336            st.session_state.session_id = data.get("session_id", st.session_state.session_id)337            st.session_state.last_intent = data.get("intent")338            st.session_state.suggestions = data.get("suggestions", [])339 340        except httpx.ConnectError:341            # Fallback: direct import342            try:343                from cc_chatbot.services.orchestrator import ask344                data = ask(question=q, session_id=st.session_state.session_id)345                answer = data.get("answer", "")346                meta = {347                    "intent": data.get("intent"),348                    "category": data.get("category"),349                    "cache_hit": data.get("cache_hit", False),350                    "similarity": data.get("similarity", 0),351                }352                st.session_state.session_id = data.get("session_id", st.session_state.session_id)353                st.session_state.suggestions = data.get("suggestions", [])354            except Exception as e:355                answer = f"❌ Error: {e}\n\nMake sure the API server is running:\n`uvicorn cc_chatbot.main:app --reload`"356                meta = {}357 358        except Exception as e:359            answer = f"❌ Request error: {e}"360            meta = {}361 362    st.session_state.messages.append({363        "role": "assistant",364        "content": answer,365        "meta": meta,366    })367 368 369if question and question.strip():370    _send_question(question.strip())371    st.rerun()372 373 374# ── Footer ────────────────────────────────────────────────────────────────────375st.markdown("---")376st.markdown(377    "<p style='text-align:center; color:#999; font-size:0.8rem;'>"378    "💳 CC AI Assistant • Data from Livemint • Powered by Gemini"379    "</p>",380    unsafe_allow_html=True,381)382