privacy-filter-openai / index.html
akhaliq's picture
akhaliq HF Staff
style: refactor UI CSS variables and layout components for a modernized look
6d4b220
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenAI Privacy Filter</title>
<link href="https://fonts.googleapis.com/css2?family=Söhne,ui-sans-serif,system-ui,-apple-system,Segoe+UI,Roboto,Ubuntu,Cantarell,Noto+Sans,sans-serif&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0d0d0d;--sidebar-bg:#171717;--surface:#1e1e1e;--surface-2:#2a2a2a;
--border:rgba(255,255,255,.08);--border-hover:rgba(255,255,255,.15);
--text:#e3e3e3;--text-2:#a1a1a1;--text-3:#6e6e6e;
--green:#10a37f;--green-dim:rgba(16,163,127,.12);--green-hover:#0d8a6a;
--white:#fff;--red:#ef4146;
}
html,body{height:100%;font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
.app{display:flex;height:100vh;overflow:hidden}
/* ── Sidebar ── */
.sidebar{width:260px;background:var(--sidebar-bg);display:flex;flex-direction:column;padding:8px;flex-shrink:0}
.s-new{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:10px;border:none;background:transparent;color:var(--text);cursor:pointer;font-size:14px;width:100%;transition:.15s}
.s-new:hover{background:var(--surface-2)}
.s-new svg{opacity:.7}
.s-label{font-size:12px;font-weight:500;color:var(--text-3);padding:16px 12px 6px;text-transform:uppercase;letter-spacing:.04em}
.s-list{flex:1;overflow-y:auto}
.s-item{padding:8px 12px;border-radius:8px;font-size:13px;color:var(--text-2);cursor:pointer;transition:.15s;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.s-item:hover{background:var(--surface-2);color:var(--text)}
.s-bottom{border-top:1px solid var(--border);padding-top:8px;margin-top:8px}
.s-bottom a{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:8px;font-size:13px;color:var(--text-2);text-decoration:none;transition:.15s}
.s-bottom a:hover{background:var(--surface-2);color:var(--text)}
/* ── Main ── */
.main{flex:1;display:flex;flex-direction:column;position:relative;min-width:0}
.topbar{height:44px;display:flex;align-items:center;padding:0 16px;gap:8px}
.topbar span{font-size:15px;font-weight:600;color:var(--text)}
.badge{font-size:10px;font-weight:600;background:var(--green);color:var(--white);padding:2px 7px;border-radius:5px}
/* ── Chat ── */
.chat{flex:1;overflow-y:auto;display:flex;flex-direction:column;align-items:center}
.chat-inner{max-width:680px;width:100%;padding:16px 20px 140px}
/* Welcome */
.welcome{display:flex;flex-direction:column;align-items:center;padding:80px 20px 40px;text-align:center}
.w-logo{width:48px;height:48px;border-radius:50%;background:var(--green);display:grid;place-items:center;margin-bottom:20px}
.w-logo svg{width:22px;height:22px;fill:var(--white)}
.welcome h1{font-size:22px;font-weight:600;margin-bottom:6px;color:var(--white)}
.welcome p{font-size:14px;color:var(--text-2);max-width:440px;line-height:1.6;margin-bottom:28px}
.examples{display:grid;grid-template-columns:1fr 1fr;gap:8px;width:100%;max-width:520px}
.ex-btn{padding:12px 14px;background:var(--surface);border:1px solid var(--border);border-radius:10px;color:var(--text-2);font-size:13px;line-height:1.45;cursor:pointer;text-align:left;transition:.2s}
.ex-btn:hover{border-color:var(--border-hover);color:var(--text);background:var(--surface-2)}
/* Messages */
.msg{padding:18px 0;animation:fadeUp .25s ease}
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.msg-row{display:flex;gap:14px}
.ava{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;flex-shrink:0;font-size:12px;font-weight:600}
.ava.u{background:var(--surface-2);color:var(--text-2)}
.ava.a{background:var(--green);color:var(--white)}
.ava.a svg{width:14px;height:14px;fill:var(--white)}
.msg-c{flex:1;min-width:0}
.msg-c .role{font-size:13px;font-weight:600;margin-bottom:4px;color:var(--white)}
.msg-c .body{font-size:14px;line-height:1.7;color:var(--text)}
/* Entity tags */
.ent{padding:1px 5px;border-radius:3px;font-weight:500;font-size:13px;position:relative;cursor:default;border-bottom:2px solid}
.ent:hover::after{content:attr(data-t);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:var(--white);color:#000;font-size:10px;font-weight:600;padding:2px 7px;border-radius:4px;white-space:nowrap;z-index:9}
.ent-private_person{background:rgba(239,68,68,.12);color:#fca5a5;border-color:rgba(239,68,68,.5)}
.ent-private_email{background:rgba(99,102,241,.12);color:#a5b4fc;border-color:rgba(99,102,241,.5)}
.ent-private_phone{background:rgba(168,85,247,.12);color:#d8b4fe;border-color:rgba(168,85,247,.5)}
.ent-private_address{background:rgba(245,158,11,.12);color:#fcd34d;border-color:rgba(245,158,11,.5)}
.ent-private_date{background:rgba(20,184,166,.12);color:#5eead4;border-color:rgba(20,184,166,.5)}
.ent-private_url{background:rgba(249,115,22,.12);color:#fdba74;border-color:rgba(249,115,22,.5)}
.ent-account_number{background:rgba(236,72,153,.12);color:#f9a8d4;border-color:rgba(236,72,153,.5)}
.ent-secret{background:rgba(220,38,38,.15);color:#fca5a5;border-color:rgba(220,38,38,.6)}
/* Redacted card */
.r-card{margin-top:14px;border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--surface)}
.r-head{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;border-bottom:1px solid var(--border)}
.r-head span{font-size:11px;font-weight:600;color:var(--text-3);text-transform:uppercase;letter-spacing:.04em}
.cp-btn{padding:3px 8px;border:1px solid var(--border);border-radius:5px;background:transparent;color:var(--text-2);font-size:11px;cursor:pointer;transition:.15s}
.cp-btn:hover{background:var(--surface-2);color:var(--text)}
.r-body{padding:12px 14px;font-size:13px;line-height:1.7;color:var(--text-2);white-space:pre-wrap;word-break:break-word}
.r-body .tag{color:var(--green);font-weight:600}
/* Summary */
.pills{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}
.pill{padding:3px 9px;border:1px solid var(--border);border-radius:16px;font-size:11px;color:var(--text-2);background:var(--surface)}
.pill b{color:var(--green);margin-right:3px}
/* Dots */
.dots{display:flex;gap:4px;padding:2px 0}
.dots i{width:6px;height:6px;background:var(--text-3);border-radius:50%;animation:pulse .6s infinite alternate;font-style:normal}
.dots i:nth-child(2){animation-delay:.15s}
.dots i:nth-child(3){animation-delay:.3s}
@keyframes pulse{to{opacity:.25;transform:translateY(-3px)}}
/* ── Input bar ── */
.input-wrap{position:fixed;bottom:0;left:260px;right:0;padding:12px 20px 20px;background:linear-gradient(transparent,var(--bg) 40%)}
.input-inner{max-width:680px;margin:0 auto}
.input-box{display:flex;align-items:flex-end;gap:8px;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:6px 6px 6px 18px;transition:.2s}
.input-box:focus-within{border-color:rgba(255,255,255,.2)}
.input-box textarea{flex:1;background:none;border:none;outline:none;color:var(--text);font-size:14px;font-family:inherit;resize:none;max-height:140px;min-height:22px;line-height:1.5;padding:6px 0}
.input-box textarea::placeholder{color:var(--text-3)}
.send{width:32px;height:32px;border-radius:50%;border:none;cursor:pointer;display:grid;place-items:center;transition:.15s;flex-shrink:0;background:var(--white)}
.send:hover{opacity:.85;transform:scale(1.04)}
.send:disabled{background:var(--surface-2);cursor:default;transform:none;opacity:.5}
.send svg{width:14px;height:14px;fill:#000}
.foot{text-align:center;font-size:11px;color:var(--text-3);padding-top:6px}
::-webkit-scrollbar{width:5px}
::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:3px}
@media(max-width:768px){.sidebar{display:none}.input-wrap{left:0}.examples{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="app">
<div class="sidebar">
<button class="s-new" onclick="resetChat()">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
New analysis
</button>
<div class="s-label">Recent</div>
<div class="s-list" id="hist"></div>
<div class="s-bottom">
<a href="https://huggingface.co/openai/privacy-filter" target="_blank">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
Model Card
</a>
</div>
</div>
<div class="main">
<div class="topbar">
<span>Privacy Filter</span>
<span class="badge">openai</span>
</div>
<div class="chat" id="chat">
<div class="chat-inner" id="chatInner">
<div class="welcome" id="welcome">
<div class="w-logo"><svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z"/></svg></div>
<h1>Privacy Filter</h1>
<p>Detect and redact PII using OpenAI's privacy-filter model. Supports names, emails, phones, addresses, dates, URLs, account numbers, and secrets.</p>
<div class="examples">
<button class="ex-btn" onclick="useEx(this)">Alice was born on 1990-01-02 and lives at 1 Main St.</button>
<button class="ex-btn" onclick="useEx(this)">Email me at alice@example.com or call 415-555-0101.</button>
<button class="ex-btn" onclick="useEx(this)">Me llamo Laura Gómez y vivo en Calle de Alcalá 21, Madrid.</button>
<button class="ex-btn" onclick="useEx(this)">Mon e-mail est jean.dupont@example.fr, tél +33 6 12 34 56 78.</button>
</div>
</div>
<div id="msgs"></div>
</div>
</div>
<div class="input-wrap">
<div class="input-inner">
<div class="input-box">
<textarea id="inp" rows="1" placeholder="Paste text to scan for PII…" oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,140)+'px'"></textarea>
<button class="send" id="sendBtn" onclick="send()"><svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg></button>
</div>
<div class="foot">8 PII categories · BIOES + Viterbi decoding</div>
</div>
</div>
</div>
</div>
<script type="module">
import{Client}from"https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
let C=null,busy=false;
(async()=>{try{C=await Client.connect(location.origin)}catch(e){console.error(e)}})();
const $=id=>document.getElementById(id);
const esc=s=>{const d=document.createElement('div');d.textContent=s;return d.innerHTML};
function addMsg(role,html){
$('welcome').style.display='none';
const d=document.createElement('div');d.className='msg';
const isA=role==='a';
const av=isA?'<svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z"/></svg>':'Y';
d.innerHTML=`<div class="msg-row"><div class="ava ${isA?'a':'u'}">${av}</div><div class="msg-c"><div class="role">${isA?'Privacy Filter':'You'}</div><div class="body">${html}</div></div></div>`;
$('msgs').appendChild(d);
$('chat').scrollTop=$('chat').scrollHeight;
return d;
}
function highlight(text,ents){
if(!ents||!ents.length)return esc(text);
const s=[...ents].sort((a,b)=>a.start-b.start);
let h='',c=0;
for(const e of s){if(e.start<c)continue;h+=esc(text.slice(c,e.start));h+=`<span class="ent ent-${e.entity}" data-t="${e.entity}">${esc(text.slice(e.start,e.end))}</span>`;c=e.end;}
return h+esc(text.slice(c));
}
function fmtRedact(r){return esc(r).replace(/\[(PERSON|EMAIL|PHONE|ADDRESS|DATE|URL|ACCOUNT_NUMBER|SECRET|REDACTED)\]/g,'<span class="tag">[$1]</span>')}
function result(d){
const{text,entities,redacted,summary}=d;
let h='<div style="margin-bottom:2px"><strong>Detected Entities</strong></div>';
h+='<div style="line-height:2">'+highlight(text,entities)+'</div>';
if(entities&&entities.length){
h+='<div class="pills">';
for(const[k,v]of Object.entries(summary||{}))h+=`<div class="pill"><b>${v}</b>${k.replace('private_','')}</div>`;
h+='</div>';
const rid='r'+Date.now();
h+=`<div class="r-card"><div class="r-head"><span>Redacted Output</span><button class="cp-btn" onclick="navigator.clipboard.writeText($('${rid}').innerText).then(()=>{this.textContent='Copied!';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button></div><div class="r-body" id="${rid}">${fmtRedact(redacted)}</div></div>`;
}else{h+='<div style="margin-top:10px;color:var(--green);font-size:13px">✓ No personal identifiers detected.</div>';}
return h;
}
window.send=async function(){
const inp=$('inp'),text=inp.value.trim();
if(!text||busy)return;
busy=true;inp.value='';inp.style.height='auto';$('sendBtn').disabled=true;
addMsg('u',esc(text));
const ld=addMsg('a','<div class="dots"><i></i><i></i><i></i></div>');
// history
const hi=document.createElement('div');hi.className='s-item';hi.textContent=text.slice(0,36)+(text.length>36?'…':'');$('hist').prepend(hi);
try{
if(!C)C=await Client.connect(location.origin);
const r=await C.predict("/predict_and_redact",{text});
ld.remove();addMsg('a',result(r.data[0]));
}catch(e){ld.remove();addMsg('a',`<span style="color:var(--red)">Error: ${esc(e.message||'Request failed')}</span>`);}
busy=false;$('sendBtn').disabled=false;inp.focus();
};
window.useEx=function(b){$('inp').value=b.textContent;send()};
window.resetChat=function(){$('msgs').innerHTML='';$('welcome').style.display='';$('inp').value='';};
$('inp').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}});
</script>
</body>
</html>