Abid Ali Awan codex commited on
Commit ·
25f0a2d
1
Parent(s): ad3a3d5
Cache built-in example assessments
Browse filesCo-Authored-By: codex <codex@openai.com>
- README.md +8 -3
- app.py +129 -3
- static/app.js +15 -2
- static/index.html +6 -6
README.md
CHANGED
|
@@ -84,9 +84,14 @@ See [local model setup](docs/local_model_setup.md) and
|
|
| 84 |
The app sends text and optional image data to the configured multimodal
|
| 85 |
OpenAI-compatible endpoint and validates its structured response.
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
a
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
## Architecture
|
| 92 |
|
|
|
|
| 84 |
The app sends text and optional image data to the configured multimodal
|
| 85 |
OpenAI-compatible endpoint and validates its structured response.
|
| 86 |
|
| 87 |
+
The six built-in text and screenshot examples use precomputed assessments.
|
| 88 |
+
Trying those examples does not call or wake the Modal endpoint. Editing an
|
| 89 |
+
example or uploading a different image switches back to normal model analysis.
|
| 90 |
+
|
| 91 |
+
There is no rule-based or sample fallback for user-submitted input. If
|
| 92 |
+
credentials are missing, the endpoint is unavailable, or the model returns
|
| 93 |
+
invalid output, the app displays a clear error and does not manufacture an
|
| 94 |
+
assessment.
|
| 95 |
|
| 96 |
## Architecture
|
| 97 |
|
app.py
CHANGED
|
@@ -36,6 +36,116 @@ REQUIRED_FIELDS = {
|
|
| 36 |
"safe_next_steps",
|
| 37 |
"reply_draft",
|
| 38 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
SYSTEM_PROMPT = """You help people in Pakistan assess notices and messages.
|
| 41 |
Return only JSON matching the supplied schema. Use simple, calm English.
|
|
@@ -251,10 +361,20 @@ def call_model(text: str, image_data_url: str) -> dict[str, Any]:
|
|
| 251 |
raise RuntimeError("Model request ended without a response.")
|
| 252 |
|
| 253 |
|
| 254 |
-
def analyze_notice(
|
|
|
|
|
|
|
| 255 |
"""Analyze supplied text/image using the configured model only."""
|
| 256 |
text = (text or "").strip()
|
| 257 |
image_data_url = image_data_url or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
if not text and not image_data_url:
|
| 259 |
return {
|
| 260 |
"ok": False,
|
|
@@ -298,8 +418,10 @@ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
| 298 |
|
| 299 |
|
| 300 |
@app.api(name="analyze", description="Assess a notice for common scam signals.", concurrency_limit=1)
|
| 301 |
-
def analyze_api(
|
| 302 |
-
|
|
|
|
|
|
|
| 303 |
|
| 304 |
|
| 305 |
@app.api(name="status", description="Return model and privacy status.", queue=False)
|
|
@@ -351,6 +473,10 @@ def run_self_tests() -> None:
|
|
| 351 |
}
|
| 352 |
)
|
| 353 |
assert inappropriate["reply_draft"] == ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
assert analyze_notice("", "")["ok"] is False
|
| 355 |
try:
|
| 356 |
normalize_assessment({"risk_label": "Looks normal"})
|
|
|
|
| 36 |
"safe_next_steps",
|
| 37 |
"reply_draft",
|
| 38 |
}
|
| 39 |
+
EXAMPLE_ASSESSMENTS: dict[str, dict[str, Any]] = {
|
| 40 |
+
"text-courier": {
|
| 41 |
+
"risk_label": "Likely scam",
|
| 42 |
+
"simple_explanation": (
|
| 43 |
+
"This message uses an urgent parcel fee and an unofficial-looking "
|
| 44 |
+
"link to pressure the recipient into paying."
|
| 45 |
+
),
|
| 46 |
+
"red_flags": [
|
| 47 |
+
"Unexpected delivery fee",
|
| 48 |
+
"Threat that the parcel will be destroyed",
|
| 49 |
+
"Link does not clearly belong to Pakistan Post",
|
| 50 |
+
],
|
| 51 |
+
"safe_next_steps": [
|
| 52 |
+
"Do not open the link or make the payment.",
|
| 53 |
+
"Check the parcel through the courier's independently found official website.",
|
| 54 |
+
"Report the message as spam or phishing.",
|
| 55 |
+
],
|
| 56 |
+
"reply_draft": "",
|
| 57 |
+
},
|
| 58 |
+
"text-fbr": {
|
| 59 |
+
"risk_label": "Likely scam",
|
| 60 |
+
"simple_explanation": (
|
| 61 |
+
"An unexpected tax refund that asks for CNIC and bank-card details "
|
| 62 |
+
"is a common identity and payment phishing pattern."
|
| 63 |
+
),
|
| 64 |
+
"red_flags": [
|
| 65 |
+
"Unexpected refund offer",
|
| 66 |
+
"Request for CNIC and card details",
|
| 67 |
+
"Pressure to act today",
|
| 68 |
+
],
|
| 69 |
+
"safe_next_steps": [
|
| 70 |
+
"Do not submit personal or banking information.",
|
| 71 |
+
"Check your tax account by navigating to the official FBR website yourself.",
|
| 72 |
+
"Report the sender through an official complaint channel.",
|
| 73 |
+
],
|
| 74 |
+
"reply_draft": "",
|
| 75 |
+
},
|
| 76 |
+
"text-bank": {
|
| 77 |
+
"risk_label": "Likely scam",
|
| 78 |
+
"simple_explanation": (
|
| 79 |
+
"A bank or support representative should never ask you to share an "
|
| 80 |
+
"OTP. The account-suspension threat is intended to create panic."
|
| 81 |
+
),
|
| 82 |
+
"red_flags": [
|
| 83 |
+
"Direct request for an OTP",
|
| 84 |
+
"Threat of immediate account suspension",
|
| 85 |
+
"Pressure to bypass normal bank security",
|
| 86 |
+
],
|
| 87 |
+
"safe_next_steps": [
|
| 88 |
+
"Do not share the OTP or reply to the sender.",
|
| 89 |
+
"Contact the bank using the number on your card or official app.",
|
| 90 |
+
"Change credentials immediately if an OTP or password was already shared.",
|
| 91 |
+
],
|
| 92 |
+
"reply_draft": "",
|
| 93 |
+
},
|
| 94 |
+
"image-courier": {
|
| 95 |
+
"risk_label": "Likely scam",
|
| 96 |
+
"simple_explanation": (
|
| 97 |
+
"This sample screenshot shows a fake delivery-fee message designed "
|
| 98 |
+
"to send the recipient to an untrusted payment link."
|
| 99 |
+
),
|
| 100 |
+
"red_flags": [
|
| 101 |
+
"Unexpected courier payment request",
|
| 102 |
+
"Urgent delivery language",
|
| 103 |
+
"Untrusted link in the message",
|
| 104 |
+
],
|
| 105 |
+
"safe_next_steps": [
|
| 106 |
+
"Do not use the link in the screenshot.",
|
| 107 |
+
"Verify any shipment through the courier's official tracking page.",
|
| 108 |
+
"Block and report the sender.",
|
| 109 |
+
],
|
| 110 |
+
"reply_draft": "",
|
| 111 |
+
},
|
| 112 |
+
"image-mobile": {
|
| 113 |
+
"risk_label": "Likely scam",
|
| 114 |
+
"simple_explanation": (
|
| 115 |
+
"This sample screenshot uses a fake mobile-related offer or alert "
|
| 116 |
+
"to prompt unsafe contact or disclosure of personal information."
|
| 117 |
+
),
|
| 118 |
+
"red_flags": [
|
| 119 |
+
"Unsolicited mobile-related claim",
|
| 120 |
+
"Pressure to respond outside an official app",
|
| 121 |
+
"Unverified sender details",
|
| 122 |
+
],
|
| 123 |
+
"safe_next_steps": [
|
| 124 |
+
"Do not reply or share account information.",
|
| 125 |
+
"Check the claim in the mobile operator's official app or website.",
|
| 126 |
+
"Report and block the sender.",
|
| 127 |
+
],
|
| 128 |
+
"reply_draft": "",
|
| 129 |
+
},
|
| 130 |
+
"image-traffic": {
|
| 131 |
+
"risk_label": "Likely scam",
|
| 132 |
+
"simple_explanation": (
|
| 133 |
+
"This sample screenshot imitates an e-challan notice and pressures "
|
| 134 |
+
"the recipient to use an unverified payment route."
|
| 135 |
+
),
|
| 136 |
+
"red_flags": [
|
| 137 |
+
"Unexpected traffic fine",
|
| 138 |
+
"Unverified payment instructions",
|
| 139 |
+
"Urgency intended to discourage independent checks",
|
| 140 |
+
],
|
| 141 |
+
"safe_next_steps": [
|
| 142 |
+
"Do not pay through the message link.",
|
| 143 |
+
"Check the challan on the relevant official traffic authority website.",
|
| 144 |
+
"Report the message if no official record exists.",
|
| 145 |
+
],
|
| 146 |
+
"reply_draft": "",
|
| 147 |
+
},
|
| 148 |
+
}
|
| 149 |
|
| 150 |
SYSTEM_PROMPT = """You help people in Pakistan assess notices and messages.
|
| 151 |
Return only JSON matching the supplied schema. Use simple, calm English.
|
|
|
|
| 361 |
raise RuntimeError("Model request ended without a response.")
|
| 362 |
|
| 363 |
|
| 364 |
+
def analyze_notice(
|
| 365 |
+
text: str = "", image_data_url: str = "", example_id: str = ""
|
| 366 |
+
) -> dict[str, Any]:
|
| 367 |
"""Analyze supplied text/image using the configured model only."""
|
| 368 |
text = (text or "").strip()
|
| 369 |
image_data_url = image_data_url or ""
|
| 370 |
+
example_id = (example_id or "").strip()
|
| 371 |
+
if example_id in EXAMPLE_ASSESSMENTS:
|
| 372 |
+
return {
|
| 373 |
+
"ok": True,
|
| 374 |
+
"assessment": dict(EXAMPLE_ASSESSMENTS[example_id]),
|
| 375 |
+
"status": model_status(),
|
| 376 |
+
"source": "cached_example",
|
| 377 |
+
}
|
| 378 |
if not text and not image_data_url:
|
| 379 |
return {
|
| 380 |
"ok": False,
|
|
|
|
| 418 |
|
| 419 |
|
| 420 |
@app.api(name="analyze", description="Assess a notice for common scam signals.", concurrency_limit=1)
|
| 421 |
+
def analyze_api(
|
| 422 |
+
text: str = "", image_data_url: str = "", example_id: str = ""
|
| 423 |
+
) -> dict[str, Any]:
|
| 424 |
+
return analyze_notice(text, image_data_url, example_id)
|
| 425 |
|
| 426 |
|
| 427 |
@app.api(name="status", description="Return model and privacy status.", queue=False)
|
|
|
|
| 473 |
}
|
| 474 |
)
|
| 475 |
assert inappropriate["reply_draft"] == ""
|
| 476 |
+
cached = analyze_notice(example_id="text-bank")
|
| 477 |
+
assert cached["ok"] is True
|
| 478 |
+
assert cached["source"] == "cached_example"
|
| 479 |
+
assert cached["assessment"]["risk_label"] == "Likely scam"
|
| 480 |
assert analyze_notice("", "")["ok"] is False
|
| 481 |
try:
|
| 482 |
normalize_assessment({"risk_label": "Looks normal"})
|
static/app.js
CHANGED
|
@@ -19,6 +19,7 @@ const elements = {
|
|
| 19 |
|
| 20 |
let imageDataUrl = "";
|
| 21 |
let activeMode = null;
|
|
|
|
| 22 |
|
| 23 |
async function callGradioApi(name, data) {
|
| 24 |
const response = await fetch(`/gradio_api/call/${name}`, {
|
|
@@ -124,13 +125,16 @@ function renderResult(payload) {
|
|
| 124 |
|
| 125 |
elements.source.textContent = payload.source === "model"
|
| 126 |
? "Analyzed by the deployed Qwen model endpoint."
|
| 127 |
-
: ""
|
|
|
|
|
|
|
| 128 |
elements.results.hidden = false;
|
| 129 |
elements.results.scrollIntoView({ behavior: "smooth", block: "start" });
|
| 130 |
}
|
| 131 |
|
| 132 |
function useImage(file) {
|
| 133 |
if (!file) return;
|
|
|
|
| 134 |
const allowed = ["image/png", "image/jpeg", "image/webp"];
|
| 135 |
if (!allowed.includes(file.type)) return showError("Use a PNG, JPG, or WebP image.");
|
| 136 |
if (file.size > 8 * 1024 * 1024) return showError("Please choose an image smaller than 8 MB.");
|
|
@@ -150,6 +154,7 @@ elements.removeImage.addEventListener("click", (event) => {
|
|
| 150 |
event.preventDefault();
|
| 151 |
event.stopPropagation();
|
| 152 |
imageDataUrl = "";
|
|
|
|
| 153 |
elements.image.value = "";
|
| 154 |
elements.preview.removeAttribute("src");
|
| 155 |
elements.dropZone.classList.remove("has-image");
|
|
@@ -165,6 +170,7 @@ elements.removeImage.addEventListener("click", (event) => {
|
|
| 165 |
}));
|
| 166 |
elements.dropZone.addEventListener("drop", (event) => useImage(event.dataTransfer.files[0]));
|
| 167 |
elements.text.addEventListener("input", () => {
|
|
|
|
| 168 |
elements.charCount.textContent = `${elements.text.value.length.toLocaleString()} / 12,000`;
|
| 169 |
if (elements.text.value.trim().length === 1) {
|
| 170 |
setMode("text");
|
|
@@ -183,6 +189,7 @@ document.querySelectorAll(".example-card").forEach((button) => {
|
|
| 183 |
const reader = new FileReader();
|
| 184 |
reader.addEventListener("load", () => {
|
| 185 |
imageDataUrl = String(reader.result);
|
|
|
|
| 186 |
elements.preview.src = imageDataUrl;
|
| 187 |
elements.dropZone.classList.add("has-image");
|
| 188 |
showError();
|
|
@@ -196,6 +203,7 @@ document.querySelectorAll(".example-card").forEach((button) => {
|
|
| 196 |
} else if (button.dataset.example) {
|
| 197 |
elements.text.value = button.dataset.example;
|
| 198 |
elements.text.dispatchEvent(new Event("input"));
|
|
|
|
| 199 |
elements.text.focus();
|
| 200 |
setMode("text");
|
| 201 |
document.querySelector(".workspace").scrollIntoView({ behavior: "smooth" });
|
|
@@ -205,6 +213,7 @@ document.querySelectorAll(".example-card").forEach((button) => {
|
|
| 205 |
|
| 206 |
elements.resetButton.addEventListener("click", () => {
|
| 207 |
imageDataUrl = "";
|
|
|
|
| 208 |
elements.image.value = "";
|
| 209 |
elements.preview.removeAttribute("src");
|
| 210 |
elements.dropZone.classList.remove("has-image");
|
|
@@ -234,7 +243,11 @@ elements.form.addEventListener("submit", async (event) => {
|
|
| 234 |
|
| 235 |
setLoading(true);
|
| 236 |
try {
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
} catch (error) {
|
| 239 |
showError(error.message || "The request could not be completed.");
|
| 240 |
} finally {
|
|
|
|
| 19 |
|
| 20 |
let imageDataUrl = "";
|
| 21 |
let activeMode = null;
|
| 22 |
+
let activeExampleId = "";
|
| 23 |
|
| 24 |
async function callGradioApi(name, data) {
|
| 25 |
const response = await fetch(`/gradio_api/call/${name}`, {
|
|
|
|
| 125 |
|
| 126 |
elements.source.textContent = payload.source === "model"
|
| 127 |
? "Analyzed by the deployed Qwen model endpoint."
|
| 128 |
+
: payload.source === "cached_example"
|
| 129 |
+
? "Loaded from a precomputed example assessment. No model request was made."
|
| 130 |
+
: "";
|
| 131 |
elements.results.hidden = false;
|
| 132 |
elements.results.scrollIntoView({ behavior: "smooth", block: "start" });
|
| 133 |
}
|
| 134 |
|
| 135 |
function useImage(file) {
|
| 136 |
if (!file) return;
|
| 137 |
+
activeExampleId = "";
|
| 138 |
const allowed = ["image/png", "image/jpeg", "image/webp"];
|
| 139 |
if (!allowed.includes(file.type)) return showError("Use a PNG, JPG, or WebP image.");
|
| 140 |
if (file.size > 8 * 1024 * 1024) return showError("Please choose an image smaller than 8 MB.");
|
|
|
|
| 154 |
event.preventDefault();
|
| 155 |
event.stopPropagation();
|
| 156 |
imageDataUrl = "";
|
| 157 |
+
activeExampleId = "";
|
| 158 |
elements.image.value = "";
|
| 159 |
elements.preview.removeAttribute("src");
|
| 160 |
elements.dropZone.classList.remove("has-image");
|
|
|
|
| 170 |
}));
|
| 171 |
elements.dropZone.addEventListener("drop", (event) => useImage(event.dataTransfer.files[0]));
|
| 172 |
elements.text.addEventListener("input", () => {
|
| 173 |
+
activeExampleId = "";
|
| 174 |
elements.charCount.textContent = `${elements.text.value.length.toLocaleString()} / 12,000`;
|
| 175 |
if (elements.text.value.trim().length === 1) {
|
| 176 |
setMode("text");
|
|
|
|
| 189 |
const reader = new FileReader();
|
| 190 |
reader.addEventListener("load", () => {
|
| 191 |
imageDataUrl = String(reader.result);
|
| 192 |
+
activeExampleId = button.dataset.exampleId || "";
|
| 193 |
elements.preview.src = imageDataUrl;
|
| 194 |
elements.dropZone.classList.add("has-image");
|
| 195 |
showError();
|
|
|
|
| 203 |
} else if (button.dataset.example) {
|
| 204 |
elements.text.value = button.dataset.example;
|
| 205 |
elements.text.dispatchEvent(new Event("input"));
|
| 206 |
+
activeExampleId = button.dataset.exampleId || "";
|
| 207 |
elements.text.focus();
|
| 208 |
setMode("text");
|
| 209 |
document.querySelector(".workspace").scrollIntoView({ behavior: "smooth" });
|
|
|
|
| 213 |
|
| 214 |
elements.resetButton.addEventListener("click", () => {
|
| 215 |
imageDataUrl = "";
|
| 216 |
+
activeExampleId = "";
|
| 217 |
elements.image.value = "";
|
| 218 |
elements.preview.removeAttribute("src");
|
| 219 |
elements.dropZone.classList.remove("has-image");
|
|
|
|
| 243 |
|
| 244 |
setLoading(true);
|
| 245 |
try {
|
| 246 |
+
const submittedImage = activeExampleId ? "" : imageDataUrl;
|
| 247 |
+
renderResult(await callGradioApi(
|
| 248 |
+
"analyze",
|
| 249 |
+
[elements.text.value, submittedImage, activeExampleId],
|
| 250 |
+
));
|
| 251 |
} catch (error) {
|
| 252 |
showError(error.message || "The request could not be completed.");
|
| 253 |
} finally {
|
static/index.html
CHANGED
|
@@ -91,28 +91,28 @@
|
|
| 91 |
<div><p class="eyebrow">Try an example</p><h2 id="examplesTitle">Common messages in Pakistan</h2></div>
|
| 92 |
</div>
|
| 93 |
<div class="example-grid">
|
| 94 |
-
<button class="example-card" data-example="PAKISTAN POST: Your parcel address is incomplete. Pay Rs. 85 today at http://pakpost-delivery.xyz or the parcel will be destroyed.">
|
| 95 |
<span class="example-icon">PK</span><span><strong>Courier fee</strong><small>Urgent parcel payment link</small></span>
|
| 96 |
</button>
|
| 97 |
-
<button class="example-card" data-example="FBR REFUND: You are eligible for Rs 42,500. Submit your CNIC and bank card details at the link today to receive payment.">
|
| 98 |
<span class="example-icon">FBR</span><span><strong>Tax refund</strong><small>Unexpected refund request</small></span>
|
| 99 |
</button>
|
| 100 |
-
<button class="example-card" data-example="HBL Security: Your account will be suspended. Share the OTP sent to your phone with our support team immediately.">
|
| 101 |
<span class="example-icon">OTP</span><span><strong>Bank alert</strong><small>Security code request</small></span>
|
| 102 |
</button>
|
| 103 |
</div>
|
| 104 |
|
| 105 |
<p class="eyebrow" style="margin:28px 0 14px">Real scam screenshots</p>
|
| 106 |
<div class="example-grid example-grid-images">
|
| 107 |
-
<button class="example-card example-image-card" data-image="/static/example-courier.jpeg">
|
| 108 |
<img src="/static/example-courier.jpeg" alt="Courier scam screenshot" class="example-thumb">
|
| 109 |
<span><strong>Courier scam</strong><small>Fake delivery fee message</small></span>
|
| 110 |
</button>
|
| 111 |
-
<button class="example-card example-image-card" data-image="/static/example-mobile.png">
|
| 112 |
<img src="/static/example-mobile.png" alt="Mobile scam screenshot" class="example-thumb">
|
| 113 |
<span><strong>Mobile scam</strong><small>Fake mobile operator message</small></span>
|
| 114 |
</button>
|
| 115 |
-
<button class="example-card example-image-card" data-image="/static/example-trafic.png">
|
| 116 |
<img src="/static/example-trafic.png" alt="Traffic challan scam screenshot" class="example-thumb">
|
| 117 |
<span><strong>Traffic challan</strong><small>Fake e-challan fine message</small></span>
|
| 118 |
</button>
|
|
|
|
| 91 |
<div><p class="eyebrow">Try an example</p><h2 id="examplesTitle">Common messages in Pakistan</h2></div>
|
| 92 |
</div>
|
| 93 |
<div class="example-grid">
|
| 94 |
+
<button class="example-card" data-example-id="text-courier" data-example="PAKISTAN POST: Your parcel address is incomplete. Pay Rs. 85 today at http://pakpost-delivery.xyz or the parcel will be destroyed.">
|
| 95 |
<span class="example-icon">PK</span><span><strong>Courier fee</strong><small>Urgent parcel payment link</small></span>
|
| 96 |
</button>
|
| 97 |
+
<button class="example-card" data-example-id="text-fbr" data-example="FBR REFUND: You are eligible for Rs 42,500. Submit your CNIC and bank card details at the link today to receive payment.">
|
| 98 |
<span class="example-icon">FBR</span><span><strong>Tax refund</strong><small>Unexpected refund request</small></span>
|
| 99 |
</button>
|
| 100 |
+
<button class="example-card" data-example-id="text-bank" data-example="HBL Security: Your account will be suspended. Share the OTP sent to your phone with our support team immediately.">
|
| 101 |
<span class="example-icon">OTP</span><span><strong>Bank alert</strong><small>Security code request</small></span>
|
| 102 |
</button>
|
| 103 |
</div>
|
| 104 |
|
| 105 |
<p class="eyebrow" style="margin:28px 0 14px">Real scam screenshots</p>
|
| 106 |
<div class="example-grid example-grid-images">
|
| 107 |
+
<button class="example-card example-image-card" data-example-id="image-courier" data-image="/static/example-courier.jpeg">
|
| 108 |
<img src="/static/example-courier.jpeg" alt="Courier scam screenshot" class="example-thumb">
|
| 109 |
<span><strong>Courier scam</strong><small>Fake delivery fee message</small></span>
|
| 110 |
</button>
|
| 111 |
+
<button class="example-card example-image-card" data-example-id="image-mobile" data-image="/static/example-mobile.png">
|
| 112 |
<img src="/static/example-mobile.png" alt="Mobile scam screenshot" class="example-thumb">
|
| 113 |
<span><strong>Mobile scam</strong><small>Fake mobile operator message</small></span>
|
| 114 |
</button>
|
| 115 |
+
<button class="example-card example-image-card" data-example-id="image-traffic" data-image="/static/example-trafic.png">
|
| 116 |
<img src="/static/example-trafic.png" alt="Traffic challan scam screenshot" class="example-thumb">
|
| 117 |
<span><strong>Traffic challan</strong><small>Fake e-challan fine message</small></span>
|
| 118 |
</button>
|