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