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=Inter:wght@300;400;500;600&family=Outfit:wght@500;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| /* Modern CSS Reset & Variables */ | |
| :root { | |
| /* Palette */ | |
| --bg-color: #030712; | |
| --surface: rgba(17, 24, 39, 0.65); | |
| --surface-hover: rgba(31, 41, 55, 0.75); | |
| --border: rgba(255, 255, 255, 0.08); | |
| --text-primary: #f9fafb; | |
| --text-secondary: #9ca3af; | |
| --accent: #6366f1; | |
| --accent-glow: rgba(99, 102, 241, 0.5); | |
| /* Entity Colors */ | |
| --entity-account_number: #ef4444; /* Red */ | |
| --entity-private_address: #f59e0b; /* Amber */ | |
| --entity-private_email: #10b981; /* Emerald */ | |
| --entity-private_person: #8b5cf6; /* Violet */ | |
| --entity-private_phone: #ec4899; /* Pink */ | |
| --entity-private_url: #06b6d4; /* Cyan */ | |
| --entity-private_date: #eab308; /* Yellow */ | |
| --entity-secret: #f43f5e; /* Rose */ | |
| } | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| background-color: var(--bg-color); | |
| color: var(--text-primary); | |
| font-family: 'Inter', sans-serif; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| overflow-x: hidden; | |
| } | |
| /* Ambient Background Orbs */ | |
| .ambient-orb { | |
| position: absolute; | |
| border-radius: 50%; | |
| filter: blur(120px); | |
| z-index: -1; | |
| animation: float 20s infinite ease-in-out alternate; | |
| } | |
| .orb-1 { width: 400px; height: 400px; background: rgba(99, 102, 241, 0.15); top: -100px; left: -100px; } | |
| .orb-2 { width: 500px; height: 500px; background: rgba(139, 92, 246, 0.12); bottom: -150px; right: -100px; animation-delay: -5s; } | |
| .orb-3 { width: 300px; height: 300px; background: rgba(16, 185, 129, 0.1); top: 40%; left: 50%; transform: translate(-50%, -50%); animation-delay: -10s; } | |
| @keyframes float { | |
| 0% { transform: translate(0, 0) scale(1); } | |
| 100% { transform: translate(30px, 50px) scale(1.1); } | |
| } | |
| .container { | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| padding: 4rem 2rem; | |
| width: 100%; | |
| box-sizing: border-box; | |
| z-index: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2.5rem; | |
| } | |
| header { | |
| text-align: center; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 1.25rem; | |
| animation: fadeDown 0.8s ease-out; | |
| } | |
| .badge-container { | |
| display: flex; | |
| gap: 0.75rem; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .badge { | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| padding: 0.35rem 0.85rem; | |
| border-radius: 100px; | |
| font-size: 0.8rem; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| backdrop-filter: blur(10px); | |
| } | |
| .badge svg { width: 14px; height: 14px; color: var(--accent); } | |
| h1 { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: 3.5rem; | |
| font-weight: 800; | |
| margin: 0; | |
| line-height: 1.1; | |
| letter-spacing: -0.02em; | |
| background: linear-gradient(135deg, #fff 0%, #a5b4fc 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .subtitle { | |
| color: var(--text-secondary); | |
| font-size: 1.15rem; | |
| max-width: 680px; | |
| line-height: 1.6; | |
| margin: 0; | |
| } | |
| .glass-panel { | |
| background: var(--surface); | |
| backdrop-filter: blur(24px); | |
| -webkit-backdrop-filter: blur(24px); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| padding: 2.5rem; | |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); | |
| transition: transform 0.3s ease, border-color 0.3s ease; | |
| } | |
| .input-wrapper { | |
| position: relative; | |
| margin-bottom: 1.5rem; | |
| } | |
| textarea { | |
| width: 100%; | |
| min-height: 180px; | |
| background: rgba(0, 0, 0, 0.2); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| color: var(--text-primary); | |
| font-family: 'Inter', sans-serif; | |
| font-size: 1.05rem; | |
| line-height: 1.6; | |
| padding: 1.25rem; | |
| resize: vertical; | |
| box-sizing: border-box; | |
| transition: all 0.3s ease; | |
| } | |
| textarea:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| background: rgba(0, 0, 0, 0.3); | |
| box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); | |
| } | |
| textarea::placeholder { color: #4b5563; } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--accent) 0%, #4f46e5 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 10px; | |
| padding: 0.875rem 2rem; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| transition: all 0.2s ease; | |
| box-shadow: 0 4px 14px 0 var(--accent-glow); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px 0 var(--accent-glow); | |
| filter: brightness(1.1); | |
| } | |
| .btn-primary:active { transform: translateY(0); } | |
| .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; } | |
| /* Spinner */ | |
| .spinner { | |
| width: 18px; | |
| height: 18px; | |
| border: 2px solid rgba(255,255,255,0.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 0.8s linear infinite; | |
| display: none; | |
| } | |
| @keyframes spin { 100% { transform: rotate(360deg); } } | |
| @keyframes fadeDown { 0% { opacity: 0; transform: translateY(-20px); } 100% { opacity: 1; transform: translateY(0); } } | |
| @keyframes fadeUp { 0% { opacity: 0; transform: translateY(20px); } 100% { opacity: 1; transform: translateY(0); } } | |
| /* Results Area */ | |
| .results-container { | |
| display: none; | |
| animation: fadeUp 0.6s ease-out forwards; | |
| gap: 1.5rem; | |
| flex-direction: column; | |
| } | |
| .results-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid var(--border); | |
| padding-bottom: 1rem; | |
| } | |
| .results-header h2 { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: 1.5rem; | |
| margin: 0; | |
| font-weight: 600; | |
| } | |
| .summary-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .stat-card { | |
| background: rgba(255, 255, 255, 0.02); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| transition: transform 0.2s; | |
| } | |
| .stat-card:hover { background: rgba(255, 255, 255, 0.04); transform: translateY(-2px); } | |
| .stat-icon { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.2rem; | |
| } | |
| .stat-info { display: flex; flex-direction: column; } | |
| .stat-value { font-size: 1.25rem; font-weight: 700; font-family: 'Outfit', sans-serif; } | |
| .stat-label { font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.2rem; } | |
| .output-text { | |
| background: rgba(0, 0, 0, 0.3); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| font-size: 1.05rem; | |
| line-height: 2.2; /* Increased line-height to accommodate badges */ | |
| white-space: pre-wrap; | |
| min-height: 100px; | |
| } | |
| /* Redaction Styling */ | |
| .redacted { | |
| position: relative; | |
| display: inline-grid; | |
| align-items: center; | |
| justify-items: center; | |
| background-color: var(--bg); | |
| border-radius: 6px; | |
| margin: 0 0.2rem; | |
| padding: 0.1rem 0.5rem; | |
| cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>'), pointer; | |
| overflow: hidden; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| box-shadow: inset 0 2px 4px rgba(0,0,0,0.3); | |
| vertical-align: middle; | |
| } | |
| .redacted::before { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| background: repeating-linear-gradient( | |
| -45deg, | |
| rgba(0,0,0,0.1) 0, | |
| rgba(0,0,0,0.1) 4px, | |
| rgba(0,0,0,0.2) 4px, | |
| rgba(0,0,0,0.2) 8px | |
| ); | |
| z-index: 1; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .redacted-text { | |
| grid-area: 1 / 1; | |
| opacity: 0; | |
| filter: blur(4px); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| color: #fff; | |
| font-weight: 500; | |
| } | |
| .redacted-label { | |
| grid-area: 1 / 1; | |
| font-size: 0.65rem; | |
| font-family: 'Outfit', sans-serif; | |
| font-weight: 700; | |
| color: #fff; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| z-index: 2; | |
| text-shadow: 0 1px 3px rgba(0,0,0,0.6); | |
| } | |
| .redacted:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| box-shadow: inset 0 0 0 rgba(0,0,0,0); | |
| } | |
| .redacted:hover .redacted-text { | |
| opacity: 1; | |
| filter: blur(0); | |
| } | |
| .redacted:hover .redacted-label { | |
| opacity: 0; | |
| transform: scale(0.9); | |
| } | |
| .redacted:hover::before { | |
| opacity: 0; | |
| } | |
| /* Footer Legend */ | |
| .legend { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| justify-content: center; | |
| margin-top: 1rem; | |
| padding: 1.5rem; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| } | |
| .legend-color { width: 10px; height: 10px; border-radius: 50%; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="ambient-orb orb-1"></div> | |
| <div class="ambient-orb orb-2"></div> | |
| <div class="ambient-orb orb-3"></div> | |
| <div class="container"> | |
| <header> | |
| <div class="badge-container"> | |
| <div class="badge"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 7h-3a2 2 0 0 1-2-2V2"/><path d="M9 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7l4 4v10a2 2 0 0 1-2 2Z"/><path d="M3 15h6"/><path d="M3 18h6"/></svg> | |
| 1.5B Parameters | |
| </div> | |
| <div class="badge"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M12 6v6l4 2"/></svg> | |
| 128k Token Context | |
| </div> | |
| <div class="badge"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> | |
| Apache 2.0 License | |
| </div> | |
| </div> | |
| <h1>OpenAI Privacy Filter</h1> | |
| <p class="subtitle">A state-of-the-art bidirectional token-classification model for highly accurate PII detection and text sanitization. Built for high-throughput privacy workflows.</p> | |
| </header> | |
| <div class="glass-panel"> | |
| <div class="input-wrapper"> | |
| <textarea id="inputText" placeholder="Enter text to detect and mask sensitive information... For example: My name is Alice Smith, I live at 123 Main St, and my email is alice@example.com. Call me at 555-0198."></textarea> | |
| </div> | |
| <div style="display: flex; justify-content: flex-end;"> | |
| <button id="analyzeBtn" class="btn-primary"> | |
| <svg class="btn-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg> | |
| <span class="btn-text">Analyze Text</span> | |
| <div class="spinner" id="spinner"></div> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="glass-panel results-container" id="resultsPanel"> | |
| <div class="results-header"> | |
| <h2>Analysis Results</h2> | |
| </div> | |
| <div class="summary-grid" id="summaryGrid"> | |
| <!-- Summary cards injected here --> | |
| </div> | |
| <div class="output-text" id="outputText"></div> | |
| </div> | |
| <div class="legend" id="legend"></div> | |
| </div> | |
| <!-- Gradio Client Library --> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| const entityColors = { | |
| 'account_number': 'var(--entity-account_number)', | |
| 'private_address': 'var(--entity-private_address)', | |
| 'private_email': 'var(--entity-private_email)', | |
| 'private_person': 'var(--entity-private_person)', | |
| 'private_phone': 'var(--entity-private_phone)', | |
| 'private_url': 'var(--entity-private_url)', | |
| 'private_date': 'var(--entity-private_date)', | |
| 'secret': 'var(--entity-secret)' | |
| }; | |
| const icons = { | |
| 'account_number': '💳', | |
| 'private_address': '📍', | |
| 'private_email': '✉️', | |
| 'private_person': '👤', | |
| 'private_phone': '📱', | |
| 'private_url': '🔗', | |
| 'private_date': '📅', | |
| 'secret': '🔑' | |
| }; | |
| // Initialize Legend | |
| const legendContainer = document.getElementById('legend'); | |
| for (const [entity, color] of Object.entries(entityColors)) { | |
| const item = document.createElement('div'); | |
| item.className = 'legend-item'; | |
| const label = entity.replace('private_', '').replace('_', ' '); | |
| item.innerHTML = `<div class="legend-color" style="background-color: ${color}"></div><span style="text-transform: capitalize">${label}</span>`; | |
| legendContainer.appendChild(item); | |
| } | |
| const inputText = document.getElementById('inputText'); | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| const spinner = document.getElementById('spinner'); | |
| const btnText = document.querySelector('.btn-text'); | |
| const btnIcon = document.querySelector('.btn-icon'); | |
| const resultsPanel = document.getElementById('resultsPanel'); | |
| const outputText = document.getElementById('outputText'); | |
| const summaryGrid = document.getElementById('summaryGrid'); | |
| let client = null; | |
| async function initClient() { | |
| try { | |
| client = await Client.connect(window.location.origin); | |
| } catch (error) { | |
| console.error("Gradio connection error:", error); | |
| } | |
| } | |
| initClient(); | |
| analyzeBtn.addEventListener('click', async () => { | |
| const text = inputText.value.trim(); | |
| if (!text) return; | |
| if (!client) { | |
| await initClient(); | |
| if (!client) { | |
| alert("Unable to connect to the analysis server."); | |
| return; | |
| } | |
| } | |
| analyzeBtn.disabled = true; | |
| btnText.textContent = 'Processing...'; | |
| btnIcon.style.display = 'none'; | |
| spinner.style.display = 'block'; | |
| resultsPanel.style.display = 'none'; | |
| try { | |
| const result = await client.predict("/predict", { text }); | |
| const entities = result.data[0]; | |
| renderResults(text, entities); | |
| resultsPanel.style.display = 'flex'; | |
| resultsPanel.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } catch (error) { | |
| console.error("Prediction failed:", error); | |
| alert("An error occurred during text analysis."); | |
| } finally { | |
| analyzeBtn.disabled = false; | |
| btnText.textContent = 'Analyze Text'; | |
| btnIcon.style.display = 'block'; | |
| spinner.style.display = 'none'; | |
| } | |
| }); | |
| function renderResults(text, entities) { | |
| if (!entities || entities.length === 0) { | |
| outputText.textContent = text; | |
| summaryGrid.innerHTML = ` | |
| <div class="stat-card"> | |
| <div class="stat-icon" style="background: rgba(255,255,255,0.05); color: #fff;">✅</div> | |
| <div class="stat-info"> | |
| <span class="stat-value">0</span> | |
| <span class="stat-label">Entities Found</span> | |
| </div> | |
| </div>`; | |
| return; | |
| } | |
| // Group counts | |
| const counts = {}; | |
| entities.forEach(ent => { | |
| let type = ent.entity || ent.entity_group; | |
| if (/^[BIES]-/.test(type)) type = type.substring(2); | |
| counts[type] = (counts[type] || 0) + 1; | |
| }); | |
| summaryGrid.innerHTML = Object.entries(counts).map(([type, count]) => { | |
| const color = entityColors[type] || '#888'; | |
| const label = type.replace('private_', '').replace('_', ' '); | |
| const icon = icons[type] || '📌'; | |
| return ` | |
| <div class="stat-card" style="border-left: 3px solid ${color}"> | |
| <div class="stat-icon" style="background: ${color}20">${icon}</div> | |
| <div class="stat-info"> | |
| <span class="stat-value" style="color: ${color}">${count}</span> | |
| <span class="stat-label">${label}</span> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| // Highlight text | |
| let lastIdx = 0; | |
| let html = ''; | |
| entities.sort((a, b) => (a.start || 0) - (b.start || 0)); | |
| for (const ent of entities) { | |
| if (ent.start !== undefined && ent.end !== undefined) { | |
| if (ent.start > lastIdx) { | |
| html += escapeHtml(text.substring(lastIdx, ent.start)); | |
| } | |
| let type = ent.entity || ent.entity_group; | |
| if (/^[BIES]-/.test(type)) type = type.substring(2); | |
| const color = entityColors[type] || '#888'; | |
| const label = type.replace('private_', ''); | |
| html += `<span class="redacted" style="--bg: ${color};"> | |
| <span class="redacted-text">${escapeHtml(text.substring(ent.start, ent.end))}</span> | |
| <span class="redacted-label">${label}</span> | |
| </span>`; | |
| lastIdx = ent.end; | |
| } | |
| } | |
| if (lastIdx < text.length) { | |
| html += escapeHtml(text.substring(lastIdx)); | |
| } | |
| outputText.innerHTML = html; | |
| } | |
| function escapeHtml(unsafe) { | |
| return unsafe | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| </script> | |
| </body> | |
| </html> | |