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; } | |
| .btn-hero { | |
| font-size: 1.15rem; | |
| padding: 1.1rem 2.75rem; | |
| border-radius: 14px; | |
| } | |
| /* Metadata */ | |
| .meta-grid { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.75rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .meta-chip { | |
| background: rgba(255,255,255,0.04); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 0.5rem 0.9rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.2rem; | |
| min-width: 120px; | |
| } | |
| .meta-key { | |
| font-size: 0.68rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--text-secondary); | |
| } | |
| .meta-value { | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| word-break: break-word; | |
| } | |
| /* 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%; } | |
| @media (max-width: 768px) { | |
| .container { padding: 2rem 0; gap: 1.5rem; } | |
| .glass-panel { padding: 1rem 0.75rem; border-radius: 12px; } | |
| .output-text { font-size: 0.95rem; line-height: 1.7; padding: 1rem 0.75rem; } | |
| .legend { padding: 1rem 0.75rem; } | |
| header { padding: 0 0.75rem; } | |
| h1 { font-size: 2.5rem; } | |
| } | |
| </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> | |
| <h1>OpenAI Privacy Filter</h1> | |
| <p class="subtitle">Beoordeel zelf hoe goed OpenAI's nieuwe open source privacyfilter-model werkt op het Nederlands.</p> | |
| </header> | |
| <div class="glass-panel" style="display: flex; flex-direction: column; align-items: center; gap: 0.5rem;"> | |
| <button id="randomBtn" class="btn-primary btn-hero"> | |
| <svg id="randomIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="7.5" cy="7.5" r="1" fill="currentColor" stroke="none"/><circle cx="16.5" cy="7.5" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="7.5" cy="16.5" r="1" fill="currentColor" stroke="none"/><circle cx="16.5" cy="16.5" r="1" fill="currentColor" stroke="none"/></svg> | |
| <div class="spinner" id="spinner"></div> | |
| <span id="randomBtnText">Random Webpagina</span> | |
| </button> | |
| <span id="docIndex" style="font-size:0.8rem; color: var(--text-secondary);"></span> | |
| </div> | |
| <div class="glass-panel results-container" id="resultsPanel"> | |
| <div class="results-header"> | |
| <h2>Analyseresultaten</h2> | |
| </div> | |
| <div class="meta-grid" id="metaGrid"></div> | |
| <div class="summary-grid" id="summaryGrid"></div> | |
| <div class="output-text" id="outputText"></div> | |
| </div> | |
| <div class="legend" id="legend"></div> | |
| <p style="text-align: center; color: var(--text-secondary); font-size: 0.85rem; margin: 0;"> | |
| Deze space is gebaseerd op <a href="https://huggingface.co/spaces/akhaliq/privacy-filter" target="_blank" rel="noopener" style="color: inherit;">khaliq/privacy-filter</a> | |
| </p> | |
| </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 randomBtn = document.getElementById('randomBtn'); | |
| const randomIcon = document.getElementById('randomIcon'); | |
| const randomBtnText = document.getElementById('randomBtnText'); | |
| const spinner = document.getElementById('spinner'); | |
| const docIndex = document.getElementById('docIndex'); | |
| const resultsPanel = document.getElementById('resultsPanel'); | |
| const outputText = document.getElementById('outputText'); | |
| const summaryGrid = document.getElementById('summaryGrid'); | |
| const metaGrid = document.getElementById('metaGrid'); | |
| let client = null; | |
| async function initClient() { | |
| try { | |
| client = await Client.connect(window.location.origin); | |
| } catch (error) { | |
| console.error("Gradio connection error:", error); | |
| } | |
| } | |
| initClient(); | |
| const META_FIELDS = ['url', 'license_abbr', 'language', 'language_score']; | |
| function renderMeta(doc) { | |
| metaGrid.innerHTML = ''; | |
| for (const key of META_FIELDS) { | |
| const val = doc[key]; | |
| if (val === null || val === undefined || val === '') continue; | |
| const chip = document.createElement('div'); | |
| chip.className = 'meta-chip'; | |
| const label = key.replace(/_/g, ' '); | |
| chip.innerHTML = `<span class="meta-key">${escapeHtml(label)}</span><span class="meta-value">${escapeHtml(String(val))}</span>`; | |
| metaGrid.appendChild(chip); | |
| } | |
| } | |
| randomBtn.addEventListener('click', async () => { | |
| if (!client) { | |
| await initClient(); | |
| if (!client) { alert("Unable to connect to the analysis server."); return; } | |
| } | |
| randomBtn.disabled = true; | |
| randomIcon.style.display = 'none'; | |
| spinner.style.display = 'block'; | |
| randomBtnText.textContent = 'Loading...'; | |
| resultsPanel.style.display = 'none'; | |
| try { | |
| const res = await fetch('/random_document'); | |
| const doc = await res.json(); | |
| const text = doc.text; | |
| docIndex.innerHTML = `Document #${doc._index + 1} van ${doc._total} uit <a href="https://huggingface.co/datasets/BramVanroy/CommonCrawl-CreativeCommons-fine" target="_blank" rel="noopener" style="color: inherit;">CommonCrawl-CreativeCommons-fine</a>. Zelf teksten invoeren? Dat kan <a href="https://huggingface.co/spaces/akhaliq/privacy-filter" target="_blank" rel="noopener" style="color: inherit;">hier</a>.`; | |
| renderMeta(doc); | |
| 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("Failed:", error); | |
| alert("An error occurred."); | |
| } finally { | |
| randomBtn.disabled = false; | |
| randomIcon.style.display = 'block'; | |
| spinner.style.display = 'none'; | |
| randomBtnText.textContent = 'Random Webpagina'; | |
| } | |
| }); | |
| 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> | |