noticecheck / static /app.js
Abid Ali Awan
Add notice image guidance and update runtime docs
edecb11
Raw
History Blame Contribute Delete
24.8 kB
const elements = {
form: document.querySelector("#noticeForm"),
text: document.querySelector("#noticeText"),
image: document.querySelector("#imageInput"),
preview: document.querySelector("#imagePreview"),
removeImage: document.querySelector("#removeImage"),
dropZone: document.querySelector("#dropZone"),
charCount: document.querySelector("#charCount"),
button: document.querySelector("#analyzeButton"),
resetButton: document.querySelector("#resetButton"),
error: document.querySelector("#formError"),
status: document.querySelector("#modelStatus"),
results: document.querySelector("#results"),
risk: document.querySelector("#riskBadge"),
source: document.querySelector("#resultSource"),
uploadHint: document.querySelector("#uploadHint"),
textHint: document.querySelector("#textHint"),
saveTrace: document.querySelector("#saveTrace"),
statusText: document.querySelector(".status-text"),
languageOptions: document.querySelectorAll(".language-option"),
};
const translations = {
en: {
pageTitle: "NoticeCheck",
pageDescription: "Review suspicious Pakistani messages before you click, pay, or reply.",
statusChecking: "Checking model",
statusReady: "Local models ready",
statusCredentials: "Local model setup required",
statusUnavailable: "Local model unavailable",
heroEyebrow: "Pause. Inspect. Decide.",
heroTitle: "Look twice before you",
heroSafe: "trust it.",
heroText: "NoticeCheck reviews suspicious messages and screenshots for pressure tactics, unsafe requests, and scam warning signs.",
featureRedFlags: "Red flags and pressure tactics",
featureVerify: "Suspicious links and sender patterns",
featureSteps: "Safer next steps to take",
trustTitle: "How it works",
trustText: "Paste a message or upload a screenshot. NoticeCheck returns a risk label, warning signs, and safer next steps — all processed locally on your device.",
checkerEyebrow: "Message review",
checkerTitle: "What did you receive?",
modelDescription: "MiniCPM5-1B reviews messages; Nemotron-Parse v1.2 reads screenshots.",
uploadLabel: "Upload a screenshot",
dropImage: "Drop an image here",
browseImage: "or tap to browse PNG, JPG, or WebP",
previewAlt: "Selected notice preview",
removeImage: "Remove image",
imageMode: "Screenshot mode active — text input is locked",
pasteLabel: "Or paste the message",
textPlaceholder: "Paste the SMS, email, bill text, or notice here...",
languageSupport: "English interface",
textMode: "Text mode active — image upload is locked",
traceTitle: "Publish privacy-safe trace",
traceText: "Stores automated redacted text or an image description. Raw text, screenshots, links, identifiers, and model text are not stored.",
checkButton: "Check this notice",
checkingButton: "Checking safely...",
startOver: "Start over",
examplesEyebrow: "Practice cases",
examplesTitle: "Recognize the pressure pattern",
courierFee: "Courier fee",
courierFeeText: "Urgent parcel payment link",
taxRefund: "Tax refund",
taxRefundText: "Unexpected refund request",
bankAlert: "Bank alert",
bankAlertText: "Security code request",
screenshotsTitle: "Real scam screenshots",
courierScam: "Courier scam",
courierScamText: "Fake delivery fee message",
mobileScam: "Mobile scam",
mobileScamText: "Fake mobile operator message",
trafficScam: "Traffic challan",
trafficScamText: "Fake e-challan fine message",
resultsEyebrow: "Safety assessment",
resultsTitle: "What we found",
explanationTitle: "Simple explanation",
redFlagsTitle: "Red flags found",
nextStepsTitle: "Safe next steps",
replyTitle: "Polite reply draft",
copy: "Copy",
copied: "Copied",
disclaimerTitle: "Before you act",
disclaimerText: "This tool flags common scam patterns — it does not verify senders. Always confirm through an official website, app, or helpline you find yourself.",
footerOne: "NoticeCheck · A local-first message review tool",
footerTwo: "Never share OTPs, PINs, passwords, or CVVs.",
requestStartError: "The app could not start the request.",
requestReadError: "The app could not read the result.",
requestFailedError: "The request could not be completed.",
noResultError: "The app returned no result.",
analyzeError: "Unable to analyze this input.",
modelConfigurationError: "The local model runtime is not configured.",
modelUnavailableError: "The model is unavailable or still starting. Please try again.",
modelInvalidError: "The model returned an incomplete response. Please try again.",
gpuQuotaError: "GPU quota exceeded. Please try again later or authenticate with a Hugging Face token for more quota.",
ocrUnavailableError: "Nemotron-Parse is unavailable. Paste the notice text instead.",
noticeImageRequiredWarning: "This image does not contain readable notice text. Upload a clear screenshot of the full notice or message.",
ocrLanguageError: "This language may not be fully supported. Results may vary.",
imageTypeError: "Use a PNG, JPG, or WebP image.",
imageSizeError: "Please choose an image smaller than 8 MB.",
exampleImageError: "Could not load the example image.",
emptyInputError: "Paste a message or upload a screenshot to continue.",
modelSource: "Analyzed locally by MiniCPM5-1B.",
cachedSource: "Cached model result",
riskLooksNormal: "Looks normal",
riskVerifyFirst: "Verify first",
riskSuspicious: "Suspicious",
riskLikelyScam: "Likely scam",
riskInappropriate: "Inappropriate",
},
ur: {
pageTitle: "نوٹس چیک",
pageDescription: "پاکستانی نوٹس اور پیغامات میں عام فراڈ کی علامات چیک کریں۔",
statusChecking: "ماڈل کی دستیابی چیک ہو رہی ہے",
statusReady: "ماڈل تیار ہے",
statusCredentials: "ماڈل کی رسائی درکار ہے",
statusUnavailable: "ماڈل فی الحال دستیاب نہیں",
heroEyebrow: "رکیں، دیکھیں، پھر فیصلہ کریں",
heroTitle: "بھروسا کرنے سے پہلے",
heroSafe: "دوبارہ دیکھیں",
heroText: "نوٹس چیک مشکوک پیغامات اور اسکرین شاٹس میں دباؤ، غیر محفوظ مطالبات اور فراڈ کی نشانیاں تلاش کرتا ہے۔",
featureRedFlags: "خطرے کی نشانیاں اور دباؤ کے طریقے",
featureVerify: "مشکوک لنکس اور بھیجنے والے کے پیٹرن",
featureSteps: "محفوظ اگلے اقدامات",
trustTitle: "یہ کیسے کام کرتا ہے",
trustText: "پیغام پیسٹ کریں یا اسکرین شاٹ اپ لوڈ کریں۔ نوٹس چیک خطرے کا لیبل، خطرے کی نشانیاں، اور محفوظ اگلے اقدامات فراہم کرتا ہے — سب کچھ آپ کے ڈیوائس پر پروسیس ہوتا ہے۔",
checkerEyebrow: "پیغام کا جائزہ",
checkerTitle: "آپ کو کیا موصول ہوا؟",
modelDescription: "MiniCPM5-1B مقامی تجزیہ کرتا ہے اور Nemotron-Parse اسکرین شاٹ پڑھتا ہے۔",
uploadLabel: "اسکرین شاٹ اپ لوڈ کریں",
dropImage: "تصویر یہاں اپ لوڈ کریں",
browseImage: "یا پی این جی، جے پی جی یا ویب پی فائل منتخب کرنے کے لیے دبائیں",
previewAlt: "منتخب نوٹس کا پیش منظر",
removeImage: "تصویر ہٹائیں",
imageMode: "اسکرین شاٹ منتخب ہے، متن کا خانہ بند کر دیا گیا ہے",
pasteLabel: "یا پیغام کا متن درج کریں",
textPlaceholder: "ایس ایم ایس، ای میل، بل یا نوٹس کا متن یہاں پیسٹ کریں…",
languageSupport: "انگریزی معاون ہے؛ اردو اور رومن اردو تجرباتی ہیں",
textMode: "متن درج ہے، تصویر اپ لوڈ کرنے کا اختیار بند ہے",
traceTitle: "رازداری کے ساتھ گمنام ٹریس شائع کریں",
traceText: "صرف چھپایا گیا متن یا تصویر کی عمومی تفصیل محفوظ ہوتی ہے۔ اصل پیغام، اسکرین شاٹ، لنک، شناختی معلومات اور ماڈل کا جواب محفوظ نہیں کیا جاتا۔",
checkButton: "نوٹس جانچیں",
checkingButton: "جانچ جاری ہے…",
startOver: "دوبارہ شروع کریں",
examplesEyebrow: "مثالی کیس",
examplesTitle: "دباؤ کے طریقے پہچانیں",
courierFee: "کوریئر فیس",
courierFeeText: "پارسل فیس کی فوری ادائیگی کا لنک",
taxRefund: "ٹیکس ریفنڈ",
taxRefundText: "غیر متوقع رقم واپسی کا پیغام",
bankAlert: "بینک الرٹ",
bankAlertText: "حفاظتی کوڈ مانگنے والا پیغام",
screenshotsTitle: "فراڈ کے حقیقی اسکرین شاٹس",
courierScam: "کوریئر فراڈ",
courierScamText: "جعلی ڈیلیوری فیس کا پیغام",
mobileScam: "موبائل فراڈ",
mobileScamText: "جعلی موبائل آپریٹر پیغام",
trafficScam: "ٹریفک چالان",
trafficScamText: "جعلی ای چالان کا پیغام",
resultsEyebrow: "حفاظتی جائزہ",
resultsTitle: "جانچ کا نتیجہ",
explanationTitle: "سادہ وضاحت",
redFlagsTitle: "خطرے کی نشانیاں",
nextStepsTitle: "محفوظ اگلے اقدامات",
replyTitle: "جواب کا شائستہ مسودہ",
copy: "کاپی کریں",
copied: "کاپی ہو گیا",
disclaimerTitle: "عمل سے پہلے",
disclaimerText: "یہ ٹول عام فراڈ کے پیٹرن نشاندہی کرتا ہے — بھیجنے والے کی تصدیق نہیں کرتا۔ ہمیشہ خود تلاش کی گئی سرکاری ویب سائٹ، ایپ، یا ہیلپ لائن سے تصدیق کریں۔",
footerOne: "نوٹس چیک · مقامی پیغام جائزہ ٹول",
footerTwo: "اپنا او ٹی پی، پن، پاس ورڈ یا سی وی وی کبھی شیئر نہ کریں۔",
requestStartError: "جانچ شروع نہیں ہو سکی۔",
requestReadError: "جانچ کا نتیجہ موصول نہیں ہو سکا۔",
requestFailedError: "درخواست مکمل نہیں ہو سکی۔",
noResultError: "کوئی نتیجہ موصول نہیں ہوا۔",
analyzeError: "اس مواد کی جانچ نہیں ہو سکی۔",
modelConfigurationError: "مقامی ماڈل درست طریقے سے ترتیب نہیں دیا گیا۔",
modelUnavailableError: "ماڈل دستیاب نہیں یا ابھی شروع ہو رہا ہے۔ براہ کرم دوبارہ کوشش کریں۔",
modelInvalidError: "ماڈل کا جواب مکمل نہیں تھا۔ براہ کرم دوبارہ کوشش کریں۔",
gpuQuotaError: "GPU کوٹہ ختم ہو گیا۔ براہ کرم بعد میں دوبارہ کوشش کریں یا مزید کوٹہ کے لیے Hugging Face ٹوکن سے تصدیق کریں۔",
ocrUnavailableError: "Nemotron-Parse دستیاب نہیں۔ نوٹس کا متن پیسٹ کریں۔",
noticeImageRequiredWarning: "اس تصویر میں نوٹس کا واضح متن موجود نہیں ہے۔ مکمل نوٹس یا پیغام کا صاف اسکرین شاٹ اپ لوڈ کریں۔",
ocrLanguageError: "یہ زبان مکمل طور پر سپورٹ نہیں ہو سکتی۔ نتائج مختلف ہو سکتے ہیں۔",
imageTypeError: "PNG، JPG یا WebP تصویر استعمال کریں۔",
imageSizeError: "براہ کرم 8 MB سے چھوٹی تصویر منتخب کریں۔",
exampleImageError: "مثالی تصویر لوڈ نہیں ہو سکی۔",
emptyInputError: "پیغام کا متن درج کریں یا اسکرین شاٹ اپ لوڈ کریں۔",
modelSource: "یہ نتیجہ MiniCPM5-1B نے مقامی طور پر تیار کیا ہے۔",
cachedSource: "پہلے سے محفوظ ماڈل نتیجہ",
riskLooksNormal: "معمول کا پیغام",
riskVerifyFirst: "پہلے تصدیق کریں",
riskSuspicious: "مشکوک",
riskLikelyScam: "ممکنہ فراڈ",
riskInappropriate: "نامناسب",
},
};
let imageDataUrl = "";
let activeMode = null;
let activeExampleId = "";
let currentLanguage = "en";
localStorage.removeItem("noticecheck-language");
let currentStatus = null;
let currentRiskLabel = "";
function t(key) {
return translations[currentLanguage][key] || translations.en[key] || key;
}
function applyLanguage(language) {
currentLanguage = "en";
document.documentElement.lang = currentLanguage;
document.documentElement.dir = currentLanguage === "ur" ? "rtl" : "ltr";
document.title = t("pageTitle");
document.querySelector('meta[name="description"]').content = t("pageDescription");
document.querySelectorAll("[data-i18n]").forEach((element) => {
element.textContent = t(element.dataset.i18n);
});
document.querySelectorAll("[data-i18n-placeholder]").forEach((element) => {
element.placeholder = t(element.dataset.i18nPlaceholder);
});
document.querySelectorAll("[data-i18n-alt]").forEach((element) => {
element.alt = t(element.dataset.i18nAlt);
});
elements.languageOptions.forEach((button) => {
const active = button.dataset.language === currentLanguage;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
setStatus(currentStatus);
if (currentRiskLabel) setRiskLabel(currentRiskLabel);
if (elements.button.classList.contains("loading")) {
elements.button.querySelector(".button-label").textContent = t("checkingButton");
}
}
function setRiskLabel(label) {
currentRiskLabel = label;
const keys = {
"Looks normal": "riskLooksNormal",
"Verify first": "riskVerifyFirst",
Suspicious: "riskSuspicious",
"Likely scam": "riskLikelyScam",
Inappropriate: "riskInappropriate",
};
elements.risk.textContent = t(keys[label] || label);
}
async function requestZeroGpuHeaders() {
if (!window.supports_zerogpu_headers) return {};
const hostname = window.location.hostname;
if (!hostname.endsWith(".hf.space") && !hostname.includes(".dev.")) return {};
const origin = hostname.includes(".dev.")
? `https://moon-${hostname.split(".")[1]}.dev.spaces.huggingface.tech`
: "https://huggingface.co";
try {
return await new Promise((resolve) => {
const channel = new MessageChannel();
const timeout = setTimeout(() => {
channel.port1.close();
resolve({});
}, 2000);
channel.port1.onmessage = ({ data }) => {
clearTimeout(timeout);
channel.port1.close();
resolve(data && typeof data === "object" ? data : {});
};
window.parent.postMessage("zerogpu-headers", origin, [channel.port2]);
});
} catch {
return {};
}
}
async function callGradioApi(name, data) {
const headers = { "Content-Type": "application/json" };
const zeroHeaders = await requestZeroGpuHeaders();
Object.assign(headers, zeroHeaders);
const response = await fetch(`/gradio_api/call/${name}`, {
method: "POST",
headers,
body: JSON.stringify({ data }),
});
if (!response.ok) throw new Error(t("requestStartError"));
const { event_id: eventId } = await response.json();
const stream = await fetch(`/gradio_api/call/${name}/${eventId}`);
if (!stream.ok || !stream.body) throw new Error(t("requestReadError"));
const reader = stream.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const chunks = buffer.split("\n\n");
buffer = chunks.pop() || "";
for (const chunk of chunks) {
const event = chunk.match(/^event:\s*(.+)$/m)?.[1];
const raw = chunk.match(/^data:\s*(.+)$/m)?.[1];
if (event === "error") throw new Error(t("requestFailedError"));
if (event === "complete" && raw) {
const values = JSON.parse(raw);
return values[0];
}
}
}
throw new Error(t("noResultError"));
}
function setStatus(status) {
if (!status) {
elements.statusText.textContent = t("statusChecking");
return;
}
currentStatus = status;
elements.statusText.textContent = status.connected
? status.label || t("statusReady")
: status.label?.toLowerCase().includes("credentials")
? t("statusCredentials")
: t("statusUnavailable");
elements.status.classList.toggle("connected", Boolean(status.connected));
}
async function loadStatus() {
try {
setStatus(await callGradioApi("status", []));
} catch {
setStatus({ connected: false, label: "Local model unavailable" });
}
}
function showError(message = "", tone = "error") {
elements.error.textContent = message;
elements.error.classList.toggle("visible", Boolean(message));
elements.error.classList.toggle("warning", Boolean(message) && tone === "warning");
}
function setMode(mode) {
activeMode = mode;
const isImage = mode === "image";
const isText = mode === "text";
elements.text.disabled = isImage;
elements.dropZone.classList.toggle("disabled", isText);
elements.image.disabled = isText;
elements.uploadHint.classList.toggle("visible", isImage);
elements.textHint.classList.toggle("visible", isText);
elements.resetButton.classList.toggle("visible", Boolean(mode));
}
function setLoading(loading) {
elements.button.disabled = loading;
elements.button.classList.toggle("loading", loading);
elements.button.querySelector(".button-label").textContent =
loading ? t("checkingButton") : t("checkButton");
}
function renderList(selector, items) {
const list = document.querySelector(selector);
list.replaceChildren(...items.map((item) => {
const li = document.createElement("li");
li.textContent = item;
return li;
}));
}
function renderResult(payload) {
if (!payload.ok) {
const localizedError = payload.error_code
? translations[currentLanguage][payload.error_code]
: "";
if (payload.warning) {
elements.results.hidden = true;
setStatus(payload.status);
showError(localizedError || payload.error || t("analyzeError"), "warning");
return;
}
throw new Error(localizedError || payload.error || t("analyzeError"));
}
const result = payload.assessment;
setStatus(payload.status);
elements.risk.className = `risk-badge risk-${result.risk_label.toLowerCase().replaceAll(" ", "-")}`;
setRiskLabel(result.risk_label);
document.querySelector("#explanationText").textContent = result.simple_explanation;
renderList("#redFlagsList", result.red_flags);
renderList("#nextStepsList", result.safe_next_steps);
const replyCard = document.querySelector("#replyCard");
const replyText = document.querySelector("#replyText");
const replyAllowed = ["Verify first", "Suspicious"].includes(result.risk_label);
if (replyAllowed && result.reply_draft && result.reply_draft.trim()) {
replyText.textContent = result.reply_draft;
replyCard.hidden = false;
} else {
replyCard.hidden = true;
}
elements.source.textContent = payload.source === "local_model"
? t("modelSource")
: payload.source === "cached_local_example"
? t("cachedSource")
: "";
elements.source.classList.toggle(
"cached-result",
payload.source === "cached_local_example",
);
elements.results.hidden = false;
elements.results.scrollIntoView({ behavior: "smooth", block: "start" });
}
function useImage(file) {
if (!file) return;
activeExampleId = "";
const allowed = ["image/png", "image/jpeg", "image/webp"];
if (!allowed.includes(file.type)) return showError(t("imageTypeError"));
if (file.size > 8 * 1024 * 1024) return showError(t("imageSizeError"));
const reader = new FileReader();
reader.addEventListener("load", () => {
imageDataUrl = String(reader.result);
elements.preview.src = imageDataUrl;
elements.dropZone.classList.add("has-image");
showError();
setMode("image");
});
reader.readAsDataURL(file);
}
elements.image.addEventListener("change", () => useImage(elements.image.files[0]));
elements.removeImage.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
imageDataUrl = "";
activeExampleId = "";
elements.image.value = "";
elements.preview.removeAttribute("src");
elements.dropZone.classList.remove("has-image");
setMode(null);
});
["dragenter", "dragover"].forEach((name) => elements.dropZone.addEventListener(name, (event) => {
event.preventDefault();
elements.dropZone.classList.add("dragging");
}));
["dragleave", "drop"].forEach((name) => elements.dropZone.addEventListener(name, (event) => {
event.preventDefault();
elements.dropZone.classList.remove("dragging");
}));
elements.dropZone.addEventListener("drop", (event) => useImage(event.dataTransfer.files[0]));
elements.text.addEventListener("input", () => {
activeExampleId = "";
elements.charCount.textContent = `${elements.text.value.length.toLocaleString()} / 12,000`;
if (elements.text.value.trim().length === 1) {
setMode("text");
}
if (elements.text.value.trim().length === 0 && activeMode === "text") {
setMode(null);
}
});
document.querySelectorAll(".example-card").forEach((button) => {
button.addEventListener("click", async () => {
if (button.dataset.image) {
try {
const response = await fetch(button.dataset.image);
const blob = await response.blob();
const reader = new FileReader();
reader.addEventListener("load", () => {
imageDataUrl = String(reader.result);
activeExampleId = button.dataset.exampleId || "";
elements.preview.src = imageDataUrl;
elements.dropZone.classList.add("has-image");
showError();
setMode("image");
document.querySelector(".workspace").scrollIntoView({ behavior: "smooth" });
});
reader.readAsDataURL(blob);
} catch {
showError(t("exampleImageError"));
}
} else if (button.dataset.example) {
elements.text.value = button.dataset.example;
elements.text.dispatchEvent(new Event("input"));
activeExampleId = button.dataset.exampleId || "";
elements.text.focus();
setMode("text");
document.querySelector(".workspace").scrollIntoView({ behavior: "smooth" });
}
});
});
elements.resetButton.addEventListener("click", () => {
imageDataUrl = "";
activeExampleId = "";
elements.image.value = "";
elements.preview.removeAttribute("src");
elements.dropZone.classList.remove("has-image");
elements.text.value = "";
elements.charCount.textContent = "0 / 12,000";
elements.results.hidden = true;
showError();
setMode(null);
});
elements.form.addEventListener("submit", async (event) => {
event.preventDefault();
showError();
if (!elements.text.value.trim() && !imageDataUrl) {
return showError(t("emptyInputError"));
}
if (activeMode === "image") {
elements.text.value = "";
elements.charCount.textContent = "0 / 12,000";
} else if (activeMode === "text") {
imageDataUrl = "";
elements.image.value = "";
elements.preview.removeAttribute("src");
elements.dropZone.classList.remove("has-image");
}
setLoading(true);
try {
const useCachedExample = currentLanguage === "en" && Boolean(activeExampleId);
const submittedImage = useCachedExample ? "" : imageDataUrl;
renderResult(await callGradioApi(
"analyze",
[
elements.text.value,
submittedImage,
useCachedExample ? activeExampleId : "",
elements.saveTrace.checked,
currentLanguage,
],
));
} catch (error) {
showError(error.message || t("requestFailedError"));
} finally {
setLoading(false);
}
});
document.querySelectorAll(".copy-button").forEach((button) => {
button.addEventListener("click", async () => {
const target = document.querySelector(`#${button.dataset.copy}`);
await navigator.clipboard.writeText(target.innerText);
button.textContent = t("copied");
setTimeout(() => { button.textContent = t("copy"); }, 1200);
});
});
elements.languageOptions.forEach((button) => {
button.addEventListener("click", () => applyLanguage(button.dataset.language));
});
applyLanguage(currentLanguage);
loadStatus();