Spaces:
Running on Zero
Running on Zero
| <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> | |