| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> |
| <title>Phishing Email Detection (Transformers.js + ONNX)</title> |
| <style> |
| :root{ |
| --bg: #0b1021; |
| --panel: #0f1531; |
| --panel-2: #0c122b; |
| --text: #e9eeff; |
| --muted: #9fb3ff; |
| --accent: #7aa2ff; |
| --success: #2ecc71; |
| --warn: #f39c12; |
| --danger: #e74c3c; |
| --border: 1px solid rgba(255,255,255,.08); |
| --radius: 14px; |
| --shadow: 0 10px 30px rgba(0,0,0,.35); |
| } |
| *{box-sizing:border-box} |
| html,body{height:100%} |
| body{ |
| margin:0; background: radial-gradient(1200px 800px at 10% -10%, #1a2252 0%, transparent 60%), |
| radial-gradient(1200px 800px at 110% 10%, #1b2d61 0%, transparent 60%), |
| var(--bg); |
| color:var(--text); font: 16px/1.6 system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial, sans-serif; |
| padding: 24px; display:flex; flex-direction:column; gap:24px; |
| } |
| header{ |
| display:flex; justify-content:space-between; align-items:center; gap:16px; |
| padding: 18px 20px; border-radius: var(--radius); backdrop-filter: blur(6px); |
| background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02)); |
| border: var(--border); box-shadow: var(--shadow); |
| } |
| h1{font-size: clamp(22px, 3.2vw, 28px); margin:0; letter-spacing:.2px} |
| .subtitle{color:var(--muted); font-size:14px} |
| |
| .grid{display:grid; grid-template-columns:1.1fr .9fr; gap:22px} |
| @media (max-width: 960px){ .grid{grid-template-columns:1fr; } } |
| |
| .card{background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02)); |
| border:var(--border); border-radius:var(--radius); box-shadow:var(--shadow); padding:18px} |
| |
| textarea{width:100%; height:220px; resize:vertical; background:var(--panel); |
| color:var(--text); border-radius:12px; border:var(--border); padding:12px 14px; outline:none} |
| |
| .row{display:flex; align-items:center; gap:10px; flex-wrap:wrap} |
| |
| button{appearance:none; border:0; border-radius:12px; padding:10px 14px; cursor:pointer; font-weight:600; |
| background: linear-gradient(180deg, #7aa2ff, #4e77ff); color:white; box-shadow: 0 6px 20px rgba(78,119,255,.35)} |
| button.secondary{background: linear-gradient(180deg, #7780a6, #5a6284); box-shadow:none} |
| button.ghost{background:transparent; border:var(--border); color:var(--text)} |
| button:disabled{opacity:.55; cursor:not-allowed} |
| |
| .muted{color:var(--muted); font-size:12px} |
| |
| .progress-wrap{display:flex; align-items:center; gap:10px} |
| progress{width:280px; height:12px; accent-color:#7aa2ff} |
| |
| #result{margin-top:12px; padding:14px; background:var(--panel-2); border:var(--border); border-radius:12px; white-space:pre-wrap} |
| |
| .badge{display:inline-flex; align-items:center; gap:6px; font-size:12px; border-radius:999px; padding:6px 10px; |
| background:#1a234a; border:var(--border)} |
| .badge.success{background: rgba(46, 204, 113, .12); border: 1px solid rgba(46, 204, 113, .35); color:#b9f6d0} |
| .badge.warn{background: rgba(243, 156, 18, .12); border: 1px solid rgba(243, 156, 18, .35); color:#ffe2b9} |
| .badge.danger{background: rgba(231, 76, 60, .12); border: 1px solid rgba(231, 76, 60, .35); color:#ffbdb4} |
| |
| #log{background:#060a1d; color:#b8d0ff; padding:12px; border-radius:12px; height:200px; overflow:auto; font-size:12px; border:var(--border)} |
| |
| .footer-note{opacity:.8; font-size:12px} |
| .spacer{flex:1} |
| </style> |
| </head> |
| <body> |
| <header> |
| <div> |
| <h1>Phishing Email Detection</h1> |
| <div class="subtitle">Transformers.js + ONNX Runtime (browser)</div> |
| </div> |
| <div class="row"> |
| <button id="load-model-btn">Load model</button> |
| <button id="cancel-load-btn" class="secondary" style="display:none;">Cancel (UI reset)</button> |
| <button id="clear-log-btn" class="ghost">Clear log</button> |
| </div> |
| </header> |
|
|
| <div class="grid"> |
| <section class="card"> |
| <p>Would you like to download and initialize the phishing-detection model now?</p> |
| <div id="loading" style="display:none; margin-bottom:8px;"> |
| <div class="progress-wrap"> |
| <progress id="progress" value="0" max="100"></progress> |
| <span id="progress-label" class="muted">0%</span> |
| </div> |
| <div class="muted">First run will download model artifacts and tokenizer files. Duration depends on network and browser cache.</div> |
| </div> |
|
|
| <div id="email-input-section" style="display:none;"> |
| <textarea id="email-input" placeholder="Paste email text here…"></textarea> |
| <div class="row"> |
| <button id="detect-btn">Run Detection</button> |
| <button id="copy-btn" class="ghost">Copy Result</button> |
| <span id="status-chip" class="badge" style="display:none;"></span> |
| </div> |
| <div id="result" aria-live="polite"></div> |
| </div> |
| </section> |
|
|
| <aside class="card"> |
| <h3 style="margin-top:6px">Debug Log</h3> |
| <div id="log" aria-live="polite"></div> |
| <div class="row" style="margin-top:10px"> |
| <span class="footer-note">Tip: open DevTools for network details.</span> |
| <span class="spacer"></span> |
| <button id="save-log-btn" class="ghost">Save Log</button> |
| </div> |
| </aside> |
| </div> |
|
|
| <script type="module"> |
| |
| |
| |
| |
| const MODEL_ID = 'onnx-community/phishing-email-detection-distilbert_v2.4.1-ONNX'; |
| |
| const CDN_PRIMARY = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0'; |
| const CDN_FALLBACK = 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2'; |
| |
| |
| const labelMap = { |
| 'LABEL_0': 'Legitimate email', |
| 'LABEL_1': 'Phishing email', |
| 'LABEL_2': 'Legitimate URL', |
| 'LABEL_3': 'Phishing URL', |
| |
| }; |
| |
| |
| |
| |
| const $ = (sel) => document.querySelector(sel); |
| const loadBtn = $('#load-model-btn'); |
| const cancelBtn = $('#cancel-load-btn'); |
| const clearBtn = $('#clear-log-btn'); |
| const emailSec = $('#email-input-section'); |
| const detectBtn = $('#detect-btn'); |
| const copyBtn = $('#copy-btn'); |
| const resultDiv = $('#result'); |
| const loadingUI = $('#loading'); |
| const pbar = $('#progress'); |
| const plabel = $('#progress-label'); |
| const logDiv = $('#log'); |
| const saveLog = $('#save-log-btn'); |
| const statusChip= $('#status-chip'); |
| |
| |
| |
| |
| let pipe = null; |
| let cancelled = false; |
| |
| |
| |
| |
| function log(...args) { |
| const line = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a, null, 2))).join(' '); |
| logDiv.textContent += line + '\n'; |
| logDiv.scrollTop = logDiv.scrollHeight; |
| |
| console.log('[LOG]', ...args); |
| } |
| |
| function setProgress(evt) { |
| if (evt?.status === 'progress') { |
| const pct = Math.max(0, Math.min(100, Math.round(evt.progress || 0))); |
| pbar.value = pct; |
| plabel.textContent = `${pct}% ${evt?.name || evt?.file || ''}`.trim(); |
| } else if (evt?.status) { |
| log(`status: ${evt.status} ${evt?.name || evt?.file || ''}`.trim()); |
| } |
| } |
| |
| function showError(err) { |
| const msg = err?.stack || err?.message || String(err); |
| log('❌ ERROR:', msg); |
| } |
| |
| function humanLabel(raw) { |
| |
| if (!raw) return 'Unknown'; |
| return labelMap[raw] || labelMap[raw.toUpperCase()] || raw; |
| } |
| |
| function setChip(kind, text) { |
| statusChip.className = 'badge'; |
| if (kind) statusChip.classList.add(kind); |
| statusChip.textContent = text; |
| statusChip.style.display = 'inline-flex'; |
| } |
| |
| function clearChip(){ statusChip.style.display = 'none'; } |
| |
| async function importTransformers() { |
| try { |
| log('Trying to import:', CDN_PRIMARY); |
| return await import(CDN_PRIMARY); |
| } catch (e1) { |
| showError(e1); |
| log('Primary import failed. Falling back to:', CDN_FALLBACK); |
| return await import(CDN_FALLBACK); |
| } |
| } |
| |
| |
| window.addEventListener('error', e => log('window.error:', e.message, e.filename, `${e.lineno}:${e.colno}`)); |
| window.addEventListener('unhandledrejection', e => log('unhandledrejection:', e.reason?.message || e.reason)); |
| |
| |
| |
| |
| loadBtn.addEventListener('click', async () => { |
| try { |
| cancelled = false; |
| loadBtn.style.display = 'none'; |
| cancelBtn.style.display = 'inline-block'; |
| loadingUI.style.display = 'block'; |
| pbar.value = 0; plabel.textContent = '0%'; |
| |
| const mod = await importTransformers(); |
| const { pipeline } = mod; |
| log('Transformers.js loaded.'); |
| |
| |
| const device = 'auto'; |
| |
| |
| const options = { |
| dtype: 'q4', |
| device, |
| progress_callback: setProgress, |
| }; |
| |
| log('Loading model from repo id:', MODEL_ID); |
| pipe = await pipeline('text-classification', MODEL_ID, options); |
| |
| if (cancelled) { |
| log('Note: Cancel only resets the UI; it cannot interrupt an in-flight pipeline load.'); |
| } |
| |
| loadingUI.style.display = 'none'; |
| cancelBtn.style.display = 'none'; |
| emailSec.style.display = 'block'; |
| log('✅ Model ready.'); |
| } catch (err) { |
| loadingUI.style.display = 'none'; |
| cancelBtn.style.display = 'none'; |
| loadBtn.style.display = 'inline-block'; |
| showError(err); |
| |
| if ((err?.message || '').includes('404')) { |
| log('Repository not found / typo / or private with no access. Current:', MODEL_ID); |
| } |
| log('Reminder: Transformers.js pipeline expects a Hub repo with ONNX weights under an onnx/ directory plus required configs.'); |
| } |
| }); |
| |
| cancelBtn.addEventListener('click', () => { |
| cancelled = true; |
| loadBtn.style.display = 'inline-block'; |
| cancelBtn.style.display = 'none'; |
| loadingUI.style.display = 'none'; |
| log('Canceled (UI only). If downloads are in-flight, they may finish or fail later.'); |
| }); |
| |
| clearBtn.addEventListener('click', () => { logDiv.textContent = ''; }); |
| |
| detectBtn.addEventListener('click', async () => { |
| try { |
| clearChip(); |
| const emailText = (document.getElementById('email-input')).value.trim(); |
| if (!emailText) { |
| resultDiv.textContent = 'Please paste some email text to classify.'; |
| return; |
| } |
| if (!pipe) { |
| resultDiv.textContent = 'Model is not loaded yet.'; |
| return; |
| } |
| resultDiv.textContent = 'Running inference…'; |
| const out = await pipe(emailText); |
| log('inference output:', out); |
| |
| |
| const first = Array.isArray(out) ? out[0] : out; |
| const niceLabel = humanLabel(first.label); |
| const score = typeof first.score === 'number' ? first.score : Number(first.score ?? NaN); |
| const confidence = Number.isFinite(score) ? (score * 100).toFixed(2) + '%' : String(first.score); |
| |
| let riskKind = 'success'; |
| if (/phishing/i.test(niceLabel)) riskKind = 'danger'; |
| else if (/url/i.test(niceLabel)) riskKind = 'warn'; |
| setChip(riskKind, niceLabel); |
| |
| resultDiv.textContent = `Prediction: ${niceLabel}\nConfidence: ${confidence}`; |
| } catch (err) { |
| showError(err); |
| resultDiv.textContent = 'Detection failed. See Debug Log for details.'; |
| } |
| }); |
| |
| copyBtn.addEventListener('click', async () => { |
| const text = resultDiv.textContent.trim(); |
| if (!text) return; |
| try { await navigator.clipboard.writeText(text); setChip('success', 'Copied'); setTimeout(clearChip, 1200); } |
| catch { setChip('warn', 'Copy failed'); setTimeout(clearChip, 1400); } |
| }); |
| |
| saveLog.addEventListener('click', () => { |
| const blob = new Blob([logDiv.textContent], { type: 'text/plain' }); |
| const url = URL.createObjectURL(blob); |
| const a = Object.assign(document.createElement('a'), { href: url, download: 'phishing-detector-log.txt' }); |
| document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); |
| }); |
| </script> |
| </body> |
| </html> |
|
|