import sqlite3 from datetime import datetime from pathlib import Path import gradio as gr from ml_utils import ScamDetectionService detector = ScamDetectionService() # ── DB ──────────────────────────────────────────────────────────────────────── DB_PATH = Path("feedback.db") def init_db(): conn = sqlite3.connect(DB_PATH) conn.execute(""" CREATE TABLE IF NOT EXISTS feedback ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, content_type TEXT NOT NULL, prediction TEXT NOT NULL, confidence REAL NOT NULL, user_agreed INTEGER NOT NULL, timestamp TEXT NOT NULL ) """) conn.commit() conn.close() init_db() def log_feedback(content, content_type, prediction, confidence, agreed): try: conn = sqlite3.connect(DB_PATH) conn.execute( "INSERT INTO feedback VALUES (NULL,?,?,?,?,?,?)", (str(content)[:2000], content_type, prediction, float(confidence), int(agreed), datetime.utcnow().isoformat()) ) conn.commit() conn.close() except Exception as e: print(f"[db] {e}") def get_stats(): try: conn = sqlite3.connect(DB_PATH) total = conn.execute("SELECT COUNT(*) FROM feedback").fetchone()[0] agreed = conn.execute("SELECT COUNT(*) FROM feedback WHERE user_agreed=1").fetchone()[0] conn.close() return total, (round(agreed / total * 100, 1) if total else 0.0) except: return 0, 0.0 # ── Styles ──────────────────────────────────────────────────────────────────── STYLES = { "Scam": {"accent": "#E05252", "bg": "#1C0E0E", "border": "#5C1F1F", "badge": "SCAM"}, "Suspicious": {"accent": "#D4924A", "bg": "#1C1408", "border": "#5C3E10", "badge": "SUSPICIOUS"}, "Safe": {"accent": "#4CAF7D", "bg": "#0C1C13", "border": "#1A5C35", "badge": "LOOKS SAFE"}, } def result_html(s, meta_rows, user_message): return f"""
{s['badge']}
{meta_rows}
{user_message}
""" EMPTY = "
Enter a value above to analyze.
" SAVED = "
Feedback saved. Thanks.
" # ── State: last result for feedback ────────────────────────────────────────── # We store last prediction in Gradio State so the feedback buttons can read it. def analyze_text(text): if not text or not text.strip(): return EMPTY, gr.update(visible=False), gr.update(visible=False), {} r = detector.analyze_text_scam(text) risk = r['risk_level'] conf = r['confidence'] lang = r.get('detected_language', 'en').upper() msg = r.get('user_message', r['reasoning']) s = STYLES.get(risk, STYLES["Suspicious"]) meta = ( f"Confidence  {conf:.0%}
" f"Language   {lang}
" ) html = result_html(s, meta, msg) state = {"content": text, "content_type": "text", "prediction": risk, "confidence": conf} return html, gr.update(visible=True), gr.update(visible=False), state def analyze_url(url, context): if not url or not url.strip(): return EMPTY, gr.update(visible=False), gr.update(visible=False), {} r = detector.analyze_url_scam(url, context) risk = r['risk_level'] conf = r['confidence'] domain = r['domain'] msg = r.get('user_message', r['reasoning']) s = STYLES.get(risk, STYLES["Suspicious"]) meta = ( f"Confidence  {conf:.0%}
" f"Domain     {domain}
" ) html = result_html(s, meta, msg) state = {"content": url, "content_type": "url", "prediction": risk, "confidence": conf} return html, gr.update(visible=True), gr.update(visible=False), state def on_yes(state): if state: log_feedback(state["content"], state["content_type"], state["prediction"], state["confidence"], agreed=1) total, rate = get_stats() return gr.update(visible=False), gr.update(visible=True), f"{SAVED}
{total} total · {rate}% agreement
" return gr.update(visible=False), gr.update(visible=True), SAVED def on_no(state): if state: log_feedback(state["content"], state["content_type"], state["prediction"], state["confidence"], agreed=0) total, rate = get_stats() return gr.update(visible=False), gr.update(visible=True), f"{SAVED}
{total} total · {rate}% agreement
" return gr.update(visible=False), gr.update(visible=True), SAVED # ── CSS ─────────────────────────────────────────────────────────────────────── css = """ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@400;500&display=swap'); *, *::before, *::after { box-sizing: border-box; } body, .gradio-container { background: #111 !important; font-family: 'IBM Plex Sans', sans-serif !important; color: #c8c8c8 !important; } .gradio-container { max-width: 660px !important; margin: 0 auto !important; padding: 32px 16px !important; } .gr-markdown h1 { font-family: 'IBM Plex Mono', monospace !important; font-size: 1.4rem !important; font-weight: 600 !important; color: #e8e8e8 !important; letter-spacing: -0.01em !important; margin-bottom: 4px !important; } .gr-markdown p { color: #666 !important; font-size: 0.82rem !important; font-family: 'IBM Plex Mono', monospace !important; line-height: 1.6 !important; } textarea, input[type=text] { background: #181818 !important; border: 1px solid #2e2e2e !important; color: #d8d8d8 !important; font-family: 'IBM Plex Sans', sans-serif !important; font-size: 0.88rem !important; border-radius: 6px !important; transition: border-color 0.15s ease !important; } textarea:focus, input[type=text]:focus { border-color: #444 !important; box-shadow: none !important; outline: none !important; } label span, .gr-form label span { color: #555 !important; font-size: 0.72rem !important; font-weight: 500 !important; text-transform: uppercase !important; letter-spacing: 0.07em !important; font-family: 'IBM Plex Mono', monospace !important; } button.primary, .gr-button-primary { background: #e8e8e8 !important; color: #111 !important; border: none !important; font-family: 'IBM Plex Mono', monospace !important; font-weight: 600 !important; font-size: 0.78rem !important; letter-spacing: 0.07em !important; text-transform: uppercase !important; border-radius: 5px !important; padding: 10px 22px !important; transition: background 0.12s ease !important; cursor: pointer !important; } button.primary:hover, .gr-button-primary:hover { background: #fff !important; } /* Feedback buttons */ .fb-yes { background: #1a2a1a !important; border: 1px solid #2a4a2a !important; color: #4CAF7D !important; font-size: 0.78rem !important; padding: 4px 14px !important; border-radius: 4px !important; } .fb-no { background: #2a1a1a !important; border: 1px solid #4a2a2a !important; color: #E05252 !important; font-size: 0.78rem !important; padding: 4px 14px !important; border-radius: 4px !important; } .tab-nav { border-bottom: 1px solid #252525 !important; margin-bottom: 20px !important; } .tab-nav button { font-family: 'IBM Plex Mono', monospace !important; font-size: 0.72rem !important; color: #555 !important; background: transparent !important; border: none !important; border-bottom: 2px solid transparent !important; text-transform: uppercase !important; letter-spacing: 0.07em !important; padding: 8px 16px !important; cursor: pointer !important; transition: color 0.12s ease !important; } .tab-nav button.selected { color: #d8d8d8 !important; border-bottom-color: #d8d8d8 !important; } .tab-nav button:hover:not(.selected) { color: #999 !important; } .gr-examples { margin-top: 12px !important; } .gr-examples table { border: none !important; background: transparent !important; } .gr-examples td, .gr-examples th { background: #181818 !important; border: 1px solid #252525 !important; color: #888 !important; font-size: 0.78rem !important; font-family: 'IBM Plex Mono', monospace !important; padding: 6px 12px !important; cursor: pointer !important; transition: background 0.1s ease !important; } .gr-examples tr:hover td { background: #202020 !important; color: #bbb !important; } .gr-form, .gr-box, .gr-block, .gr-panel { background: transparent !important; border: none !important; box-shadow: none !important; } .tabitem { padding: 0 !important; } hr { border-color: #222 !important; margin: 24px 0 !important; } """ # ── UI ──────────────────────────────────────────────────────────────────────── with gr.Blocks(css=css, title="Scam Detector") as demo: gr.Markdown("# Scam Detector") gr.Markdown("Paste a suspicious message or URL. Results are flagged as Safe, Suspicious, or Scam.") with gr.Tab("Text / SMS"): text_state = gr.State({}) text_input = gr.Textbox( label="Message", placeholder="Hi, this is from your bank's fraud prevention team...", lines=4 ) text_btn = gr.Button("Analyze", variant="primary") text_out = gr.HTML(EMPTY) with gr.Row(visible=False) as text_fb_row: gr.HTML("Was this correct?") text_yes = gr.Button("Yes", elem_classes=["fb-yes"]) text_no = gr.Button("No", elem_classes=["fb-no"]) text_fb_msg = gr.HTML(visible=False) text_btn.click( analyze_text, inputs=text_input, outputs=[text_out, text_fb_row, text_fb_msg, text_state] ) text_yes.click(on_yes, inputs=text_state, outputs=[text_fb_row, text_fb_msg, text_fb_msg]) text_no.click( on_no, inputs=text_state, outputs=[text_fb_row, text_fb_msg, text_fb_msg]) gr.Examples( examples=[ ["CONGRATULATIONS! You've WON 1000! Click here to claim NOW!"], ["Hi, this is Rahul from HDFC fraud monitoring. We noticed a Rs.18,420 charge. Confirm here: https://hdfc-secureverify.co"], ["Your KYC is expiring in 24 hours. Update now to avoid account suspension."], ["Your Aadhaar is being used to open bank accounts. Immediate verification required."], ["Amazon: Your package will arrive tomorrow between 2-5 PM."], ["Hey, want to grab coffee tomorrow at 3pm?"], ], inputs=text_input, label="Try these examples" ) with gr.Tab("URL / Link"): url_state = gr.State({}) url_input = gr.Textbox(label="URL", placeholder="http://paypa1-secure.tk/verify") ctx_input = gr.Textbox( label="Message context (optional)", placeholder="Your account has been suspended. Verify now.", lines=2 ) url_btn = gr.Button("Analyze", variant="primary") url_out = gr.HTML(EMPTY) with gr.Row(visible=False) as url_fb_row: gr.HTML("Was this correct?") url_yes = gr.Button("Yes", elem_classes=["fb-yes"]) url_no = gr.Button("No", elem_classes=["fb-no"]) url_fb_msg = gr.HTML(visible=False) url_btn.click( analyze_url, inputs=[url_input, ctx_input], outputs=[url_out, url_fb_row, url_fb_msg, url_state] ) url_yes.click(on_yes, inputs=url_state, outputs=[url_fb_row, url_fb_msg, url_fb_msg]) url_no.click( on_no, inputs=url_state, outputs=[url_fb_row, url_fb_msg, url_fb_msg]) gr.Examples( examples=[ ["http://paypa1-secure.tk/verify", "Your account has been suspended"], ["https://netflix-payment-failed-verify-account.info", ""], ["http://crypto-investment-double-money-fast.site", ""], ["https://www.google.com", ""], ["https://bluedart-track-update.net", "Your courier could not be delivered"], ], inputs=[url_input, ctx_input], label="Try these examples" ) with gr.Row(): stats_btn = gr.Button("Show feedback stats", size="sm") stats_out = gr.HTML() stats_btn.click( lambda: (lambda t, r: f"
{t} feedback logged · {r}% agreement
")(*get_stats()), outputs=stats_out ) gr.Markdown( "TF-IDF + LR (text) · 3-model URL ensemble (LR + RF + XGBoost) · " "India-specific dataset (500 msgs + 250 URLs) · " "[GitHub](https://github.com/SD1920/ScamDetector)" ) if __name__ == "__main__": demo.launch()