pii-detection / index.html
iamleonie's picture mlabonne's picture
claude opus 4.8 (1m context)
Initial public commit
f645508
Raw History Blame Contribute Delete
12.2 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PII detection — Liquid AI</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<script>
// Private Space auth: HF signs the embedded iframe URL with ?__sign=<token>.
// Incognito / Safari block the third-party .hf.space cookie, so that token is
// the only credential — carry it on same-origin API requests, else /api/*
// returns 404 and the model-ready poll hangs.
(() => {
const sign = new URLSearchParams(location.search).get("__sign");
if (!sign) return;
const orig = self.fetch.bind(self);
self.fetch = (input, init) => {
try {
const u = new URL(typeof input === "string" ? input : input.url, location.href);
if (u.origin === location.origin && !u.searchParams.has("__sign")) {
u.searchParams.set("__sign", sign);
input = u.toString();
}
} catch (e) { /* non-URL input: pass through */ }
return orig(input, init);
};
})();
</script>
</head>
<body>
<div class="wrap">
<header class="site-intro">
<h1>Multilingual <em>PII detection</em></h1>
<p class="sub">Spot <strong>40 kinds of personal data</strong> across 16 languages and redact them, in one bidirectional pass.</p>
</header>
<section class="model-strip is-loading" id="model-strip" aria-label="Model status">
<span class="model-logo-frame" aria-hidden="true">
<img id="model-logo" class="model-logo" width="56" height="59" alt="" src="lliquid.gif">
</span>
<div class="model-copy">
<div class="model-name-row">
<a href="https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-PII-Detector" target="_blank" rel="noopener">LFM2.5 Encoder</a>
<span class="model-variant">350M parameters</span>
</div>
</div>
<div class="load-block">
<div class="load-meta">
<span id="status" role="status" aria-live="polite">Connecting to server…</span>
<span id="load-percent">0%</span>
</div>
<div class="load-track" role="progressbar" aria-label="Model loading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span id="load-progress"></span></div>
</div>
</section>
<main>
<div class="pii-workspace">
<section class="workspace-card input-panel">
<div class="panel-header">
<div><h2>Text</h2></div>
</div>
<div class="editor-stage">
<textarea id="input" spellcheck="false" placeholder="Paste text containing names, emails, IDs, cards…">Update the account for david.miller@example.com: phone +1 415 555 0199, SSN 123-45-6789, card 4111 1111 1111 1111, passport C01X00T47.</textarea>
</div>
</section>
<section class="workspace-card output-panel">
<div class="panel-header">
<div><h2>Detected PII</h2></div>
<div class="view-toggle" id="view-toggle" role="tablist" aria-label="Output view">
<button type="button" data-view="highlight" class="active" role="tab">Highlight</button>
<button type="button" data-view="redacted" role="tab">Redacted</button>
</div>
</div>
<div class="output-stage">
<div id="output" class="output-body"><div class="output-empty">Detected personal data will appear here.</div></div>
</div>
<div class="controls">
<div class="legend" id="legend"></div>
</div>
</section>
</div>
<section class="examples-panel" aria-label="Examples">
<span class="examples-label">Examples</span>
<div class="example-list" id="examples">
<button type="button" class="ex active" data-t="Update the account for david.miller@example.com: phone +1 415 555 0199, SSN 123-45-6789, card 4111 1111 1111 1111, passport C01X00T47.">Contact & ID</button>
<button type="button" class="ex" data-t="Please wire EUR 5,000 to IBAN DE89 3704 0044 0532 0130 00 (SWIFT COBADEFFXXX) from account 0532013000. My server is at 192.168.1.42.">Financial</button>
<button type="button" class="ex" data-t="Deploy creds: AWS key AKIAIOSFODNN7EXAMPLE, session token eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3In0.dozjgNryP4J3jVmNHl0w, and DB password Str0ngP@ssw0rd!.">Credentials</button>
<button type="button" class="ex" data-t="Company laptop MAC 00:1A:2B:3C:4D:5E connected from IP 10.0.0.5 near GPS 37.7749, -122.4194.">Device</button>
<button type="button" class="ex" data-t="Send 0.5 BTC to bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh or ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e. Rotate the API key sk-proj-T3BlbkFJ4aBcDeFgHiJkLmNoP.">Crypto & keys</button>
<button type="button" class="ex" data-t="Follow username jsmith on https://social.example.com/jsmith or email j.smith@example.com for details.">Online</button>
<button type="button" class="ex" data-t="Patient Sarah Connor, date of birth 03/14/1985, record number 887766, diagnosed with Type 2 diabetes and prescribed Metformin.">Healthcare</button>
<button type="button" class="ex" data-t="Ich heiße Hans Müller und wohne in der Hauptstraße 5, 80331 München. Erreichbar unter +49 30 4505 1234 oder hans.mueller@example.de.">Deutsch</button>
<button type="button" class="ex" data-t="Je m'appelle Jean Dupont, email jean.dupont@example.fr, téléphone +33 6 12 34 56 78, IBAN FR76 3000 6000 0112 3456 7890 189.">Français</button>
<button type="button" class="ex" data-t="María García, correo maria.garcia@example.es, teléfono +34 612 345 678, tarjeta 4111 1111 1111 1111.">Español</button>
<button type="button" class="ex" data-t="Иван Петров, email ivan.petrov@example.ru, телефон +7 495 123 45 67.">Русский</button>
<button type="button" class="ex" data-t="我叫张伟,电话号码是13800138000,邮箱是 zhang.wei@example.cn,住在北京市朝阳区建国路88号。">中文</button>
<button type="button" class="ex" data-t="田中太郎と申します。メールアドレスは taro.tanaka@example.jp、電話番号は 090-1234-5678 です。">日本語</button>
<button type="button" class="ex" data-t="제 이름은 김민수입니다. 이메일은 minsu.kim@example.kr, 전화번호는 010-1234-5678입니다.">한국어</button>
<button type="button" class="ex" data-t="اسمي محمد أحمد، بريدي الإلكتروني mohammed.ahmed@example.com، هاتفي +20 10 1234 5678.">العربية</button>
</div>
</section>
</main>
</div>
<script>
window.addEventListener("error", e => {
const el = document.getElementById("status");
if (el) el.textContent = "· ERROR: " + (e.message || (e.target && (e.target.src || e.target.href)) || "resource failed");
}, true);
</script>
<script type="module">
// The detector runs on the server (CPU, f32). This script drives the UI and asks
// /api/detect for char-anchored PII spans; highlighting, the redacted view, the
// legend, and stats are done here. Nothing is downloaded to the client.
const DOMAINS = ["identity","contact","financial","credential","online","device","location","healthcare","org","special","legal","other"];
const $ = (id) => document.getElementById(id);
const escapeHtml = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
function setStatus(message, tone = "loading") {
$("status").textContent = message;
const strip = $("model-strip");
strip.classList.remove("is-loading", "is-preparing", "is-ready", "is-error");
strip.classList.add(`is-${tone}`);
}
function setLoadProgress(value) {
const pct = Math.max(0, Math.min(100, Number(value) || 0));
$("load-progress").style.width = `${pct}%`;
$("load-percent").textContent = `${Math.round(pct)}%`;
document.querySelector(".load-track").setAttribute("aria-valuenow", String(Math.round(pct)));
}
let READY = false, timer = null, version = 0;
let lastText = "", lastResult = null, view = "highlight";
function renderHighlight(text, spans) {
let html = "", prev = 0, i = 0;
for (const sp of spans) {
html += escapeHtml(text.slice(prev, sp.start));
const t = escapeHtml(text.slice(sp.start, sp.end));
const tip = escapeHtml(`${sp.label} · ${sp.type}`);
html += `<span class="pii is-animated" style="--pii-color:var(--d-${sp.domain});--i:${i++}" title="${tip}">${t}</span>`;
prev = sp.end;
}
return html + escapeHtml(text.slice(prev));
}
function renderRedacted(redacted) {
return escapeHtml(redacted).replace(/\[([A-Z0-9_]+)\]/g, '<span class="redacted-tag">[$1]</span>');
}
function renderLegend(spans) {
const counts = {};
for (const sp of spans) counts[sp.domain] = (counts[sp.domain] || 0) + 1;
$("legend").innerHTML = DOMAINS.filter(d => counts[d]).map(d =>
`<span class="legend-chip" style="--pii-color:var(--d-${d})"><span class="legend-swatch" aria-hidden="true"></span>${cap(d)}<span class="legend-count">${counts[d]}</span></span>`
).join("");
}
function paint() {
const out = $("output");
if (!lastResult || !lastText.trim()) {
out.className = "output-body";
out.innerHTML = '<div class="output-empty">Detected personal data will appear here.</div>';
return;
}
out.className = view === "redacted" ? "output-body is-redacted" : "output-body";
out.innerHTML = view === "redacted" ? renderRedacted(lastResult.redacted) : renderHighlight(lastText, lastResult.spans);
}
async function detect() {
if (!READY) return;
const runVersion = ++version;
const text = $("input").value;
if (!text.trim()) { lastText = text; lastResult = null; paint(); $("legend").innerHTML = ""; return; }
document.querySelector(".output-panel").classList.add("is-detecting");
let data;
try {
const resp = await fetch("api/detect", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
data = await resp.json();
} catch (e) {
document.querySelector(".output-panel").classList.remove("is-detecting");
return;
}
if (runVersion !== version) return;
lastText = text; lastResult = data;
paint();
renderLegend(data.spans);
document.querySelector(".output-panel").classList.remove("is-detecting");
}
function detectSoon(delay = 350) { clearTimeout(timer); timer = setTimeout(() => detect(), delay); }
$("input").addEventListener("input", () => detectSoon());
$("view-toggle").addEventListener("click", (e) => {
const b = e.target.closest("button"); if (!b) return;
view = b.dataset.view;
$("view-toggle").querySelectorAll("button").forEach(x => x.classList.toggle("active", x === b));
paint();
});
$("examples").addEventListener("click", (e) => {
const b = e.target.closest(".ex"); if (!b) return;
$("examples").querySelectorAll(".ex").forEach(x => x.classList.toggle("active", x === b));
$("input").value = b.dataset.t;
detectSoon(40);
});
async function waitForServer() {
setStatus("Connecting to server…", "loading");
setLoadProgress(12);
for (let attempt = 0; attempt < 900; attempt++) {
try {
const r = await fetch("api/ready", { cache: "no-store" });
if (r.ok && (await r.json()).ready) return true;
} catch (e) { /* server still starting */ }
setStatus("Warming up model on the server…", "preparing");
setLoadProgress(Math.min(92, 12 + attempt * 4));
await new Promise((r) => setTimeout(r, 1000));
}
return false;
}
(async () => {
const ok = await waitForServer();
if (!ok) { setStatus("Model unavailable", "error"); setLoadProgress(100); $("load-percent").textContent = "Error"; return; }
READY = true;
setLoadProgress(100);
setStatus("Model ready", "ready");
await detect();
window.__PII_READY = true;
})();
</script>
</body>
</html>