HarbourSOFT's picture
Update index.html
65519f6 verified
<!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">
// ==========================
// Configuration
// ==========================
// Use a Hugging Face Hub repository ID (NOT a direct .onnx URL)
const MODEL_ID = 'onnx-community/phishing-email-detection-distilbert_v2.4.1-ONNX';
// Prefer v3 (@huggingface/transformers). Fallback to v2 (@xenova/transformers)
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';
// Human-friendly label mapping. We WILL NOT display raw LABEL_* tokens.
const labelMap = {
'LABEL_0': 'Legitimate email',
'LABEL_1': 'Phishing email',
'LABEL_2': 'Legitimate URL',
'LABEL_3': 'Phishing URL',
// In case a model uses different strings (e.g., 'SAFE', 'SCAM'), add them here as needed
};
// ==========================
// DOM
// ==========================
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');
// ==========================
// State
// ==========================
let pipe = null;
let cancelled = false;
// ==========================
// Utilities
// ==========================
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;
// Also mirror in console for convenience
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) {
// Normalize and map labels to human-friendly values
if (!raw) return 'Unknown';
return labelMap[raw] || labelMap[raw.toUpperCase()] || raw; // fallback to original if unmapped
}
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);
}
}
// Global error capture for easier debugging
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));
// ==========================
// Events
// ==========================
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; // v2 and v3 both expose pipeline
log('Transformers.js loaded.');
// Select device (auto | webgpu | wasm). Auto lets library choose the best available.
const device = 'auto';
// Prefer quantized weights where available for smaller, faster downloads.
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 = /** @type {HTMLTextAreaElement} */(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); // usually returns top-1 result
log('inference output:', out);
// Handle output shape from v2/v3 (either array or single object)
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>