Spaces:
Running
Running
Download index.html from Gm82/darkbert-ner-fixed: direct link, hf CLI and curl.
- Browser
- Download file 22.8 kB
-
https://huggingface.co/spaces/Gm82/darkbert-ner-fixed/resolve/main/index.html
- Command line
-
hf download hf://spaces/Gm82/darkbert-ner-fixed/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Gm82/darkbert-ner-fixed/resolve/main/index.html
22.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>VERISBERTA · DarkBERT NER</title> | |
| <style> | |
| :root { | |
| --bg: #080c14; | |
| --surface: #0d1420; | |
| --surface2: #111b2e; | |
| --border: #1e2d4a; | |
| --accent: #00d4ff; | |
| --accent2: #ff3a6e; | |
| --accent3: #a259ff; | |
| --text: #c8d8f0; | |
| --text-dim: #4a6080; | |
| --actor: #ff3a6e; | |
| --actor-bg: rgba(255,58,110,0.12); | |
| --action: #00d4ff; | |
| --action-bg: rgba(0,212,255,0.10); | |
| --assets: #a259ff; | |
| --assets-bg: rgba(162,89,255,0.12); | |
| --o: #4a6080; | |
| --glow: 0 0 20px rgba(0,212,255,0.15); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'Segoe UI', system-ui, sans-serif; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* ── GRID BACKGROUND ── */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background-image: | |
| linear-gradient(rgba(0,212,255,0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0,212,255,0.03) 1px, transparent 1px); | |
| background-size: 48px 48px; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| /* ── FLOATING NODES ── */ | |
| .node { | |
| position: fixed; | |
| border-radius: 50%; | |
| filter: blur(60px); | |
| pointer-events: none; | |
| z-index: 0; | |
| animation: drift 18s ease-in-out infinite; | |
| } | |
| .node-1 { width:300px; height:300px; background:rgba(0,212,255,0.07); top:-60px; left:10%; } | |
| .node-2 { width:200px; height:200px; background:rgba(255,58,110,0.07); bottom:10%; right:5%; animation-delay:-7s; } | |
| .node-3 { width:250px; height:250px; background:rgba(162,89,255,0.06); top:40%; left:70%; animation-delay:-13s; } | |
| @keyframes drift { | |
| 0%,100% { transform: translateY(0) translateX(0); } | |
| 33% { transform: translateY(-30px) translateX(20px); } | |
| 66% { transform: translateY(20px) translateX(-15px); } | |
| } | |
| /* ── LAYOUT ── */ | |
| .wrapper { | |
| position: relative; | |
| z-index: 1; | |
| max-width: 980px; | |
| margin: 0 auto; | |
| padding: 40px 24px 80px; | |
| } | |
| /* ── HEADER ── */ | |
| header { | |
| text-align: center; | |
| margin-bottom: 48px; | |
| } | |
| .badge-row { | |
| display: flex; | |
| justify-content: center; | |
| gap: 8px; | |
| margin-bottom: 18px; | |
| flex-wrap: wrap; | |
| } | |
| .badge { | |
| font-size: 11px; | |
| font-weight: 600; | |
| letter-spacing: .08em; | |
| text-transform: uppercase; | |
| padding: 3px 10px; | |
| border-radius: 20px; | |
| border: 1px solid currentColor; | |
| } | |
| .badge-dark { color: #888; border-color: #333; } | |
| .badge-cyber { color: var(--accent); border-color: rgba(0,212,255,0.3); } | |
| .badge-ner { color: var(--accent2); border-color: rgba(255,58,110,0.3); } | |
| h1 { | |
| font-size: clamp(28px, 5vw, 46px); | |
| font-weight: 800; | |
| letter-spacing: -.02em; | |
| line-height: 1.1; | |
| background: linear-gradient(135deg, #fff 30%, var(--accent) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .sub { | |
| margin-top: 12px; | |
| color: var(--text-dim); | |
| font-size: 14px; | |
| max-width: 500px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| line-height: 1.6; | |
| } | |
| .model-link { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| margin-top: 14px; | |
| font-size: 12px; | |
| color: var(--accent); | |
| text-decoration: none; | |
| border: 1px solid rgba(0,212,255,0.2); | |
| border-radius: 20px; | |
| padding: 4px 12px; | |
| transition: all .2s; | |
| } | |
| .model-link:hover { background: rgba(0,212,255,0.1); border-color: var(--accent); } | |
| .fixed-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| margin-left: 8px; | |
| font-size: 12px; | |
| color: #22c55e; | |
| text-decoration: none; | |
| border: 1px solid rgba(34,197,94,0.3); | |
| border-radius: 20px; | |
| padding: 4px 12px; | |
| } | |
| /* ── PLATFORM CARD ── */ | |
| .platform { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 28px; | |
| margin-bottom: 24px; | |
| box-shadow: var(--glow), 0 2px 40px rgba(0,0,0,0.4); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .platform::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; right: 0; | |
| height: 1px; | |
| background: linear-gradient(90deg, transparent, var(--accent), transparent); | |
| opacity: .4; | |
| } | |
| .platform-label { | |
| font-size: 10px; | |
| font-weight: 700; | |
| letter-spacing: .12em; | |
| text-transform: uppercase; | |
| color: var(--text-dim); | |
| margin-bottom: 14px; | |
| } | |
| /* ── TOKEN INPUT ── */ | |
| .token-row { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .token-row label { font-size: 13px; color: var(--text-dim); white-space: nowrap; } | |
| #hf-token { | |
| flex: 1; | |
| min-width: 200px; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 8px 12px; | |
| color: var(--text); | |
| font-size: 13px; | |
| font-family: monospace; | |
| outline: none; | |
| transition: border-color .2s; | |
| } | |
| #hf-token:focus { border-color: var(--accent); } | |
| .token-hint { font-size: 11px; color: var(--text-dim); margin-top: 6px; } | |
| .token-hint a { color: var(--accent); text-decoration: none; } | |
| /* ── TEXTAREA + CONTROLS ── */ | |
| .input-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 10px; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| textarea { | |
| width: 100%; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 14px 16px; | |
| color: var(--text); | |
| font-size: 14px; | |
| line-height: 1.6; | |
| resize: vertical; | |
| min-height: 120px; | |
| outline: none; | |
| font-family: inherit; | |
| transition: border-color .2s; | |
| } | |
| textarea:focus { border-color: var(--accent); } | |
| .samples-row { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| margin-top: 10px; | |
| } | |
| .sample-btn { | |
| font-size: 11px; | |
| padding: 4px 10px; | |
| border-radius: 20px; | |
| border: 1px solid var(--border); | |
| background: transparent; | |
| color: var(--text-dim); | |
| cursor: pointer; | |
| transition: all .2s; | |
| } | |
| .sample-btn:hover { border-color: var(--accent); color: var(--accent); } | |
| .run-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: linear-gradient(135deg, #00a8cc, #0070ff); | |
| color: #fff; | |
| font-weight: 700; | |
| font-size: 14px; | |
| border: none; | |
| border-radius: 10px; | |
| padding: 10px 24px; | |
| cursor: pointer; | |
| transition: opacity .2s, transform .15s; | |
| margin-top: 14px; | |
| } | |
| .run-btn:hover { opacity: .9; transform: translateY(-1px); } | |
| .run-btn:active { transform: translateY(0); } | |
| .run-btn:disabled { opacity: .4; cursor: not-allowed; transform: none; } | |
| .spinner { | |
| width: 14px; height: 14px; | |
| border: 2px solid rgba(255,255,255,.3); | |
| border-top-color: #fff; | |
| border-radius: 50%; | |
| animation: spin .7s linear infinite; | |
| display: none; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* ── OUTPUT ── */ | |
| #output-section { display: none; } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| } | |
| .stat-card { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 14px; | |
| text-align: center; | |
| } | |
| .stat-label { font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 4px; } | |
| .stat-value { font-size: 28px; font-weight: 800; } | |
| .stat-actor .stat-label { color: var(--actor); } .stat-actor .stat-value { color: var(--actor); } | |
| .stat-action .stat-label { color: var(--action); } .stat-action .stat-value { color: var(--action); } | |
| .stat-assets .stat-label { color: var(--assets); } .stat-assets .stat-value { color: var(--assets); } | |
| .stat-total .stat-label { color: var(--text-dim); } .stat-total .stat-value { color: var(--text); } | |
| #rendered-text { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 18px 20px; | |
| font-size: 15px; | |
| line-height: 2; | |
| word-break: break-word; | |
| } | |
| /* ── ENTITY HIGHLIGHT CHIPS ── */ | |
| .ent { | |
| display: inline; | |
| padding: 2px 0; | |
| border-radius: 4px; | |
| position: relative; | |
| cursor: default; | |
| } | |
| .ent-label { | |
| display: inline-block; | |
| font-size: 10px; | |
| font-weight: 800; | |
| letter-spacing: .08em; | |
| border-radius: 3px; | |
| padding: 0px 5px; | |
| margin-left: 3px; | |
| vertical-align: middle; | |
| position: relative; | |
| top: -1px; | |
| } | |
| .ent-ACTOR { background: var(--actor-bg); color: var(--actor); border-bottom: 2px solid var(--actor); } | |
| .ent-ACTION { background: var(--action-bg); color: var(--action); border-bottom: 2px solid var(--action); } | |
| .ent-ASSETS { background: var(--assets-bg); color: var(--assets); border-bottom: 2px solid var(--assets); } | |
| .ent-label-ACTOR { background: var(--actor); color: #fff; } | |
| .ent-label-ACTION { background: var(--action); color: #000; } | |
| .ent-label-ASSETS { background: var(--assets); color: #fff; } | |
| /* tooltip */ | |
| .ent:hover::after { | |
| content: attr(data-score); | |
| position: absolute; | |
| bottom: 100%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: #1a2840; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| padding: 3px 8px; | |
| font-size: 11px; | |
| color: var(--text); | |
| white-space: nowrap; | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| /* ── LEGEND ── */ | |
| .legend { | |
| display: flex; | |
| gap: 18px; | |
| flex-wrap: wrap; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| font-size: 13px; | |
| } | |
| .legend-dot { | |
| width: 10px; height: 10px; | |
| border-radius: 50%; | |
| } | |
| /* ── JSON ZONE ── */ | |
| #json-toggle { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text-dim); | |
| font-size: 12px; | |
| padding: 6px 14px; | |
| cursor: pointer; | |
| transition: all .2s; | |
| margin-top: 14px; | |
| } | |
| #json-toggle:hover { border-color: var(--accent3); color: var(--accent3); } | |
| #json-pane { | |
| display: none; | |
| margin-top: 12px; | |
| background: #060b12; | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 16px; | |
| font-family: monospace; | |
| font-size: 12px; | |
| color: #7ec8e3; | |
| max-height: 280px; | |
| overflow-y: auto; | |
| line-height: 1.5; | |
| white-space: pre-wrap; | |
| } | |
| /* ── ERROR ── */ | |
| #error-box { | |
| display: none; | |
| background: rgba(255,58,110,0.1); | |
| border: 1px solid rgba(255,58,110,0.3); | |
| border-radius: 10px; | |
| padding: 14px 18px; | |
| color: var(--actor); | |
| font-size: 13px; | |
| margin-top: 14px; | |
| } | |
| /* ── FOOTER ── */ | |
| footer { | |
| text-align: center; | |
| margin-top: 60px; | |
| font-size: 12px; | |
| color: var(--text-dim); | |
| } | |
| footer a { color: var(--accent); text-decoration: none; } | |
| /* ── RESPONSIVE ── */ | |
| @media (max-width: 600px) { | |
| .wrapper { padding: 24px 16px 60px; } | |
| .platform { padding: 20px 16px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="node node-1"></div> | |
| <div class="node node-2"></div> | |
| <div class="node node-3"></div> | |
| <div class="wrapper"> | |
| <!-- HEADER --> | |
| <header> | |
| <div class="badge-row"> | |
| <span class="badge badge-dark">DarkBERT</span> | |
| <span class="badge badge-cyber">VERIS Vocabulary</span> | |
| <span class="badge badge-ner">Named Entity Recognition</span> | |
| <span class="badge fixed-badge">🔧 Fixed Endpoint</span> | |
| </div> | |
| <h1>VERISBERTA</h1> | |
| <p class="sub"> | |
| Cybersecurity incident NER — identifies <strong>Actors</strong>, <strong>Actions</strong>, | |
| and <strong>Assets</strong> in threat intelligence narratives using VERIS 4A taxonomy. | |
| </p> | |
| <a class="model-link" href="https://huggingface.co/processpl/DarkBERT-finetuned-ner" target="_blank"> | |
| 🤗 processpl/DarkBERT-finetuned-ner | |
| </a> | |
| <a class="fixed-badge" href="https://huggingface.co/spaces/super9igga/darkbert-ner-demo" target="_blank"> | |
| Fork of super9igga demo | |
| </a> | |
| </header> | |
| <!-- TOKEN PLATFORM --> | |
| <div class="platform"> | |
| <div class="platform-label">🔑 Authentication</div> | |
| <div class="token-row"> | |
| <label for="hf-token">HF Token (recommended)</label> | |
| <input type="password" id="hf-token" placeholder="hf_..." autocomplete="off" /> | |
| </div> | |
| <p class="token-hint"> | |
| A Hugging Face token is <strong>required</strong> for the Inference Providers API. | |
| Create a free read token at | |
| <a href="https://huggingface.co/settings/tokens" target="_blank">huggingface.co/settings/tokens</a>. | |
| Your token is used only in your browser — it is never stored or sent anywhere except the HF API. | |
| </p> | |
| </div> | |
| <!-- INPUT PLATFORM --> | |
| <div class="platform"> | |
| <div class="platform-label">📝 Incident Narrative Input</div> | |
| <div class="input-header"> | |
| <span style="font-size:13px; color:var(--text-dim)">Paste a cybersecurity incident description</span> | |
| <div class="legend"> | |
| <div class="legend-item"><div class="legend-dot" style="background:var(--actor)"></div> ACTOR</div> | |
| <div class="legend-item"><div class="legend-dot" style="background:var(--action)"></div> ACTION</div> | |
| <div class="legend-item"><div class="legend-dot" style="background:var(--assets)"></div> ASSETS</div> | |
| </div> | |
| </div> | |
| <textarea id="input-text" rows="5" placeholder="e.g. A nation-state actor exploited a zero-day vulnerability in the VPN gateway to steal employee credentials from the HR database..."></textarea> | |
| <div class="samples-row"> | |
| <span style="font-size:11px;color:var(--text-dim);align-self:center">Try:</span> | |
| <button class="sample-btn" onclick="setSample(0)">Ransomware</button> | |
| <button class="sample-btn" onclick="setSample(1)">Data Breach</button> | |
| <button class="sample-btn" onclick="setSample(2)">Insider Threat</button> | |
| <button class="sample-btn" onclick="setSample(3)">APT Attack</button> | |
| </div> | |
| <button class="run-btn" id="run-btn" onclick="runNER()"> | |
| <div class="spinner" id="spinner"></div> | |
| <span id="btn-text">⚡ Analyze Entities</span> | |
| </button> | |
| <div id="error-box"></div> | |
| </div> | |
| <!-- OUTPUT PLATFORM --> | |
| <div class="platform" id="output-section"> | |
| <div class="platform-label">🎯 Entity Detection Results</div> | |
| <div class="stats-grid"> | |
| <div class="stat-card stat-actor"> | |
| <div class="stat-label">Actor</div> | |
| <div class="stat-value" id="cnt-actor">0</div> | |
| </div> | |
| <div class="stat-card stat-action"> | |
| <div class="stat-label">Action</div> | |
| <div class="stat-value" id="cnt-action">0</div> | |
| </div> | |
| <div class="stat-card stat-assets"> | |
| <div class="stat-label">Assets</div> | |
| <div class="stat-value" id="cnt-assets">0</div> | |
| </div> | |
| <div class="stat-card stat-total"> | |
| <div class="stat-label">Total</div> | |
| <div class="stat-value" id="cnt-total">0</div> | |
| </div> | |
| </div> | |
| <div id="rendered-text"></div> | |
| <button id="json-toggle" onclick="toggleJson()">{ } Raw API Response</button> | |
| <div id="json-pane"></div> | |
| </div> | |
| <!-- FOOTER --> | |
| <footer> | |
| Built on <a href="https://huggingface.co/s2w-ai/DarkBERT" target="_blank">DarkBERT</a> · | |
| Fine-tuned on <a href="https://huggingface.co/datasets/guidobenb/VCDB_NER_LG2220" target="_blank">VCDB dataset</a> · | |
| Model by <a href="https://huggingface.co/processpl" target="_blank">processpl</a> · | |
| Fixed fork by <a href="https://huggingface.co/Gm82" target="_blank">Gm82</a> | |
| </footer> | |
| </div> | |
| <script> | |
| /* ── FIXED: current Inference Providers router endpoint ── | |
| (original demo used the deprecated api-inference.huggingface.co) */ | |
| const API_URL = 'https://router.huggingface.co/hf-inference/models/processpl/DarkBERT-finetuned-ner'; | |
| const SAMPLES = [ | |
| "A ransomware group encrypted the company's file servers after gaining access through a phishing email targeting the finance department. The attacker exfiltrated sensitive payroll data before deploying the malware payload.", | |
| "An external hacker breached the customer database by exploiting an unpatched SQL injection vulnerability in the web application. Personal information including credit card numbers and social security numbers were stolen.", | |
| "A malicious insider with privileged access to the network infrastructure intentionally deleted critical backup systems and leaked proprietary source code to a competitor. The employee abused administrator credentials over several weeks.", | |
| "A nation-state sponsored threat actor performed a spear-phishing campaign against defense contractors. The APT group installed a rootkit on workstations to maintain persistent access and exfiltrated classified documents from secure file shares." | |
| ]; | |
| function setSample(i) { | |
| document.getElementById('input-text').value = SAMPLES[i]; | |
| } | |
| let rawResult = null; | |
| let jsonVisible = false; | |
| function toggleJson() { | |
| const pane = document.getElementById('json-pane'); | |
| const btn = document.getElementById('json-toggle'); | |
| jsonVisible = !jsonVisible; | |
| pane.style.display = jsonVisible ? 'block' : 'none'; | |
| btn.textContent = jsonVisible ? '{ } Hide Raw Response' : '{ } Raw API Response'; | |
| if (jsonVisible && rawResult) { | |
| pane.textContent = JSON.stringify(rawResult, null, 2); | |
| } | |
| } | |
| async function runNER() { | |
| const text = document.getElementById('input-text').value.trim(); | |
| const token = document.getElementById('hf-token').value.trim(); | |
| const errBox = document.getElementById('error-box'); | |
| if (!text) { showError('Please enter some text first.'); return; } | |
| if (!token) { showError('An HF token is required. Create one at huggingface.co/settings/tokens and paste it in the Authentication box.'); return; } | |
| errBox.style.display = 'none'; | |
| setLoading(true); | |
| const headers = { 'Content-Type': 'application/json' }; | |
| if (token) headers['Authorization'] = `Bearer ${token}`; | |
| try { | |
| const resp = await fetch(API_URL, { | |
| method: 'POST', | |
| headers, | |
| body: JSON.stringify({ inputs: text }) | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({})); | |
| const msg = err.error || `HTTP ${resp.status}: ${resp.statusText}`; | |
| throw new Error(msg); | |
| } | |
| const data = await resp.json(); | |
| rawResult = data; | |
| /* HF token-classification returns array of entities OR nested array */ | |
| const entities = Array.isArray(data[0]) ? data[0] : data; | |
| renderResults(text, entities); | |
| } catch (e) { | |
| const m = e.message || ''; | |
| showError('API error: ' + m + | |
| (m.includes('401') ? ' — invalid or missing token. Paste a read token above (settings/tokens).' : '') + | |
| (m.includes('403') ? ' — token lacks Inference permission. Create a new token with "Make calls to Inference Providers" enabled.' : '') + | |
| (m.includes('404') ? ' — model not available on the hf-inference provider; check the model page.' : '') + | |
| (m.includes('429') ? ' — rate limited; wait a moment and retry.' : '') + | |
| (m.includes('Failed to fetch') ? ' — network/CORS error. Make sure you are using a valid token and not blocking the request.' : '') | |
| ); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| function setLoading(on) { | |
| const btn = document.getElementById('run-btn'); | |
| const spinner = document.getElementById('spinner'); | |
| const btnText = document.getElementById('btn-text'); | |
| btn.disabled = on; | |
| spinner.style.display = on ? 'block' : 'none'; | |
| btnText.textContent = on ? 'Analyzing…' : '⚡ Analyze Entities'; | |
| } | |
| function showError(msg) { | |
| const b = document.getElementById('error-box'); | |
| b.textContent = msg; | |
| b.style.display = 'block'; | |
| } | |
| function renderResults(text, entities) { | |
| const out = document.getElementById('output-section'); | |
| const el = document.getElementById('rendered-text'); | |
| const typeOf = e => (e.entity_group || e.entity || '').replace('B-','').replace('I-',''); | |
| /* Count by type */ | |
| const counts = { ACTOR: 0, ACTION: 0, ASSETS: 0 }; | |
| entities.forEach(e => { | |
| const t = typeOf(e); | |
| if (counts[t] !== undefined) counts[t]++; | |
| }); | |
| document.getElementById('cnt-actor').textContent = counts.ACTOR; | |
| document.getElementById('cnt-action').textContent = counts.ACTION; | |
| document.getElementById('cnt-assets').textContent = counts.ASSETS; | |
| document.getElementById('cnt-total').textContent = counts.ACTOR + counts.ACTION + counts.ASSETS; | |
| /* Build highlighted HTML */ | |
| let html = ''; | |
| let cursor = 0; | |
| /* Sort by start position */ | |
| const sorted = [...entities] | |
| .filter(e => ['ACTOR','ACTION','ASSETS'].includes(typeOf(e))) | |
| .sort((a, b) => (a.start || 0) - (b.start || 0)); | |
| sorted.forEach(ent => { | |
| const start = ent.start || 0; | |
| const end = ent.end || 0; | |
| const type = typeOf(ent); | |
| const score = ((ent.score || 0) * 100).toFixed(1) + '%'; | |
| const word = ent.word || text.slice(start, end); | |
| if (start > cursor) { | |
| html += escHtml(text.slice(cursor, start)); | |
| } | |
| html += `<span class="ent ent-${type}" data-score="${type} · ${score}">${escHtml(word)}<span class="ent-label ent-label-${type}">${type}</span></span>`; | |
| cursor = end; | |
| }); | |
| if (cursor < text.length) html += escHtml(text.slice(cursor)); | |
| el.innerHTML = html || '<span style="color:var(--text-dim)">No entities found — try a different text.</span>'; | |
| if (jsonVisible && rawResult) { | |
| document.getElementById('json-pane').textContent = JSON.stringify(rawResult, null, 2); | |
| } | |
| out.style.display = 'block'; | |
| out.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } | |
| function escHtml(s) { | |
| return s | |
| .replace(/&/g,'&') | |
| .replace(/</g,'<') | |
| .replace(/>/g,'>') | |
| .replace(/"/g,'"'); | |
| } | |
| /* Load a sample on first visit */ | |
| setSample(0); | |
| </script> | |
| </body> | |
| </html> | |