Abid Ali Awan Codex commited on
Commit
3fae071
·
1 Parent(s): 0f5c289

Add English and Urdu interface

Browse files

Add 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>

Files changed (4) hide show
  1. app.py +25 -4
  2. static/app.js +244 -21
  3. static/index.html +49 -43
  4. 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 simple, calm English.
 
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.\n\n"
 
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 = call_model(text, image_data_url, telemetry)
 
 
 
 
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(text, image_data_url, example_id, save_trace)
 
 
 
 
 
 
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("The app could not start the request.");
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("The app could not read the result.");
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("The request could not be completed.");
49
  if (event === "complete" && raw) {
50
  const values = JSON.parse(raw);
51
  return values[0];
52
  }
53
  }
54
  }
55
- throw new Error("The app returned no result.");
56
  }
57
 
58
  function setStatus(status) {
59
- if (!status) return;
60
- elements.status.lastChild.textContent = status.label || "Modal model unavailable";
 
 
 
 
 
 
 
 
 
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 ? "Checking safely..." : "Check this notice";
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(payload.error || "Unable to analyze this input.");
109
  const result = payload.assessment;
110
  setStatus(payload.status);
111
  elements.risk.className = `risk-badge risk-${result.risk_label.toLowerCase().replaceAll(" ", "-")}`;
112
- elements.risk.textContent = result.risk_label;
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
- ? "Analyzed by the deployed Qwen3.5 4B model endpoint."
129
  : payload.source === "cached_modal_example"
130
- ? "Cached model result"
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("Use a PNG, JPG, or WebP image.");
145
- if (file.size > 8 * 1024 * 1024) return showError("Please choose an image smaller than 8 MB.");
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("Could not load the example image.");
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("Paste a message or upload a screenshot to continue.");
237
  }
238
 
239
  if (activeMode === "image") {
@@ -248,13 +460,20 @@ elements.form.addEventListener("submit", async (event) => {
248
 
249
  setLoading(true);
250
  try {
251
- const submittedImage = activeExampleId ? "" : imageDataUrl;
 
252
  renderResult(await callGradioApi(
253
  "analyze",
254
- [elements.text.value, submittedImage, activeExampleId, elements.saveTrace.checked],
 
 
 
 
 
 
255
  ));
256
  } catch (error) {
257
- showError(error.message || "The request could not be completed.");
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
- const original = button.textContent;
268
- button.textContent = "Copied";
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
- <a class="brand" href="/" aria-label="Pakistan Notice Helper home">
16
- <span class="brand-mark"><img src="/static/logo.png" alt=""></span>
17
- <span class="brand-name">Pakistan Notice Helper</span>
18
- </a>
 
 
 
 
 
 
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
- .brand-name { font-size: 12px; white-space: nowrap; }
 
 
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; }