Spaces:
Running
Running
| <html lang="id"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>PII Test β Prompt Builder</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" | |
| rel="stylesheet"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| fontFamily: { sans: ['"Plus Jakarta Sans"', 'Arial', 'sans-serif'] }, | |
| extend: { | |
| colors: { | |
| line: '#dbe3ea', | |
| input: '#cbd5df', | |
| teal: { DEFAULT: '#167c80', dark: '#0f5f63', light: '#dff4f5', lighter: '#e7f7f8' }, | |
| }, | |
| screens: { mobile: { max: '860px' } }, | |
| }, | |
| }, | |
| } | |
| </script> | |
| </head> | |
| <body class="font-sans bg-[#f6f8fb] text-[#1f2933] min-h-screen | |
| py-[clamp(16px,3vh,24px)] px-[clamp(16px,2.5vw,24px)] box-border"> | |
| <main class="w-full max-w-[1280px] mx-auto flex flex-col gap-4"> | |
| <!-- Header --> | |
| <div class="flex items-baseline justify-between flex-wrap gap-3"> | |
| <div> | |
| <h1 class="m-0 text-[28px] font-extrabold leading-tight">PII Test</h1> | |
| <p class="mt-1 text-sm text-[#52606d]">Uji deteksi informasi pribadi dalam teks Bahasa Indonesia.</p> | |
| </div> | |
| <span id="statusBadge" class="inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm font-semibold | |
| bg-slate-100 text-slate-500 border border-slate-200"> | |
| <span id="statusDot" class="w-2 h-2 rounded-full bg-slate-400"></span> | |
| <span id="statusText">Menghubungi serverβ¦</span> | |
| </span> | |
| </div> | |
| <!-- Grid dua kolom --> | |
| <div class="grid grid-cols-2 gap-6 mobile:grid-cols-1"> | |
| <!-- ββ Panel Kiri: Input ββ --> | |
| <section class="bg-white border border-line rounded-lg p-5 flex flex-col gap-4"> | |
| <label class="text-sm font-bold m-0" for="piiInput">Teks Input</label> | |
| <textarea id="piiInput" class="w-full box-border px-[10px] py-[10px] border border-input rounded-[6px] | |
| font-[inherit] text-base resize-none leading-[1.55] | |
| focus:outline-none focus:border-teal focus:ring-[3px] focus:ring-teal/[.14]" rows="10" | |
| placeholder="Tempel teks untuk mencari data pribadi seperti NIK, email, nomor HP, alamat, atau rekening. Contoh: Nama: Budi Santoso, NIK 3273011203980001 email budi@perusahaan.co.id, HP 0812-3456-7890"></textarea> | |
| <!-- Slider confidence --> | |
| <div class="flex flex-col gap-1"> | |
| <div class="flex justify-between items-center"> | |
| <label class="text-xs font-bold text-[#52606d] uppercase tracking-[0.5px]" for="confSlider"> | |
| Minimum Confidence | |
| </label> | |
| <span id="confValue" class="text-xs font-bold text-teal tabular-nums">0.70</span> | |
| </div> | |
| <input id="confSlider" type="range" min="0" max="100" value="70" step="5" | |
| class="w-full accent-teal h-1.5 rounded-full cursor-pointer"> | |
| <p class="text-xs text-[#52606d]">Entitas dengan skor di bawah nilai ini diabaikan.</p> | |
| </div> | |
| <!-- Tombol aksi --> | |
| <div class="flex gap-2 flex-wrap"> | |
| <button id="detectBtn" class="px-4 py-[10px] bg-teal text-white rounded-[6px] font-bold border-0 | |
| cursor-pointer hover:bg-teal-dark transition-colors duration-150" type="button">Deteksi PII</button> | |
| <button id="sampleBtn" class="px-4 py-[10px] bg-teal-light text-teal rounded-[6px] font-bold border-0 | |
| cursor-pointer hover:bg-teal-lighter transition-colors duration-150" type="button">Isi | |
| Contoh</button> | |
| <button id="clearBtn" class="px-4 py-[10px] bg-white text-[#52606d] rounded-[6px] font-bold | |
| border border-line cursor-pointer hover:bg-[#f6f8fb] transition-colors duration-150" | |
| type="button">Hapus</button> | |
| </div> | |
| <!-- Legenda --> | |
| <div> | |
| <p class="text-xs font-bold text-[#52606d] uppercase tracking-[0.5px] mb-2">Legenda Tipe PII</p> | |
| <div id="legend" class="flex flex-wrap gap-[6px] text-xs font-semibold leading-[2]"></div> | |
| </div> | |
| </section> | |
| <!-- ββ Panel Kanan: Output ββ --> | |
| <section class="bg-white border border-line rounded-lg p-5 flex flex-col gap-5"> | |
| <!-- Tab navigasi --> | |
| <div class="flex gap-0 border-b border-line -mx-5 px-5"> | |
| <button data-tab="highlight" | |
| class="tab-btn px-4 py-2 text-sm font-bold border-b-2 border-teal text-teal -mb-px transition-colors" | |
| type="button">Teks Ternotasi</button> | |
| <button data-tab="redacted" class="tab-btn px-4 py-2 text-sm font-bold border-b-2 border-transparent text-[#52606d] -mb-px | |
| hover:text-[#1f2933] transition-colors" type="button">Teks Tersunting</button> | |
| <button data-tab="entities" class="tab-btn px-4 py-2 text-sm font-bold border-b-2 border-transparent text-[#52606d] -mb-px | |
| hover:text-[#1f2933] transition-colors" type="button">Daftar Entitas</button> | |
| </div> | |
| <!-- Tab: Teks Ternotasi --> | |
| <div id="tab-highlight" class="tab-panel flex flex-col gap-3"> | |
| <div id="highlightBox" class="min-h-[120px] p-4 bg-[#fbfdff] border border-line rounded-[6px] | |
| leading-[2.1] text-base [overflow-wrap:anywhere]"> | |
| <span class="text-[#52606d] text-sm italic">Hasil highlight akan muncul di sini.</span> | |
| </div> | |
| </div> | |
| <!-- Tab: Teks Tersunting --> | |
| <div id="tab-redacted" class="tab-panel hidden flex-col gap-3"> | |
| <div class="flex justify-between items-center"> | |
| <p class="text-xs text-[#52606d] m-0">Semua PII diganti dengan placeholder bernomor.</p> | |
| <button id="copyRedacted" class="text-xs font-bold text-teal hover:text-teal-dark transition-colors" | |
| type="button">Salin</button> | |
| </div> | |
| <pre id="redactedBox" | |
| class="m-0 p-4 bg-[#fbfdff] border border-line rounded-[6px] text-sm | |
| leading-[1.7] [overflow-wrap:anywhere] whitespace-pre-wrap | |
| font-[inherit] text-[#1f2933]"><span class="text-[#52606d] italic">Teks tersunting akan muncul di sini.</span></pre> | |
| </div> | |
| <!-- Tab: Daftar Entitas --> | |
| <div id="tab-entities" class="tab-panel hidden flex-col gap-3"> | |
| <p id="entitySummary" class="text-xs text-[#52606d] m-0"></p> | |
| <div id="entityList" class="flex flex-col gap-4"></div> | |
| <p id="noEntities" class="text-sm text-[#52606d] italic hidden">Tidak ada PII terdeteksi.</p> | |
| </div> | |
| <!-- Error box --> | |
| <div id="errorBox" hidden | |
| class="p-4 bg-red-50 border border-red-200 rounded-[6px] text-sm text-red-700 font-medium"></div> | |
| </section> | |
| </div> | |
| </main> | |
| <script> | |
| ; | |
| const API_BASE = "http://127.0.0.1:8002"; | |
| const SAMPLES = [ | |
| "Nama: Budi Santoso, NIK 3273011203980001, lahir di Bandung.", | |
| "NPWP: 01.234.567.4-901.000 β hubungi budi@perusahaan.co.id", | |
| "Rekening: 1234567890 BCA, HP 0812-3456-7890 atau +6281298765432.", | |
| "Plat kendaraan B 1234 ABC, IP server: 192.168.1.254.", | |
| "Kartu kredit Visa 4111 1111 1111 1111 atas nama Ibu Sari.", | |
| "Alamat: Jl. Merdeka No. 45, RT 03/RW 07, Kel. Sukamaju, Kec. Cibeunying.", | |
| "Paspor A1234567, SIM No. 123456789012, BPJS Kesehatan 0001234567890.", | |
| "Tgl. Lahir: 15 Januari 1990, nomor tetap (021) 55667788.", | |
| ].join("\n"); | |
| // Warna per tipe PII β berbeda dari NER agar mudah dibedakan | |
| const PII_COLORS = { | |
| NIK: { bg: "#fee2e2", color: "#7f1d1d", border: "#fca5a5", label: "NIK" }, | |
| NPWP_LAMA: { bg: "#ffedd5", color: "#7c2d12", border: "#fdba74", label: "NPWP (lama)" }, | |
| NPWP_BARU: { bg: "#fef3c7", color: "#78350f", border: "#fcd34d", label: "NPWP (baru)" }, | |
| BPJS_KES: { bg: "#fce7f3", color: "#831843", border: "#f9a8d4", label: "BPJS Kes." }, | |
| BPJS_TK: { bg: "#fdf2f8", color: "#701a75", border: "#f0abfc", label: "BPJS TK" }, | |
| PASPOR: { bg: "#ede9fe", color: "#4c1d95", border: "#c4b5fd", label: "Paspor" }, | |
| SIM: { bg: "#e0e7ff", color: "#1e1b4b", border: "#a5b4fc", label: "SIM" }, | |
| PLAT_NOMOR: { bg: "#d1fae5", color: "#064e3b", border: "#6ee7b7", label: "Plat Nomor" }, | |
| EMAIL: { bg: "#dbeafe", color: "#1e3a8a", border: "#93c5fd", label: "Email" }, | |
| TELEPON_HP: { bg: "#cffafe", color: "#164e63", border: "#67e8f9", label: "Telepon HP" }, | |
| TELEPON_TETAP: { bg: "#e0f2fe", color: "#0c4a6e", border: "#7dd3fc", label: "Telepon Tetap" }, | |
| REKENING_BANK: { bg: "#ccfbf1", color: "#134e4a", border: "#5eead4", label: "Rekening Bank" }, | |
| KARTU_KREDIT: { bg: "#f3e8ff", color: "#581c87", border: "#d8b4fe", label: "Kartu Kredit" }, | |
| IP_V4: { bg: "#f0fdf4", color: "#14532d", border: "#86efac", label: "IPv4" }, | |
| IP_V6: { bg: "#ecfdf5", color: "#065f46", border: "#6ee7b7", label: "IPv6" }, | |
| TANGGAL_LAHIR: { bg: "#f8fafc", color: "#1e293b", border: "#cbd5e1", label: "Tgl. Lahir" }, | |
| ALAMAT: { bg: "#fafaf9", color: "#292524", border: "#d6d3d1", label: "Alamat" }, | |
| }; | |
| const DEFAULT_COLOR = { bg: "#f3f4f6", color: "#374151", border: "#d1d5db", label: "Lainnya" }; | |
| // ββ DOM refs βββββββββββββββββββββββββββββββββββββββββββββββ | |
| const $ = (id) => document.getElementById(id); | |
| const piiInput = $("piiInput"); | |
| const detectBtn = $("detectBtn"); | |
| const sampleBtn = $("sampleBtn"); | |
| const clearBtn = $("clearBtn"); | |
| const confSlider = $("confSlider"); | |
| const confValue = $("confValue"); | |
| const highlightBox = $("highlightBox"); | |
| const redactedBox = $("redactedBox"); | |
| const copyRedacted = $("copyRedacted"); | |
| const entityList = $("entityList"); | |
| const entitySummary = $("entitySummary"); | |
| const noEntities = $("noEntities"); | |
| const errorBox = $("errorBox"); | |
| const statusBadge = $("statusBadge"); | |
| const statusDot = $("statusDot"); | |
| const statusText = $("statusText"); | |
| const legend = $("legend"); | |
| // ββ Tab logic ββββββββββββββββββββββββββββββββββββββββββββββ | |
| function switchTab(name) { | |
| document.querySelectorAll(".tab-panel").forEach(p => p.classList.add("hidden")); | |
| document.querySelectorAll(".tab-btn").forEach(b => { | |
| const active = b.dataset.tab === name; | |
| b.classList.toggle("border-teal", active); | |
| b.classList.toggle("text-teal", active); | |
| b.classList.toggle("border-transparent", !active); | |
| b.classList.toggle("text-[#52606d]", !active); | |
| }); | |
| const panel = $("tab-" + name); | |
| panel.classList.remove("hidden"); | |
| panel.classList.add("flex"); | |
| } | |
| document.querySelectorAll(".tab-btn").forEach(b => | |
| b.addEventListener("click", () => switchTab(b.dataset.tab)) | |
| ); | |
| // ββ Legenda ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| (function buildLegend() { | |
| for (const [key, c] of Object.entries(PII_COLORS)) { | |
| const el = document.createElement("span"); | |
| el.style.cssText = `background:${c.bg};color:${c.color};border:1px solid ${c.border}; | |
| border-radius:4px;padding:2px 8px;`; | |
| el.textContent = c.label; | |
| legend.appendChild(el); | |
| } | |
| })(); | |
| // ββ Server status ββββββββββββββββββββββββββββββββββββββββββ | |
| async function checkStatus() { | |
| try { | |
| const res = await fetch(API_BASE + "/api/status", { signal: AbortSignal.timeout(3000) }); | |
| if (!res.ok) throw new Error(); | |
| statusDot.className = "w-2 h-2 rounded-full bg-emerald-500"; | |
| statusText.textContent = "Server siap"; | |
| statusBadge.className = statusBadge.className | |
| .replace(/bg-\S+\s+text-\S+\s+border-\S+/, "") | |
| .trim() + " bg-emerald-50 text-emerald-700 border-emerald-200"; | |
| } catch { | |
| statusDot.className = "w-2 h-2 rounded-full bg-red-500"; | |
| statusText.textContent = "Server tidak terjangkau"; | |
| statusBadge.className = statusBadge.className | |
| .replace(/bg-\S+\s+text-\S+\s+border-\S+/, "") | |
| .trim() + " bg-red-50 text-red-700 border-red-200"; | |
| setTimeout(checkStatus, 5000); | |
| } | |
| } | |
| // ββ HTML escape ββββββββββββββββββββββββββββββββββββββββββββ | |
| function esc(s) { | |
| return String(s) | |
| .replace(/&/g, "&").replace(/</g, "<") | |
| .replace(/>/g, ">").replace(/"/g, """); | |
| } | |
| // ββ Highlight builder ββββββββββββββββββββββββββββββββββββββ | |
| function buildHighlight(text, entities) { | |
| if (!entities.length) { | |
| return '<span class="text-[#52606d] text-sm italic">Tidak ada PII terdeteksi.</span>'; | |
| } | |
| const sorted = [...entities].sort((a, b) => a.start - b.start); | |
| let html = "", cursor = 0; | |
| for (const e of sorted) { | |
| if (e.start < cursor) continue; | |
| html += esc(text.slice(cursor, e.start)); | |
| const c = PII_COLORS[e.label] || DEFAULT_COLOR; | |
| const pct = Math.round(e.confidence * 100); | |
| html += `<mark | |
| style="background:${c.bg};color:${c.color};border:1px solid ${c.border}; | |
| border-radius:4px;padding:1px 6px;cursor:default;white-space:nowrap;" | |
| title="${esc(c.label)} β ${pct}% kepercayaan" | |
| >${esc(e.word)}<sup style="font-size:9px;margin-left:3px;opacity:.75;font-weight:700;" | |
| >${esc(e.label)}</sup></mark>`; | |
| cursor = e.end; | |
| } | |
| html += esc(text.slice(cursor)); | |
| return html; | |
| } | |
| // ββ Daftar entitas per grup ββββββββββββββββββββββββββββββββ | |
| function buildEntityList(entities) { | |
| entityList.innerHTML = ""; | |
| // Kelompokkan per label | |
| const grouped = {}; | |
| for (const e of entities) { | |
| (grouped[e.label] ??= []).push(e); | |
| } | |
| const types = Object.keys(grouped).sort(); | |
| for (const label of types) { | |
| const items = grouped[label]; | |
| const c = PII_COLORS[label] || DEFAULT_COLOR; | |
| const section = document.createElement("div"); | |
| // Header | |
| const header = document.createElement("div"); | |
| header.className = "flex items-center justify-between mb-1"; | |
| header.innerHTML = ` | |
| <span class="text-xs font-bold text-[#1f2933]">${esc(c.label)}</span> | |
| <span class="text-xs text-[#52606d] font-semibold">${items.length} entitas</span>`; | |
| section.appendChild(header); | |
| // Badge per entitas | |
| const wrap = document.createElement("div"); | |
| wrap.style.lineHeight = "2.4"; | |
| for (const e of items) { | |
| const badge = document.createElement("span"); | |
| badge.style.cssText = ` | |
| display:inline-block;margin:2px 4px;padding:3px 9px; | |
| background:${c.bg};color:${c.color};border:1px solid ${c.border}; | |
| border-radius:5px;font-size:13px;font-weight:600;cursor:default;`; | |
| badge.title = `Skor: ${Math.round(e.confidence * 100)}%\nKonteks: β¦${e.context}β¦`; | |
| badge.textContent = e.word; | |
| // Meta NIK: tampilkan info tanggal lahir | |
| if (e.label === "NIK" && e.meta && e.meta.day) { | |
| const gender = e.meta.is_female ? "P" : "L"; | |
| const info = document.createElement("sup"); | |
| info.style.cssText = "font-size:9px;margin-left:4px;opacity:.7;"; | |
| info.textContent = `${gender} ${String(e.meta.day).padStart(2, "0")}/${String(e.meta.month).padStart(2, "0")}/${e.meta.year_suffix}`; | |
| badge.appendChild(info); | |
| } | |
| wrap.appendChild(badge); | |
| } | |
| section.appendChild(wrap); | |
| entityList.appendChild(section); | |
| const hr = document.createElement("hr"); | |
| hr.className = "border-line"; | |
| entityList.appendChild(hr); | |
| } | |
| entityList.lastElementChild?.remove(); // hapus HR terakhir | |
| } | |
| // ββ Deteksi ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function detect() { | |
| const text = piiInput.value.trim(); | |
| if (!text) return; | |
| detectBtn.disabled = true; | |
| detectBtn.textContent = "Mendeteksiβ¦"; | |
| errorBox.hidden = true; | |
| const minConf = parseFloat(confSlider.value) / 100; | |
| try { | |
| const res = await fetch(API_BASE + "/api/pii", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ text, min_confidence: minConf }), | |
| signal: AbortSignal.timeout(15000), | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| const entities = data.entities || []; | |
| // Tab highlight | |
| highlightBox.innerHTML = buildHighlight(text, entities); | |
| // Tab redacted | |
| redactedBox.textContent = data.redacted || text; | |
| // Tab entities | |
| if (entities.length) { | |
| const typeCount = new Set(entities.map(e => e.label)).size; | |
| entitySummary.textContent = | |
| `${entities.length} entitas terdeteksi dari ${typeCount} tipe PII.`; | |
| buildEntityList(entities); | |
| noEntities.classList.add("hidden"); | |
| } else { | |
| entitySummary.textContent = ""; | |
| entityList.innerHTML = ""; | |
| noEntities.classList.remove("hidden"); | |
| } | |
| switchTab("highlight"); | |
| } catch (err) { | |
| errorBox.textContent = `Gagal menghubungi server PII: ${err.message}`; | |
| errorBox.hidden = false; | |
| } finally { | |
| detectBtn.disabled = false; | |
| detectBtn.textContent = "Deteksi PII"; | |
| checkStatus(); | |
| } | |
| } | |
| // ββ Salin teks tersunting βββββββββββββββββββββββββββββββββ | |
| copyRedacted.addEventListener("click", () => { | |
| const text = redactedBox.textContent; | |
| if (navigator.clipboard?.writeText) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| copyRedacted.textContent = "Disalin!"; | |
| setTimeout(() => { copyRedacted.textContent = "Salin"; }, 1500); | |
| }); | |
| } | |
| }); | |
| // ββ Slider confidence βββββββββββββββββββββββββββββββββββββ | |
| confSlider.addEventListener("input", () => { | |
| confValue.textContent = (confSlider.value / 100).toFixed(2); | |
| }); | |
| // ββ Event binding ββββββββββββββββββββββββββββββββββββββββββ | |
| detectBtn.addEventListener("click", detect); | |
| piiInput.addEventListener("keydown", e => { | |
| if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) detect(); | |
| }); | |
| sampleBtn.addEventListener("click", () => { piiInput.value = SAMPLES; }); | |
| clearBtn.addEventListener("click", () => { | |
| piiInput.value = ""; | |
| highlightBox.innerHTML = | |
| '<span class="text-[#52606d] text-sm italic">Hasil highlight akan muncul di sini.</span>'; | |
| redactedBox.innerHTML = | |
| '<span class="text-[#52606d] italic">Teks tersunting akan muncul di sini.</span>'; | |
| entityList.innerHTML = ""; | |
| entitySummary.textContent = ""; | |
| noEntities.classList.add("hidden"); | |
| errorBox.hidden = true; | |
| }); | |
| // ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| checkStatus(); | |
| switchTab("highlight"); | |
| </script> | |
| </body> | |
| </html> | |