Scam-Detector / app.py
Sanjam19's picture
Fixing Feedback Loop
72033cc verified
Raw
History Blame Contribute Delete
15 kB
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"""
<div style="
background:{s['bg']};border:1px solid {s['border']};border-radius:8px;
padding:20px 24px;font-family:'IBM Plex Mono',monospace;margin-top:4px;
">
<div style="
display:inline-block;font-size:0.78rem;font-weight:700;color:{s['accent']};
letter-spacing:0.08em;text-transform:uppercase;
border:1px solid {s['border']};border-radius:4px;
padding:3px 10px;margin-bottom:14px;
">{s['badge']}</div>
<div style="color:#c8c8c8;font-size:0.85rem;line-height:1.75;">
{meta_rows}
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #2a2a2a;
color:#aaa;font-size:0.82rem;line-height:1.7;">
{user_message}
</div>
</div>
</div>"""
EMPTY = "<div style='color:#666;padding:16px;font-family:\"IBM Plex Mono\",monospace;font-size:0.85rem;'>Enter a value above to analyze.</div>"
SAVED = "<div style='color:#4CAF7D;font-family:\"IBM Plex Mono\",monospace;font-size:0.78rem;padding:6px 0;'>Feedback saved. Thanks.</div>"
# ── 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"<span style='color:#666;'>Confidence</span>&nbsp;&nbsp;{conf:.0%}<br>"
f"<span style='color:#666;'>Language&nbsp;</span>&nbsp;&nbsp;{lang}<br>"
)
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"<span style='color:#666;'>Confidence</span>&nbsp;&nbsp;{conf:.0%}<br>"
f"<span style='color:#666;'>Domain&nbsp;&nbsp;&nbsp;</span>&nbsp;&nbsp;{domain}<br>"
)
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}<div style='color:#555;font-family:IBM Plex Mono,monospace;font-size:0.72rem;padding:2px 0;'>{total} total Β· {rate}% agreement</div>"
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}<div style='color:#555;font-family:IBM Plex Mono,monospace;font-size:0.72rem;padding:2px 0;'>{total} total Β· {rate}% agreement</div>"
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("<span style='font-family:IBM Plex Mono,monospace;font-size:0.75rem;color:#555;'>Was this correct?</span>")
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("<span style='font-family:IBM Plex Mono,monospace;font-size:0.75rem;color:#555;'>Was this correct?</span>")
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"<div style='font-family:IBM Plex Mono,monospace;font-size:0.75rem;color:#555;padding:4px 0;'>{t} feedback logged &middot; {r}% agreement</div>")(*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()