Abid Ali Awan Codex commited on
Commit ·
3fae071
1
Parent(s): 0f5c289
Add English and Urdu interface
Browse filesAdd a persistent language toggle, full Urdu RTL localization, translated interface states, and Urdu model responses for live assessments.
Co-Authored-By: Codex <noreply@openai.com>
- app.py +25 -4
- static/app.js +244 -21
- static/index.html +49 -43
- static/styles.css +32 -3
app.py
CHANGED
|
@@ -40,7 +40,8 @@ REQUIRED_FIELDS = {
|
|
| 40 |
EXAMPLE_CACHE_PATH = ROOT / "data" / "example_assessments.json"
|
| 41 |
|
| 42 |
SYSTEM_PROMPT = """Assess Pakistani notices and messages for scam risk.
|
| 43 |
-
Return only JSON matching the schema. Use
|
|
|
|
| 44 |
|
| 45 |
Apply this label rubric strictly:
|
| 46 |
- Looks normal: a relevant notice with no meaningful scam indicator and no
|
|
@@ -306,12 +307,19 @@ def call_model(
|
|
| 306 |
text: str,
|
| 307 |
image_data_url: str,
|
| 308 |
telemetry: dict[str, Any] | None = None,
|
|
|
|
| 309 |
) -> dict[str, Any]:
|
| 310 |
telemetry = telemetry if telemetry is not None else {}
|
| 311 |
client, model_name = create_model_client()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
prompt = (
|
| 313 |
"Assess the following Pakistani notice or message for scam risk. "
|
| 314 |
-
"Explain visible evidence and give safe next steps.
|
|
|
|
| 315 |
f"Message text:\n{text.strip() or '[No text supplied; inspect the image.]'}"
|
| 316 |
)
|
| 317 |
content: Any = prompt
|
|
@@ -392,11 +400,13 @@ def analyze_notice(
|
|
| 392 |
image_data_url: str = "",
|
| 393 |
example_id: str = "",
|
| 394 |
save_trace: bool = True,
|
|
|
|
| 395 |
) -> dict[str, Any]:
|
| 396 |
"""Analyze supplied text/image using the configured model only."""
|
| 397 |
text = (text or "").strip()
|
| 398 |
image_data_url = image_data_url or ""
|
| 399 |
example_id = (example_id or "").strip()
|
|
|
|
| 400 |
|
| 401 |
def finish(
|
| 402 |
response: dict[str, Any],
|
|
@@ -451,7 +461,11 @@ def analyze_notice(
|
|
| 451 |
)
|
| 452 |
telemetry: dict[str, Any] = {}
|
| 453 |
try:
|
| 454 |
-
result =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
return finish(
|
| 456 |
{
|
| 457 |
"ok": True,
|
|
@@ -493,8 +507,15 @@ def analyze_api(
|
|
| 493 |
image_data_url: str = "",
|
| 494 |
example_id: str = "",
|
| 495 |
save_trace: bool = True,
|
|
|
|
| 496 |
) -> dict[str, Any]:
|
| 497 |
-
return analyze_notice(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
|
| 499 |
|
| 500 |
@app.api(name="status", description="Return model and privacy status.", queue=False)
|
|
|
|
| 40 |
EXAMPLE_CACHE_PATH = ROOT / "data" / "example_assessments.json"
|
| 41 |
|
| 42 |
SYSTEM_PROMPT = """Assess Pakistani notices and messages for scam risk.
|
| 43 |
+
Return only JSON matching the schema. Use the response language requested by
|
| 44 |
+
the user. Default to simple, calm English.
|
| 45 |
|
| 46 |
Apply this label rubric strictly:
|
| 47 |
- Looks normal: a relevant notice with no meaningful scam indicator and no
|
|
|
|
| 307 |
text: str,
|
| 308 |
image_data_url: str,
|
| 309 |
telemetry: dict[str, Any] | None = None,
|
| 310 |
+
output_language: str = "en",
|
| 311 |
) -> dict[str, Any]:
|
| 312 |
telemetry = telemetry if telemetry is not None else {}
|
| 313 |
client, model_name = create_model_client()
|
| 314 |
+
language_instruction = (
|
| 315 |
+
"Write all user-facing JSON values in clear Urdu script."
|
| 316 |
+
if output_language == "ur"
|
| 317 |
+
else "Write all user-facing JSON values in simple English."
|
| 318 |
+
)
|
| 319 |
prompt = (
|
| 320 |
"Assess the following Pakistani notice or message for scam risk. "
|
| 321 |
+
"Explain visible evidence and give safe next steps. "
|
| 322 |
+
f"{language_instruction}\n\n"
|
| 323 |
f"Message text:\n{text.strip() or '[No text supplied; inspect the image.]'}"
|
| 324 |
)
|
| 325 |
content: Any = prompt
|
|
|
|
| 400 |
image_data_url: str = "",
|
| 401 |
example_id: str = "",
|
| 402 |
save_trace: bool = True,
|
| 403 |
+
output_language: str = "en",
|
| 404 |
) -> dict[str, Any]:
|
| 405 |
"""Analyze supplied text/image using the configured model only."""
|
| 406 |
text = (text or "").strip()
|
| 407 |
image_data_url = image_data_url or ""
|
| 408 |
example_id = (example_id or "").strip()
|
| 409 |
+
output_language = "ur" if output_language == "ur" else "en"
|
| 410 |
|
| 411 |
def finish(
|
| 412 |
response: dict[str, Any],
|
|
|
|
| 461 |
)
|
| 462 |
telemetry: dict[str, Any] = {}
|
| 463 |
try:
|
| 464 |
+
result = (
|
| 465 |
+
call_model(text, image_data_url, telemetry, output_language="ur")
|
| 466 |
+
if output_language == "ur"
|
| 467 |
+
else call_model(text, image_data_url, telemetry)
|
| 468 |
+
)
|
| 469 |
return finish(
|
| 470 |
{
|
| 471 |
"ok": True,
|
|
|
|
| 507 |
image_data_url: str = "",
|
| 508 |
example_id: str = "",
|
| 509 |
save_trace: bool = True,
|
| 510 |
+
output_language: str = "en",
|
| 511 |
) -> dict[str, Any]:
|
| 512 |
+
return analyze_notice(
|
| 513 |
+
text,
|
| 514 |
+
image_data_url,
|
| 515 |
+
example_id,
|
| 516 |
+
save_trace,
|
| 517 |
+
output_language,
|
| 518 |
+
)
|
| 519 |
|
| 520 |
|
| 521 |
@app.api(name="status", description="Return model and privacy status.", queue=False)
|
static/app.js
CHANGED
|
@@ -16,11 +16,214 @@ const elements = {
|
|
| 16 |
uploadHint: document.querySelector("#uploadHint"),
|
| 17 |
textHint: document.querySelector("#textHint"),
|
| 18 |
saveTrace: document.querySelector("#saveTrace"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
};
|
| 20 |
|
| 21 |
let imageDataUrl = "";
|
| 22 |
let activeMode = null;
|
| 23 |
let activeExampleId = "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
async function callGradioApi(name, data) {
|
| 26 |
const response = await fetch(`/gradio_api/call/${name}`, {
|
|
@@ -28,10 +231,10 @@ async function callGradioApi(name, data) {
|
|
| 28 |
headers: { "Content-Type": "application/json" },
|
| 29 |
body: JSON.stringify({ data }),
|
| 30 |
});
|
| 31 |
-
if (!response.ok) throw new Error("
|
| 32 |
const { event_id: eventId } = await response.json();
|
| 33 |
const stream = await fetch(`/gradio_api/call/${name}/${eventId}`);
|
| 34 |
-
if (!stream.ok || !stream.body) throw new Error("
|
| 35 |
|
| 36 |
const reader = stream.body.getReader();
|
| 37 |
const decoder = new TextDecoder();
|
|
@@ -45,19 +248,28 @@ async function callGradioApi(name, data) {
|
|
| 45 |
for (const chunk of chunks) {
|
| 46 |
const event = chunk.match(/^event:\s*(.+)$/m)?.[1];
|
| 47 |
const raw = chunk.match(/^data:\s*(.+)$/m)?.[1];
|
| 48 |
-
if (event === "error") throw new Error("
|
| 49 |
if (event === "complete" && raw) {
|
| 50 |
const values = JSON.parse(raw);
|
| 51 |
return values[0];
|
| 52 |
}
|
| 53 |
}
|
| 54 |
}
|
| 55 |
-
throw new Error("
|
| 56 |
}
|
| 57 |
|
| 58 |
function setStatus(status) {
|
| 59 |
-
if (!status)
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
elements.status.classList.toggle("connected", Boolean(status.connected));
|
| 62 |
}
|
| 63 |
|
|
@@ -92,7 +304,7 @@ function setLoading(loading) {
|
|
| 92 |
elements.button.disabled = loading;
|
| 93 |
elements.button.classList.toggle("loading", loading);
|
| 94 |
elements.button.querySelector(".button-label").textContent =
|
| 95 |
-
loading ? "
|
| 96 |
}
|
| 97 |
|
| 98 |
function renderList(selector, items) {
|
|
@@ -105,11 +317,11 @@ function renderList(selector, items) {
|
|
| 105 |
}
|
| 106 |
|
| 107 |
function renderResult(payload) {
|
| 108 |
-
if (!payload.ok) throw new Error(
|
| 109 |
const result = payload.assessment;
|
| 110 |
setStatus(payload.status);
|
| 111 |
elements.risk.className = `risk-badge risk-${result.risk_label.toLowerCase().replaceAll(" ", "-")}`;
|
| 112 |
-
|
| 113 |
document.querySelector("#explanationText").textContent = result.simple_explanation;
|
| 114 |
renderList("#redFlagsList", result.red_flags);
|
| 115 |
renderList("#nextStepsList", result.safe_next_steps);
|
|
@@ -125,9 +337,9 @@ function renderResult(payload) {
|
|
| 125 |
}
|
| 126 |
|
| 127 |
elements.source.textContent = payload.source === "model"
|
| 128 |
-
? "
|
| 129 |
: payload.source === "cached_modal_example"
|
| 130 |
-
? "
|
| 131 |
: "";
|
| 132 |
elements.source.classList.toggle(
|
| 133 |
"cached-result",
|
|
@@ -141,8 +353,8 @@ function useImage(file) {
|
|
| 141 |
if (!file) return;
|
| 142 |
activeExampleId = "";
|
| 143 |
const allowed = ["image/png", "image/jpeg", "image/webp"];
|
| 144 |
-
if (!allowed.includes(file.type)) return showError("
|
| 145 |
-
if (file.size > 8 * 1024 * 1024) return showError("
|
| 146 |
const reader = new FileReader();
|
| 147 |
reader.addEventListener("load", () => {
|
| 148 |
imageDataUrl = String(reader.result);
|
|
@@ -203,7 +415,7 @@ document.querySelectorAll(".example-card").forEach((button) => {
|
|
| 203 |
});
|
| 204 |
reader.readAsDataURL(blob);
|
| 205 |
} catch {
|
| 206 |
-
showError("
|
| 207 |
}
|
| 208 |
} else if (button.dataset.example) {
|
| 209 |
elements.text.value = button.dataset.example;
|
|
@@ -233,7 +445,7 @@ elements.form.addEventListener("submit", async (event) => {
|
|
| 233 |
event.preventDefault();
|
| 234 |
showError();
|
| 235 |
if (!elements.text.value.trim() && !imageDataUrl) {
|
| 236 |
-
return showError("
|
| 237 |
}
|
| 238 |
|
| 239 |
if (activeMode === "image") {
|
|
@@ -248,13 +460,20 @@ elements.form.addEventListener("submit", async (event) => {
|
|
| 248 |
|
| 249 |
setLoading(true);
|
| 250 |
try {
|
| 251 |
-
const
|
|
|
|
| 252 |
renderResult(await callGradioApi(
|
| 253 |
"analyze",
|
| 254 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
));
|
| 256 |
} catch (error) {
|
| 257 |
-
showError(error.message || "
|
| 258 |
} finally {
|
| 259 |
setLoading(false);
|
| 260 |
}
|
|
@@ -264,10 +483,14 @@ document.querySelectorAll(".copy-button").forEach((button) => {
|
|
| 264 |
button.addEventListener("click", async () => {
|
| 265 |
const target = document.querySelector(`#${button.dataset.copy}`);
|
| 266 |
await navigator.clipboard.writeText(target.innerText);
|
| 267 |
-
|
| 268 |
-
button.textContent = "
|
| 269 |
-
setTimeout(() => { button.textContent = original; }, 1200);
|
| 270 |
});
|
| 271 |
});
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
loadStatus();
|
|
|
|
| 16 |
uploadHint: document.querySelector("#uploadHint"),
|
| 17 |
textHint: document.querySelector("#textHint"),
|
| 18 |
saveTrace: document.querySelector("#saveTrace"),
|
| 19 |
+
statusText: document.querySelector(".status-text"),
|
| 20 |
+
languageOptions: document.querySelectorAll(".language-option"),
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
const translations = {
|
| 24 |
+
en: {
|
| 25 |
+
pageTitle: "Pakistan Notice Helper",
|
| 26 |
+
pageDescription: "Check Pakistani notices and messages for common scam signals.",
|
| 27 |
+
statusChecking: "Checking model",
|
| 28 |
+
statusReady: "Modal model ready",
|
| 29 |
+
statusCredentials: "Modal credentials required",
|
| 30 |
+
statusUnavailable: "Modal model unavailable",
|
| 31 |
+
heroEyebrow: "Understand before you act",
|
| 32 |
+
heroTitle: "Does this notice look",
|
| 33 |
+
heroSafe: "safe?",
|
| 34 |
+
heroText: "Check suspicious bills, bank alerts, FBR-style messages, challans, courier notices, and SMS screenshots for common scam signals.",
|
| 35 |
+
trustTitle: "AI-assisted safety check",
|
| 36 |
+
trustText: "The AI reads the notice, identifies scam signals, and returns a structured risk assessment with safer next steps.",
|
| 37 |
+
checkerEyebrow: "Free safety check",
|
| 38 |
+
checkerTitle: "Check a notice or message",
|
| 39 |
+
modelDescription: "Analysis runs on the deployed Qwen3.5 4B multimodal model.",
|
| 40 |
+
uploadLabel: "Upload a screenshot",
|
| 41 |
+
dropImage: "Drop an image here",
|
| 42 |
+
browseImage: "or tap to browse PNG, JPG, or WebP",
|
| 43 |
+
previewAlt: "Selected notice preview",
|
| 44 |
+
removeImage: "Remove image",
|
| 45 |
+
imageMode: "Screenshot mode active — text input is locked",
|
| 46 |
+
pasteLabel: "Or paste the message",
|
| 47 |
+
textPlaceholder: "Paste the SMS, email, bill text, or notice here...",
|
| 48 |
+
languageSupport: "English, Urdu, and Roman Urdu supported",
|
| 49 |
+
textMode: "Text mode active — image upload is locked",
|
| 50 |
+
traceTitle: "Publish privacy-safe trace",
|
| 51 |
+
traceText: "Stores automated redacted text or an image description. Raw text, screenshots, links, identifiers, and model text are not stored.",
|
| 52 |
+
checkButton: "Check this notice",
|
| 53 |
+
checkingButton: "Checking safely...",
|
| 54 |
+
startOver: "Start over",
|
| 55 |
+
examplesEyebrow: "Try an example",
|
| 56 |
+
examplesTitle: "Common messages in Pakistan",
|
| 57 |
+
courierFee: "Courier fee",
|
| 58 |
+
courierFeeText: "Urgent parcel payment link",
|
| 59 |
+
taxRefund: "Tax refund",
|
| 60 |
+
taxRefundText: "Unexpected refund request",
|
| 61 |
+
bankAlert: "Bank alert",
|
| 62 |
+
bankAlertText: "Security code request",
|
| 63 |
+
screenshotsTitle: "Real scam screenshots",
|
| 64 |
+
courierScam: "Courier scam",
|
| 65 |
+
courierScamText: "Fake delivery fee message",
|
| 66 |
+
mobileScam: "Mobile scam",
|
| 67 |
+
mobileScamText: "Fake mobile operator message",
|
| 68 |
+
trafficScam: "Traffic challan",
|
| 69 |
+
trafficScamText: "Fake e-challan fine message",
|
| 70 |
+
resultsEyebrow: "Safety assessment",
|
| 71 |
+
resultsTitle: "What we found",
|
| 72 |
+
explanationTitle: "Simple explanation",
|
| 73 |
+
redFlagsTitle: "Red flags found",
|
| 74 |
+
nextStepsTitle: "Safe next steps",
|
| 75 |
+
replyTitle: "Polite reply draft",
|
| 76 |
+
copy: "Copy",
|
| 77 |
+
copied: "Copied",
|
| 78 |
+
disclaimerTitle: "Important safety note",
|
| 79 |
+
disclaimerText: "Pakistan Notice Helper does not provide official verification. It checks common scam signals and gives safe next steps. Always verify through official websites or helplines before making payments or sharing personal information.",
|
| 80 |
+
footerOne: "Built for safer digital decisions in Pakistan.",
|
| 81 |
+
footerTwo: "Never share OTPs, PINs, passwords, or CVVs.",
|
| 82 |
+
requestStartError: "The app could not start the request.",
|
| 83 |
+
requestReadError: "The app could not read the result.",
|
| 84 |
+
requestFailedError: "The request could not be completed.",
|
| 85 |
+
noResultError: "The app returned no result.",
|
| 86 |
+
analyzeError: "Unable to analyze this input.",
|
| 87 |
+
imageTypeError: "Use a PNG, JPG, or WebP image.",
|
| 88 |
+
imageSizeError: "Please choose an image smaller than 8 MB.",
|
| 89 |
+
exampleImageError: "Could not load the example image.",
|
| 90 |
+
emptyInputError: "Paste a message or upload a screenshot to continue.",
|
| 91 |
+
modelSource: "Analyzed by the deployed Qwen3.5 4B model endpoint.",
|
| 92 |
+
cachedSource: "Cached model result",
|
| 93 |
+
riskLooksNormal: "Looks normal",
|
| 94 |
+
riskVerifyFirst: "Verify first",
|
| 95 |
+
riskSuspicious: "Suspicious",
|
| 96 |
+
riskLikelyScam: "Likely scam",
|
| 97 |
+
riskInappropriate: "Inappropriate",
|
| 98 |
+
},
|
| 99 |
+
ur: {
|
| 100 |
+
pageTitle: "پاکستان نوٹس ہیلپر",
|
| 101 |
+
pageDescription: "پاکستانی نوٹس اور پیغامات میں عام فراڈ کی علامات چیک کریں۔",
|
| 102 |
+
statusChecking: "ماڈل چیک ہو رہا ہے",
|
| 103 |
+
statusReady: "ماڈل تیار ہے",
|
| 104 |
+
statusCredentials: "Modal کی اسناد درکار ہیں",
|
| 105 |
+
statusUnavailable: "ماڈل دستیاب نہیں",
|
| 106 |
+
heroEyebrow: "عمل کرنے سے پہلے سمجھیں",
|
| 107 |
+
heroTitle: "کیا یہ نوٹس",
|
| 108 |
+
heroSafe: "محفوظ ہے؟",
|
| 109 |
+
heroText: "مشکوک بل، بینک الرٹس، ایف بی آر طرز کے پیغامات، چالان، کوریئر نوٹس اور ایس ایم ایس اسکرین شاٹس میں فراڈ کی عام علامات چیک ک��یں۔",
|
| 110 |
+
trustTitle: "اے آئی کی مدد سے حفاظتی جانچ",
|
| 111 |
+
trustText: "اے آئی نوٹس پڑھ کر فراڈ کی علامات شناخت کرتا ہے اور محفوظ اگلے اقدامات کے ساتھ منظم جائزہ دیتا ہے۔",
|
| 112 |
+
checkerEyebrow: "مفت حفاظتی جانچ",
|
| 113 |
+
checkerTitle: "نوٹس یا پیغام چیک کریں",
|
| 114 |
+
modelDescription: "تجزیہ Qwen3.5 4B ملٹی موڈل ماڈل پر چلتا ہے۔",
|
| 115 |
+
uploadLabel: "اسکرین شاٹ اپ لوڈ کریں",
|
| 116 |
+
dropImage: "تصویر یہاں چھوڑیں",
|
| 117 |
+
browseImage: "یا PNG، JPG یا WebP منتخب کرنے کے لیے دبائیں",
|
| 118 |
+
previewAlt: "منتخب نوٹس کا پیش منظر",
|
| 119 |
+
removeImage: "تصویر ہٹائیں",
|
| 120 |
+
imageMode: "اسکرین شاٹ موڈ فعال ہے، متن بند ہے",
|
| 121 |
+
pasteLabel: "یا پیغام یہاں لکھیں",
|
| 122 |
+
textPlaceholder: "ایس ایم ایس، ای میل، بل یا نوٹس کا متن یہاں پیسٹ کریں۔۔۔",
|
| 123 |
+
languageSupport: "انگریزی، اردو اور رومن اردو معاون ہیں",
|
| 124 |
+
textMode: "متن موڈ فعال ہے، تصویر اپ لوڈ بند ہے",
|
| 125 |
+
traceTitle: "رازداری محفوظ رکھنے والا ٹریس شائع کریں",
|
| 126 |
+
traceText: "صرف خودکار طور پر چھپایا گیا متن یا تصویر کی مختصر تفصیل محفوظ ہوتی ہے۔ اصل متن، تصاویر، لنکس، شناختی معلومات اور ماڈل کا متن محفوظ نہیں ہوتا۔",
|
| 127 |
+
checkButton: "یہ نوٹس چیک کریں",
|
| 128 |
+
checkingButton: "محفوظ جانچ جاری ہے۔۔۔",
|
| 129 |
+
startOver: "دوبارہ شروع کریں",
|
| 130 |
+
examplesEyebrow: "مثال آزمائیں",
|
| 131 |
+
examplesTitle: "پاکستان میں عام پیغامات",
|
| 132 |
+
courierFee: "کوریئر فیس",
|
| 133 |
+
courierFeeText: "فوری پارسل ادائیگی کا لنک",
|
| 134 |
+
taxRefund: "ٹیکس ریفنڈ",
|
| 135 |
+
taxRefundText: "غیر متوقع رقم واپسی کی درخواست",
|
| 136 |
+
bankAlert: "بینک الرٹ",
|
| 137 |
+
bankAlertText: "سیکیورٹی کوڈ کی درخواست",
|
| 138 |
+
screenshotsTitle: "حقیقی فراڈ اسکرین شاٹس",
|
| 139 |
+
courierScam: "کوریئر فراڈ",
|
| 140 |
+
courierScamText: "جعلی ڈیلیوری فیس کا پیغام",
|
| 141 |
+
mobileScam: "موبائل فراڈ",
|
| 142 |
+
mobileScamText: "جعلی موبائل آپریٹر پیغام",
|
| 143 |
+
trafficScam: "ٹریفک چالان",
|
| 144 |
+
trafficScamText: "جعلی ای چالان جرمانے کا پیغام",
|
| 145 |
+
resultsEyebrow: "حفاظتی جائزہ",
|
| 146 |
+
resultsTitle: "ہمیں کیا ملا",
|
| 147 |
+
explanationTitle: "سادہ وضاحت",
|
| 148 |
+
redFlagsTitle: "خطرے کی علامات",
|
| 149 |
+
nextStepsTitle: "محفوظ اگلے اقدامات",
|
| 150 |
+
replyTitle: "شائستہ جواب کا مسودہ",
|
| 151 |
+
copy: "کاپی کریں",
|
| 152 |
+
copied: "کاپی ہو گیا",
|
| 153 |
+
disclaimerTitle: "اہم حفاظتی نوٹ",
|
| 154 |
+
disclaimerText: "پاکستان نوٹس ہیلپر سرکاری تصدیق فراہم نہیں کرتا۔ یہ عام فراڈ کی علامات دیکھ کر محفوظ اگلے اقدامات بتاتا ہے۔ ادائیگی یا ذاتی معلومات دینے سے پہلے ہمیشہ سرکاری ویب سائٹ یا ہیلپ لائن سے تصدیق کریں۔",
|
| 155 |
+
footerOne: "پاکستان میں محفوظ ڈیجیٹل فیصلوں کے لیے تیار کیا گیا۔",
|
| 156 |
+
footerTwo: "اپنا OTP، PIN، پاس ورڈ یا CVV کبھی شیئر نہ کریں۔",
|
| 157 |
+
requestStartError: "درخواست شروع نہیں ہو سکی۔",
|
| 158 |
+
requestReadError: "نتیجہ پڑھا نہیں جا سکا۔",
|
| 159 |
+
requestFailedError: "درخواست مکمل نہیں ہو سکی۔",
|
| 160 |
+
noResultError: "کوئی نتیجہ موصول نہیں ہوا۔",
|
| 161 |
+
analyzeError: "اس مواد کا تجزیہ نہیں ہو سکا۔",
|
| 162 |
+
imageTypeError: "PNG، JPG یا WebP تصویر استعمال کریں۔",
|
| 163 |
+
imageSizeError: "براہ کرم 8 MB سے چھوٹی تصویر منتخب کریں۔",
|
| 164 |
+
exampleImageError: "مثالی تصویر لوڈ نہیں ہو سکی۔",
|
| 165 |
+
emptyInputError: "پیغام پیسٹ کریں یا اسکرین شاٹ اپ لوڈ کریں۔",
|
| 166 |
+
modelSource: "Qwen3.5 4B ماڈل نے اس کا تجزیہ کیا ہے۔",
|
| 167 |
+
cachedSource: "محفوظ شدہ ماڈل نتیجہ",
|
| 168 |
+
riskLooksNormal: "معمول کے مطابق",
|
| 169 |
+
riskVerifyFirst: "پہلے تصدیق کریں",
|
| 170 |
+
riskSuspicious: "مشکوک",
|
| 171 |
+
riskLikelyScam: "ممکنہ فراڈ",
|
| 172 |
+
riskInappropriate: "نامناسب",
|
| 173 |
+
},
|
| 174 |
};
|
| 175 |
|
| 176 |
let imageDataUrl = "";
|
| 177 |
let activeMode = null;
|
| 178 |
let activeExampleId = "";
|
| 179 |
+
let currentLanguage = localStorage.getItem("notice-helper-language") === "ur" ? "ur" : "en";
|
| 180 |
+
let currentStatus = null;
|
| 181 |
+
let currentRiskLabel = "";
|
| 182 |
+
|
| 183 |
+
function t(key) {
|
| 184 |
+
return translations[currentLanguage][key] || translations.en[key] || key;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
function applyLanguage(language) {
|
| 188 |
+
currentLanguage = language === "ur" ? "ur" : "en";
|
| 189 |
+
localStorage.setItem("notice-helper-language", currentLanguage);
|
| 190 |
+
document.documentElement.lang = currentLanguage;
|
| 191 |
+
document.documentElement.dir = currentLanguage === "ur" ? "rtl" : "ltr";
|
| 192 |
+
document.title = t("pageTitle");
|
| 193 |
+
document.querySelector('meta[name="description"]').content = t("pageDescription");
|
| 194 |
+
|
| 195 |
+
document.querySelectorAll("[data-i18n]").forEach((element) => {
|
| 196 |
+
element.textContent = t(element.dataset.i18n);
|
| 197 |
+
});
|
| 198 |
+
document.querySelectorAll("[data-i18n-placeholder]").forEach((element) => {
|
| 199 |
+
element.placeholder = t(element.dataset.i18nPlaceholder);
|
| 200 |
+
});
|
| 201 |
+
document.querySelectorAll("[data-i18n-alt]").forEach((element) => {
|
| 202 |
+
element.alt = t(element.dataset.i18nAlt);
|
| 203 |
+
});
|
| 204 |
+
elements.languageOptions.forEach((button) => {
|
| 205 |
+
const active = button.dataset.language === currentLanguage;
|
| 206 |
+
button.classList.toggle("active", active);
|
| 207 |
+
button.setAttribute("aria-pressed", String(active));
|
| 208 |
+
});
|
| 209 |
+
setStatus(currentStatus);
|
| 210 |
+
if (currentRiskLabel) setRiskLabel(currentRiskLabel);
|
| 211 |
+
if (elements.button.classList.contains("loading")) {
|
| 212 |
+
elements.button.querySelector(".button-label").textContent = t("checkingButton");
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
function setRiskLabel(label) {
|
| 217 |
+
currentRiskLabel = label;
|
| 218 |
+
const keys = {
|
| 219 |
+
"Looks normal": "riskLooksNormal",
|
| 220 |
+
"Verify first": "riskVerifyFirst",
|
| 221 |
+
Suspicious: "riskSuspicious",
|
| 222 |
+
"Likely scam": "riskLikelyScam",
|
| 223 |
+
Inappropriate: "riskInappropriate",
|
| 224 |
+
};
|
| 225 |
+
elements.risk.textContent = t(keys[label] || label);
|
| 226 |
+
}
|
| 227 |
|
| 228 |
async function callGradioApi(name, data) {
|
| 229 |
const response = await fetch(`/gradio_api/call/${name}`, {
|
|
|
|
| 231 |
headers: { "Content-Type": "application/json" },
|
| 232 |
body: JSON.stringify({ data }),
|
| 233 |
});
|
| 234 |
+
if (!response.ok) throw new Error(t("requestStartError"));
|
| 235 |
const { event_id: eventId } = await response.json();
|
| 236 |
const stream = await fetch(`/gradio_api/call/${name}/${eventId}`);
|
| 237 |
+
if (!stream.ok || !stream.body) throw new Error(t("requestReadError"));
|
| 238 |
|
| 239 |
const reader = stream.body.getReader();
|
| 240 |
const decoder = new TextDecoder();
|
|
|
|
| 248 |
for (const chunk of chunks) {
|
| 249 |
const event = chunk.match(/^event:\s*(.+)$/m)?.[1];
|
| 250 |
const raw = chunk.match(/^data:\s*(.+)$/m)?.[1];
|
| 251 |
+
if (event === "error") throw new Error(t("requestFailedError"));
|
| 252 |
if (event === "complete" && raw) {
|
| 253 |
const values = JSON.parse(raw);
|
| 254 |
return values[0];
|
| 255 |
}
|
| 256 |
}
|
| 257 |
}
|
| 258 |
+
throw new Error(t("noResultError"));
|
| 259 |
}
|
| 260 |
|
| 261 |
function setStatus(status) {
|
| 262 |
+
if (!status) {
|
| 263 |
+
elements.statusText.textContent = t("statusChecking");
|
| 264 |
+
return;
|
| 265 |
+
}
|
| 266 |
+
currentStatus = status;
|
| 267 |
+
const modelName = status.label?.match(/:\s*(.+)$/)?.[1] || "";
|
| 268 |
+
elements.statusText.textContent = status.connected
|
| 269 |
+
? `${t("statusReady")}${modelName ? `: ${modelName}` : ""}`
|
| 270 |
+
: status.label?.toLowerCase().includes("credentials")
|
| 271 |
+
? t("statusCredentials")
|
| 272 |
+
: t("statusUnavailable");
|
| 273 |
elements.status.classList.toggle("connected", Boolean(status.connected));
|
| 274 |
}
|
| 275 |
|
|
|
|
| 304 |
elements.button.disabled = loading;
|
| 305 |
elements.button.classList.toggle("loading", loading);
|
| 306 |
elements.button.querySelector(".button-label").textContent =
|
| 307 |
+
loading ? t("checkingButton") : t("checkButton");
|
| 308 |
}
|
| 309 |
|
| 310 |
function renderList(selector, items) {
|
|
|
|
| 317 |
}
|
| 318 |
|
| 319 |
function renderResult(payload) {
|
| 320 |
+
if (!payload.ok) throw new Error(t("analyzeError"));
|
| 321 |
const result = payload.assessment;
|
| 322 |
setStatus(payload.status);
|
| 323 |
elements.risk.className = `risk-badge risk-${result.risk_label.toLowerCase().replaceAll(" ", "-")}`;
|
| 324 |
+
setRiskLabel(result.risk_label);
|
| 325 |
document.querySelector("#explanationText").textContent = result.simple_explanation;
|
| 326 |
renderList("#redFlagsList", result.red_flags);
|
| 327 |
renderList("#nextStepsList", result.safe_next_steps);
|
|
|
|
| 337 |
}
|
| 338 |
|
| 339 |
elements.source.textContent = payload.source === "model"
|
| 340 |
+
? t("modelSource")
|
| 341 |
: payload.source === "cached_modal_example"
|
| 342 |
+
? t("cachedSource")
|
| 343 |
: "";
|
| 344 |
elements.source.classList.toggle(
|
| 345 |
"cached-result",
|
|
|
|
| 353 |
if (!file) return;
|
| 354 |
activeExampleId = "";
|
| 355 |
const allowed = ["image/png", "image/jpeg", "image/webp"];
|
| 356 |
+
if (!allowed.includes(file.type)) return showError(t("imageTypeError"));
|
| 357 |
+
if (file.size > 8 * 1024 * 1024) return showError(t("imageSizeError"));
|
| 358 |
const reader = new FileReader();
|
| 359 |
reader.addEventListener("load", () => {
|
| 360 |
imageDataUrl = String(reader.result);
|
|
|
|
| 415 |
});
|
| 416 |
reader.readAsDataURL(blob);
|
| 417 |
} catch {
|
| 418 |
+
showError(t("exampleImageError"));
|
| 419 |
}
|
| 420 |
} else if (button.dataset.example) {
|
| 421 |
elements.text.value = button.dataset.example;
|
|
|
|
| 445 |
event.preventDefault();
|
| 446 |
showError();
|
| 447 |
if (!elements.text.value.trim() && !imageDataUrl) {
|
| 448 |
+
return showError(t("emptyInputError"));
|
| 449 |
}
|
| 450 |
|
| 451 |
if (activeMode === "image") {
|
|
|
|
| 460 |
|
| 461 |
setLoading(true);
|
| 462 |
try {
|
| 463 |
+
const useCachedExample = currentLanguage === "en" && Boolean(activeExampleId);
|
| 464 |
+
const submittedImage = useCachedExample ? "" : imageDataUrl;
|
| 465 |
renderResult(await callGradioApi(
|
| 466 |
"analyze",
|
| 467 |
+
[
|
| 468 |
+
elements.text.value,
|
| 469 |
+
submittedImage,
|
| 470 |
+
useCachedExample ? activeExampleId : "",
|
| 471 |
+
elements.saveTrace.checked,
|
| 472 |
+
currentLanguage,
|
| 473 |
+
],
|
| 474 |
));
|
| 475 |
} catch (error) {
|
| 476 |
+
showError(error.message || t("requestFailedError"));
|
| 477 |
} finally {
|
| 478 |
setLoading(false);
|
| 479 |
}
|
|
|
|
| 483 |
button.addEventListener("click", async () => {
|
| 484 |
const target = document.querySelector(`#${button.dataset.copy}`);
|
| 485 |
await navigator.clipboard.writeText(target.innerText);
|
| 486 |
+
button.textContent = t("copied");
|
| 487 |
+
setTimeout(() => { button.textContent = t("copy"); }, 1200);
|
|
|
|
| 488 |
});
|
| 489 |
});
|
| 490 |
|
| 491 |
+
elements.languageOptions.forEach((button) => {
|
| 492 |
+
button.addEventListener("click", () => applyLanguage(button.dataset.language));
|
| 493 |
+
});
|
| 494 |
+
|
| 495 |
+
applyLanguage(currentLanguage);
|
| 496 |
loadStatus();
|
static/index.html
CHANGED
|
@@ -12,26 +12,32 @@
|
|
| 12 |
</head>
|
| 13 |
<body>
|
| 14 |
<header class="topbar">
|
| 15 |
-
<
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
<div class="status-row">
|
| 20 |
-
<span id="modelStatus" class="status-badge"><span class="status-dot"></span>Checking model</span>
|
| 21 |
</div>
|
| 22 |
</header>
|
| 23 |
|
| 24 |
<main>
|
| 25 |
<section class="hero">
|
| 26 |
<div class="hero-copy">
|
| 27 |
-
<p class="eyebrow">Understand before you act</p>
|
| 28 |
-
<h1>Does this notice look <span>safe?</span></h1>
|
| 29 |
-
<p class="hero-text">Check suspicious bills, bank alerts, FBR-style messages, challans, courier notices, and SMS screenshots for common scam signals.</p>
|
| 30 |
<div class="trust-note">
|
| 31 |
<span class="shield" aria-hidden="true">✓</span>
|
| 32 |
<p>
|
| 33 |
-
<strong>AI-assisted safety check</strong>
|
| 34 |
-
<span>The AI reads the notice, identifies scam signals, and returns a structured risk assessment with safer next steps.</span>
|
| 35 |
</p>
|
| 36 |
</div>
|
| 37 |
</div>
|
|
@@ -43,32 +49,32 @@
|
|
| 43 |
<section class="workspace" aria-labelledby="checkerTitle">
|
| 44 |
<div class="section-heading">
|
| 45 |
<div>
|
| 46 |
-
<p class="eyebrow">Free safety check</p>
|
| 47 |
-
<h2 id="checkerTitle">Check a notice or message</h2>
|
| 48 |
</div>
|
| 49 |
-
<p>Analysis runs on the deployed Qwen3.5 4B multimodal model.</p>
|
| 50 |
</div>
|
| 51 |
|
| 52 |
<form id="noticeForm">
|
| 53 |
<div class="input-grid">
|
| 54 |
<div class="field-card upload-card">
|
| 55 |
-
<div class="field-label"><span>1</span> Upload a screenshot</div>
|
| 56 |
<label id="dropZone" class="drop-zone" for="imageInput">
|
| 57 |
<input id="imageInput" type="file" accept="image/png,image/jpeg,image/webp">
|
| 58 |
<span class="upload-icon" aria-hidden="true">↑</span>
|
| 59 |
-
<strong>Drop an image here</strong>
|
| 60 |
-
<small>or tap to browse PNG, JPG, or WebP</small>
|
| 61 |
-
<img id="imagePreview" alt="Selected notice preview">
|
| 62 |
-
<button id="removeImage" class="text-button" type="button">Remove image</button>
|
| 63 |
</label>
|
| 64 |
-
<div id="uploadHint" class="mode-hint"><span class="hint-icon">↑</span> Screenshot mode active — text input is locked</div>
|
| 65 |
</div>
|
| 66 |
|
| 67 |
<div class="field-card">
|
| 68 |
-
<label class="field-label" for="noticeText"><span>2</span> Or paste the message</label>
|
| 69 |
-
<textarea id="noticeText" maxlength="12000" placeholder="Paste the SMS, email, bill text, or notice here..."></textarea>
|
| 70 |
-
<div class="field-meta"><span>English, Urdu, and Roman Urdu supported by compatible models</span><span id="charCount">0 / 12,000</span></div>
|
| 71 |
-
<div id="textHint" class="mode-hint"><span class="hint-icon">✎</span> Text mode active — image upload is locked</div>
|
| 72 |
</div>
|
| 73 |
</div>
|
| 74 |
|
|
@@ -76,17 +82,17 @@
|
|
| 76 |
<label class="trace-consent" for="saveTrace">
|
| 77 |
<input id="saveTrace" type="checkbox" checked>
|
| 78 |
<span>
|
| 79 |
-
<strong>Publish privacy-safe trace</strong>
|
| 80 |
-
<small>Stores automated redacted text or an image description. Raw text, screenshots, links, identifiers, and model text are not stored.</small>
|
| 81 |
</span>
|
| 82 |
</label>
|
| 83 |
<div class="form-actions">
|
| 84 |
<button id="analyzeButton" class="primary-button" type="submit">
|
| 85 |
-
<span class="button-label">Check this notice</span>
|
| 86 |
<span class="spinner" aria-hidden="true"></span>
|
| 87 |
</button>
|
| 88 |
<button id="resetButton" class="reset-button" type="button">
|
| 89 |
-
Start over
|
| 90 |
</button>
|
| 91 |
</div>
|
| 92 |
</form>
|
|
@@ -94,69 +100,69 @@
|
|
| 94 |
|
| 95 |
<section class="examples" aria-labelledby="examplesTitle">
|
| 96 |
<div class="section-heading compact">
|
| 97 |
-
<div><p class="eyebrow">Try an example</p><h2 id="examplesTitle">Common messages in Pakistan</h2></div>
|
| 98 |
</div>
|
| 99 |
<div class="example-grid">
|
| 100 |
<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.">
|
| 101 |
-
<span class="example-icon">PK</span><span><strong>Courier fee</strong><small>Urgent parcel payment link</small></span>
|
| 102 |
</button>
|
| 103 |
<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.">
|
| 104 |
-
<span class="example-icon">FBR</span><span><strong>Tax refund</strong><small>Unexpected refund request</small></span>
|
| 105 |
</button>
|
| 106 |
<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.">
|
| 107 |
-
<span class="example-icon">OTP</span><span><strong>Bank alert</strong><small>Security code request</small></span>
|
| 108 |
</button>
|
| 109 |
</div>
|
| 110 |
|
| 111 |
-
<p class="eyebrow" style="margin:28px 0 14px">Real scam screenshots</p>
|
| 112 |
<div class="example-grid example-grid-images">
|
| 113 |
<button class="example-card example-image-card" data-example-id="image-courier" data-image="/static/example-courier.jpeg">
|
| 114 |
<img src="/static/example-courier.jpeg" alt="Courier scam screenshot" class="example-thumb">
|
| 115 |
-
<span><strong>Courier scam</strong><small>Fake delivery fee message</small></span>
|
| 116 |
</button>
|
| 117 |
<button class="example-card example-image-card" data-example-id="image-mobile" data-image="/static/example-mobile.png">
|
| 118 |
<img src="/static/example-mobile.png" alt="Mobile scam screenshot" class="example-thumb">
|
| 119 |
-
<span><strong>Mobile scam</strong><small>Fake mobile operator message</small></span>
|
| 120 |
</button>
|
| 121 |
<button class="example-card example-image-card" data-example-id="image-traffic" data-image="/static/example-trafic.png">
|
| 122 |
<img src="/static/example-trafic.png" alt="Traffic challan scam screenshot" class="example-thumb">
|
| 123 |
-
<span><strong>Traffic challan</strong><small>Fake e-challan fine message</small></span>
|
| 124 |
</button>
|
| 125 |
</div>
|
| 126 |
</section>
|
| 127 |
|
| 128 |
<section id="results" class="results" aria-live="polite" hidden>
|
| 129 |
<div class="result-header">
|
| 130 |
-
<div><p class="eyebrow">Safety assessment</p><h2>What we found</h2></div>
|
| 131 |
<div id="riskBadge" class="risk-badge"></div>
|
| 132 |
</div>
|
| 133 |
<p id="resultSource" class="result-source"></p>
|
| 134 |
<div class="result-grid">
|
| 135 |
<article class="result-card explanation-card">
|
| 136 |
-
<div class="card-title"><span>i</span><h3>Simple explanation</h3><button class="copy-button" data-copy="explanationText">Copy</button></div>
|
| 137 |
<p id="explanationText"></p>
|
| 138 |
</article>
|
| 139 |
<article class="result-card">
|
| 140 |
-
<div class="card-title"><span>!</span><h3>Red flags found</h3><button class="copy-button" data-copy="redFlagsList">Copy</button></div>
|
| 141 |
<ul id="redFlagsList"></ul>
|
| 142 |
</article>
|
| 143 |
<article class="result-card">
|
| 144 |
-
<div class="card-title"><span>✓</span><h3>Safe next steps</h3><button class="copy-button" data-copy="nextStepsList">Copy</button></div>
|
| 145 |
<ol id="nextStepsList"></ol>
|
| 146 |
</article>
|
| 147 |
<article id="replyCard" class="result-card reply-card">
|
| 148 |
-
<div class="card-title"><span>↗</span><h3>Polite reply draft</h3><button class="copy-button" data-copy="replyText">Copy</button></div>
|
| 149 |
<p id="replyText"></p>
|
| 150 |
</article>
|
| 151 |
</div>
|
| 152 |
</section>
|
| 153 |
|
| 154 |
<section class="disclaimer">
|
| 155 |
-
<strong>Important safety note</strong>
|
| 156 |
-
<p>Pakistan Notice Helper does not provide official verification. It checks common scam signals and gives safe next steps. Always verify through official websites or helplines before making payments or sharing personal information.</p>
|
| 157 |
</section>
|
| 158 |
</main>
|
| 159 |
|
| 160 |
-
<footer><span>Built for safer digital decisions in Pakistan.</span><span>Never share OTPs, PINs, passwords, or CVVs.</span></footer>
|
| 161 |
</body>
|
| 162 |
</html>
|
|
|
|
| 12 |
</head>
|
| 13 |
<body>
|
| 14 |
<header class="topbar">
|
| 15 |
+
<div class="topbar-left">
|
| 16 |
+
<div class="language-switch" role="group" aria-label="Language">
|
| 17 |
+
<button class="language-option active" type="button" data-language="en" aria-pressed="true">English</button>
|
| 18 |
+
<button class="language-option urdu-option" type="button" data-language="ur" aria-pressed="false">اردو</button>
|
| 19 |
+
</div>
|
| 20 |
+
<a class="brand" href="/" aria-label="Pakistan Notice Helper home">
|
| 21 |
+
<span class="brand-mark"><img src="/static/logo.png" alt=""></span>
|
| 22 |
+
<span class="brand-name">Pakistan Notice Helper</span>
|
| 23 |
+
</a>
|
| 24 |
+
</div>
|
| 25 |
<div class="status-row">
|
| 26 |
+
<span id="modelStatus" class="status-badge"><span class="status-dot"></span><span class="status-text" data-i18n="statusChecking">Checking model</span></span>
|
| 27 |
</div>
|
| 28 |
</header>
|
| 29 |
|
| 30 |
<main>
|
| 31 |
<section class="hero">
|
| 32 |
<div class="hero-copy">
|
| 33 |
+
<p class="eyebrow" data-i18n="heroEyebrow">Understand before you act</p>
|
| 34 |
+
<h1><span data-i18n="heroTitle">Does this notice look</span> <span data-i18n="heroSafe">safe?</span></h1>
|
| 35 |
+
<p class="hero-text" data-i18n="heroText">Check suspicious bills, bank alerts, FBR-style messages, challans, courier notices, and SMS screenshots for common scam signals.</p>
|
| 36 |
<div class="trust-note">
|
| 37 |
<span class="shield" aria-hidden="true">✓</span>
|
| 38 |
<p>
|
| 39 |
+
<strong data-i18n="trustTitle">AI-assisted safety check</strong>
|
| 40 |
+
<span data-i18n="trustText">The AI reads the notice, identifies scam signals, and returns a structured risk assessment with safer next steps.</span>
|
| 41 |
</p>
|
| 42 |
</div>
|
| 43 |
</div>
|
|
|
|
| 49 |
<section class="workspace" aria-labelledby="checkerTitle">
|
| 50 |
<div class="section-heading">
|
| 51 |
<div>
|
| 52 |
+
<p class="eyebrow" data-i18n="checkerEyebrow">Free safety check</p>
|
| 53 |
+
<h2 id="checkerTitle" data-i18n="checkerTitle">Check a notice or message</h2>
|
| 54 |
</div>
|
| 55 |
+
<p data-i18n="modelDescription">Analysis runs on the deployed Qwen3.5 4B multimodal model.</p>
|
| 56 |
</div>
|
| 57 |
|
| 58 |
<form id="noticeForm">
|
| 59 |
<div class="input-grid">
|
| 60 |
<div class="field-card upload-card">
|
| 61 |
+
<div class="field-label"><span>1</span><span class="field-label-text" data-i18n="uploadLabel">Upload a screenshot</span></div>
|
| 62 |
<label id="dropZone" class="drop-zone" for="imageInput">
|
| 63 |
<input id="imageInput" type="file" accept="image/png,image/jpeg,image/webp">
|
| 64 |
<span class="upload-icon" aria-hidden="true">↑</span>
|
| 65 |
+
<strong data-i18n="dropImage">Drop an image here</strong>
|
| 66 |
+
<small data-i18n="browseImage">or tap to browse PNG, JPG, or WebP</small>
|
| 67 |
+
<img id="imagePreview" alt="Selected notice preview" data-i18n-alt="previewAlt">
|
| 68 |
+
<button id="removeImage" class="text-button" type="button" data-i18n="removeImage">Remove image</button>
|
| 69 |
</label>
|
| 70 |
+
<div id="uploadHint" class="mode-hint"><span class="hint-icon">↑</span><span data-i18n="imageMode">Screenshot mode active — text input is locked</span></div>
|
| 71 |
</div>
|
| 72 |
|
| 73 |
<div class="field-card">
|
| 74 |
+
<label class="field-label" for="noticeText"><span>2</span><span class="field-label-text" data-i18n="pasteLabel">Or paste the message</span></label>
|
| 75 |
+
<textarea id="noticeText" maxlength="12000" placeholder="Paste the SMS, email, bill text, or notice here..." data-i18n-placeholder="textPlaceholder"></textarea>
|
| 76 |
+
<div class="field-meta"><span data-i18n="languageSupport">English, Urdu, and Roman Urdu supported by compatible models</span><span id="charCount">0 / 12,000</span></div>
|
| 77 |
+
<div id="textHint" class="mode-hint"><span class="hint-icon">✎</span><span data-i18n="textMode">Text mode active — image upload is locked</span></div>
|
| 78 |
</div>
|
| 79 |
</div>
|
| 80 |
|
|
|
|
| 82 |
<label class="trace-consent" for="saveTrace">
|
| 83 |
<input id="saveTrace" type="checkbox" checked>
|
| 84 |
<span>
|
| 85 |
+
<strong data-i18n="traceTitle">Publish privacy-safe trace</strong>
|
| 86 |
+
<small data-i18n="traceText">Stores automated redacted text or an image description. Raw text, screenshots, links, identifiers, and model text are not stored.</small>
|
| 87 |
</span>
|
| 88 |
</label>
|
| 89 |
<div class="form-actions">
|
| 90 |
<button id="analyzeButton" class="primary-button" type="submit">
|
| 91 |
+
<span class="button-label" data-i18n="checkButton">Check this notice</span>
|
| 92 |
<span class="spinner" aria-hidden="true"></span>
|
| 93 |
</button>
|
| 94 |
<button id="resetButton" class="reset-button" type="button">
|
| 95 |
+
<span data-i18n="startOver">Start over</span>
|
| 96 |
</button>
|
| 97 |
</div>
|
| 98 |
</form>
|
|
|
|
| 100 |
|
| 101 |
<section class="examples" aria-labelledby="examplesTitle">
|
| 102 |
<div class="section-heading compact">
|
| 103 |
+
<div><p class="eyebrow" data-i18n="examplesEyebrow">Try an example</p><h2 id="examplesTitle" data-i18n="examplesTitle">Common messages in Pakistan</h2></div>
|
| 104 |
</div>
|
| 105 |
<div class="example-grid">
|
| 106 |
<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.">
|
| 107 |
+
<span class="example-icon">PK</span><span><strong data-i18n="courierFee">Courier fee</strong><small data-i18n="courierFeeText">Urgent parcel payment link</small></span>
|
| 108 |
</button>
|
| 109 |
<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.">
|
| 110 |
+
<span class="example-icon">FBR</span><span><strong data-i18n="taxRefund">Tax refund</strong><small data-i18n="taxRefundText">Unexpected refund request</small></span>
|
| 111 |
</button>
|
| 112 |
<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.">
|
| 113 |
+
<span class="example-icon">OTP</span><span><strong data-i18n="bankAlert">Bank alert</strong><small data-i18n="bankAlertText">Security code request</small></span>
|
| 114 |
</button>
|
| 115 |
</div>
|
| 116 |
|
| 117 |
+
<p class="eyebrow" style="margin:28px 0 14px" data-i18n="screenshotsTitle">Real scam screenshots</p>
|
| 118 |
<div class="example-grid example-grid-images">
|
| 119 |
<button class="example-card example-image-card" data-example-id="image-courier" data-image="/static/example-courier.jpeg">
|
| 120 |
<img src="/static/example-courier.jpeg" alt="Courier scam screenshot" class="example-thumb">
|
| 121 |
+
<span><strong data-i18n="courierScam">Courier scam</strong><small data-i18n="courierScamText">Fake delivery fee message</small></span>
|
| 122 |
</button>
|
| 123 |
<button class="example-card example-image-card" data-example-id="image-mobile" data-image="/static/example-mobile.png">
|
| 124 |
<img src="/static/example-mobile.png" alt="Mobile scam screenshot" class="example-thumb">
|
| 125 |
+
<span><strong data-i18n="mobileScam">Mobile scam</strong><small data-i18n="mobileScamText">Fake mobile operator message</small></span>
|
| 126 |
</button>
|
| 127 |
<button class="example-card example-image-card" data-example-id="image-traffic" data-image="/static/example-trafic.png">
|
| 128 |
<img src="/static/example-trafic.png" alt="Traffic challan scam screenshot" class="example-thumb">
|
| 129 |
+
<span><strong data-i18n="trafficScam">Traffic challan</strong><small data-i18n="trafficScamText">Fake e-challan fine message</small></span>
|
| 130 |
</button>
|
| 131 |
</div>
|
| 132 |
</section>
|
| 133 |
|
| 134 |
<section id="results" class="results" aria-live="polite" hidden>
|
| 135 |
<div class="result-header">
|
| 136 |
+
<div><p class="eyebrow" data-i18n="resultsEyebrow">Safety assessment</p><h2 data-i18n="resultsTitle">What we found</h2></div>
|
| 137 |
<div id="riskBadge" class="risk-badge"></div>
|
| 138 |
</div>
|
| 139 |
<p id="resultSource" class="result-source"></p>
|
| 140 |
<div class="result-grid">
|
| 141 |
<article class="result-card explanation-card">
|
| 142 |
+
<div class="card-title"><span>i</span><h3 data-i18n="explanationTitle">Simple explanation</h3><button class="copy-button" data-copy="explanationText" data-i18n="copy">Copy</button></div>
|
| 143 |
<p id="explanationText"></p>
|
| 144 |
</article>
|
| 145 |
<article class="result-card">
|
| 146 |
+
<div class="card-title"><span>!</span><h3 data-i18n="redFlagsTitle">Red flags found</h3><button class="copy-button" data-copy="redFlagsList" data-i18n="copy">Copy</button></div>
|
| 147 |
<ul id="redFlagsList"></ul>
|
| 148 |
</article>
|
| 149 |
<article class="result-card">
|
| 150 |
+
<div class="card-title"><span>✓</span><h3 data-i18n="nextStepsTitle">Safe next steps</h3><button class="copy-button" data-copy="nextStepsList" data-i18n="copy">Copy</button></div>
|
| 151 |
<ol id="nextStepsList"></ol>
|
| 152 |
</article>
|
| 153 |
<article id="replyCard" class="result-card reply-card">
|
| 154 |
+
<div class="card-title"><span>↗</span><h3 data-i18n="replyTitle">Polite reply draft</h3><button class="copy-button" data-copy="replyText" data-i18n="copy">Copy</button></div>
|
| 155 |
<p id="replyText"></p>
|
| 156 |
</article>
|
| 157 |
</div>
|
| 158 |
</section>
|
| 159 |
|
| 160 |
<section class="disclaimer">
|
| 161 |
+
<strong data-i18n="disclaimerTitle">Important safety note</strong>
|
| 162 |
+
<p data-i18n="disclaimerText">Pakistan Notice Helper does not provide official verification. It checks common scam signals and gives safe next steps. Always verify through official websites or helplines before making payments or sharing personal information.</p>
|
| 163 |
</section>
|
| 164 |
</main>
|
| 165 |
|
| 166 |
+
<footer><span data-i18n="footerOne">Built for safer digital decisions in Pakistan.</span><span data-i18n="footerTwo">Never share OTPs, PINs, passwords, or CVVs.</span></footer>
|
| 167 |
</body>
|
| 168 |
</html>
|
static/styles.css
CHANGED
|
@@ -34,6 +34,19 @@ button { color: inherit; }
|
|
| 34 |
background: var(--green-950);
|
| 35 |
color: white;
|
| 36 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
.brand { color: white; text-decoration: none; display: flex; align-items: center; gap: 12px; font-weight: 800; }
|
| 38 |
.brand-mark {
|
| 39 |
width: 38px; height: 38px; display: grid; place-items: center; overflow: hidden;
|
|
@@ -65,7 +78,7 @@ main { overflow: hidden; }
|
|
| 65 |
.hero .eyebrow { color: #8de3b4; }
|
| 66 |
h1, h2, h3, p { overflow-wrap: anywhere; }
|
| 67 |
h1 { margin: 0; font-size: clamp(42px, 7vw, 78px); line-height: .98; letter-spacing: -.055em; }
|
| 68 |
-
h1 span { color: #79d9a6; }
|
| 69 |
.hero-text { margin: 24px 0; color: #d7e9df; font-size: clamp(17px, 2vw, 21px); line-height: 1.6; }
|
| 70 |
.trust-note {
|
| 71 |
max-width: 680px; padding: 16px; display: grid; grid-template-columns: 36px minmax(0, 1fr);
|
|
@@ -102,7 +115,7 @@ h1 span { color: #79d9a6; }
|
|
| 102 |
.input-grid { display: grid; grid-template-columns: .9fr 1.1fr; gap: 20px; }
|
| 103 |
.field-card { min-width: 0; }
|
| 104 |
.field-label { display: flex; align-items: center; gap: 9px; margin-bottom: 10px; font-size: 14px; font-weight: 800; }
|
| 105 |
-
.field-label span { width: 24px; height: 24px; border-radius: 8px; display: grid; place-items: center; background: var(--green-100); color: var(--green-800); font-size: 12px; }
|
| 106 |
.drop-zone {
|
| 107 |
min-height: 230px; padding: 24px; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
| 108 |
border: 1.5px dashed #a8c8b5; border-radius: 18px; background: var(--green-50); cursor: pointer; text-align: center; transition: .2s ease;
|
|
@@ -214,6 +227,20 @@ textarea:disabled { opacity: .45; background: #f0f5f2; cursor: not-allowed; }
|
|
| 214 |
.disclaimer p { margin: 7px 0 0; color: #675f4e; line-height: 1.6; font-size: 14px; }
|
| 215 |
footer { padding: 24px clamp(18px, 5vw, 72px); display: flex; justify-content: space-between; gap: 18px; color: #c4d9cc; background: var(--green-950); font-size: 12px; }
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
@media (max-width: 1100px) {
|
| 218 |
.hero { grid-template-columns: 1fr; }
|
| 219 |
.hero-image { display: none; }
|
|
@@ -233,7 +260,9 @@ footer { padding: 24px clamp(18px, 5vw, 72px); display: flex; justify-content: s
|
|
| 233 |
.topbar { min-height: 64px; padding: 11px 14px; align-items: center; gap: 10px; }
|
| 234 |
.brand { gap: 9px; min-width: 0; }
|
| 235 |
.brand-mark { width: 32px; height: 32px; flex: 0 0 32px; }
|
| 236 |
-
.
|
|
|
|
|
|
|
| 237 |
.status-badge { max-width: 126px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 238 |
.hero { min-height: 390px; padding: 48px 20px 105px; }
|
| 239 |
.workspace { width: calc(100% - 20px); padding: 22px 16px; border-radius: 22px; }
|
|
|
|
| 34 |
background: var(--green-950);
|
| 35 |
color: white;
|
| 36 |
}
|
| 37 |
+
.topbar-left { display: flex; align-items: center; gap: 18px; min-width: 0; }
|
| 38 |
+
.language-switch {
|
| 39 |
+
flex: 0 0 auto; padding: 3px; display: flex; direction: ltr;
|
| 40 |
+
border: 1px solid rgba(255,255,255,.22); border-radius: 11px;
|
| 41 |
+
background: rgba(255,255,255,.08);
|
| 42 |
+
}
|
| 43 |
+
.language-option {
|
| 44 |
+
min-height: 30px; padding: 5px 9px; border: 0; border-radius: 8px;
|
| 45 |
+
background: transparent; color: #d6e8de; font-size: 11px; font-weight: 800;
|
| 46 |
+
cursor: pointer;
|
| 47 |
+
}
|
| 48 |
+
.language-option.active { background: white; color: var(--green-950); }
|
| 49 |
+
.urdu-option { font-family: "Noto Nastaliq Urdu", "Noto Sans Arabic", "Segoe UI", sans-serif; font-size: 13px; }
|
| 50 |
.brand { color: white; text-decoration: none; display: flex; align-items: center; gap: 12px; font-weight: 800; }
|
| 51 |
.brand-mark {
|
| 52 |
width: 38px; height: 38px; display: grid; place-items: center; overflow: hidden;
|
|
|
|
| 78 |
.hero .eyebrow { color: #8de3b4; }
|
| 79 |
h1, h2, h3, p { overflow-wrap: anywhere; }
|
| 80 |
h1 { margin: 0; font-size: clamp(42px, 7vw, 78px); line-height: .98; letter-spacing: -.055em; }
|
| 81 |
+
h1 > span:last-child { color: #79d9a6; }
|
| 82 |
.hero-text { margin: 24px 0; color: #d7e9df; font-size: clamp(17px, 2vw, 21px); line-height: 1.6; }
|
| 83 |
.trust-note {
|
| 84 |
max-width: 680px; padding: 16px; display: grid; grid-template-columns: 36px minmax(0, 1fr);
|
|
|
|
| 115 |
.input-grid { display: grid; grid-template-columns: .9fr 1.1fr; gap: 20px; }
|
| 116 |
.field-card { min-width: 0; }
|
| 117 |
.field-label { display: flex; align-items: center; gap: 9px; margin-bottom: 10px; font-size: 14px; font-weight: 800; }
|
| 118 |
+
.field-label > span:first-child { width: 24px; height: 24px; border-radius: 8px; display: grid; place-items: center; background: var(--green-100); color: var(--green-800); font-size: 12px; }
|
| 119 |
.drop-zone {
|
| 120 |
min-height: 230px; padding: 24px; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
| 121 |
border: 1.5px dashed #a8c8b5; border-radius: 18px; background: var(--green-50); cursor: pointer; text-align: center; transition: .2s ease;
|
|
|
|
| 227 |
.disclaimer p { margin: 7px 0 0; color: #675f4e; line-height: 1.6; font-size: 14px; }
|
| 228 |
footer { padding: 24px clamp(18px, 5vw, 72px); display: flex; justify-content: space-between; gap: 18px; color: #c4d9cc; background: var(--green-950); font-size: 12px; }
|
| 229 |
|
| 230 |
+
html[lang="ur"] body {
|
| 231 |
+
direction: rtl;
|
| 232 |
+
font-family: "Noto Nastaliq Urdu", "Noto Sans Arabic", "Segoe UI", Tahoma, sans-serif;
|
| 233 |
+
}
|
| 234 |
+
html[lang="ur"] .topbar { direction: ltr; }
|
| 235 |
+
html[lang="ur"] .brand, html[lang="ur"] .status-row { direction: ltr; }
|
| 236 |
+
html[lang="ur"] .hero-copy, html[lang="ur"] .section-heading,
|
| 237 |
+
html[lang="ur"] .field-card, html[lang="ur"] .examples,
|
| 238 |
+
html[lang="ur"] .results, html[lang="ur"] .disclaimer,
|
| 239 |
+
html[lang="ur"] footer { direction: rtl; text-align: right; }
|
| 240 |
+
html[lang="ur"] .example-card { text-align: right; }
|
| 241 |
+
html[lang="ur"] .result-card ul, html[lang="ur"] .result-card ol { padding-left: 0; padding-right: 22px; }
|
| 242 |
+
html[lang="ur"] textarea { direction: rtl; text-align: right; }
|
| 243 |
+
|
| 244 |
@media (max-width: 1100px) {
|
| 245 |
.hero { grid-template-columns: 1fr; }
|
| 246 |
.hero-image { display: none; }
|
|
|
|
| 260 |
.topbar { min-height: 64px; padding: 11px 14px; align-items: center; gap: 10px; }
|
| 261 |
.brand { gap: 9px; min-width: 0; }
|
| 262 |
.brand-mark { width: 32px; height: 32px; flex: 0 0 32px; }
|
| 263 |
+
.topbar-left { gap: 8px; }
|
| 264 |
+
.brand-name { display: none; }
|
| 265 |
+
.language-option { padding: 4px 7px; }
|
| 266 |
.status-badge { max-width: 126px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 267 |
.hero { min-height: 390px; padding: 48px 20px 105px; }
|
| 268 |
.workspace { width: calc(100% - 20px); padding: 22px 16px; border-radius: 22px; }
|