Spaces:
Sleeping
Sleeping
| <html lang="id"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>NER 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> | |
| <style> | |
| /* Warna highlight per tipe entitas (dipakai via inline style oleh JS) */ | |
| :root { | |
| --c-orang: #dcfce7; | |
| --ct-orang: #14532d; | |
| --cb-orang: #86efac; | |
| --c-organisasi: #dbeafe; | |
| --ct-organisasi: #1e3a8a; | |
| --cb-organisasi: #93c5fd; | |
| --c-lokasi: #ede9fe; | |
| --ct-lokasi: #4c1d95; | |
| --cb-lokasi: #c4b5fd; | |
| --c-fasilitas: #ffedd5; | |
| --ct-fasilitas: #7c2d12; | |
| --cb-fasilitas: #fdba74; | |
| --c-kejadian: #fef9c3; | |
| --ct-kejadian: #713f12; | |
| --cb-kejadian: #fde047; | |
| --c-produk: #ccfbf1; | |
| --ct-produk: #134e4a; | |
| --cb-produk: #5eead4; | |
| --c-karya: #fce7f3; | |
| --ct-karya: #831843; | |
| --cb-karya: #f9a8d4; | |
| --c-peraturan: #fee2e2; | |
| --ct-peraturan: #7f1d1d; | |
| --cb-peraturan: #fca5a5; | |
| --c-tanggal: #f1f5f9; | |
| --ct-tanggal: #1e293b; | |
| --cb-tanggal: #cbd5e1; | |
| --c-waktu: #e0e7ff; | |
| --ct-waktu: #1e1b4b; | |
| --cb-waktu: #a5b4fc; | |
| --c-uang: #fef3c7; | |
| --ct-uang: #78350f; | |
| --cb-uang: #fcd34d; | |
| --c-agama: #d1fae5; | |
| --ct-agama: #064e3b; | |
| --cb-agama: #6ee7b7; | |
| --c-bahasa: #cffafe; | |
| --ct-bahasa: #164e63; | |
| --cb-bahasa: #67e8f9; | |
| } | |
| </style> | |
| </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">NER Test</h1> | |
| <p class="mt-1 text-sm text-[#52606d]"> | |
| Uji model <span id="modelName" class="font-semibold text-teal">memuat…</span> | |
| </p> | |
| </div> | |
| <!-- Status badge --> | |
| <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="nerInput">Teks Input</label> | |
| <textarea id="nerInput" 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 mengenali nama orang, organisasi, lokasi, tanggal, atau dokumen. Contoh: Ahmad Santoso bekerja di PT Teknologi Maju Indonesia di Jakarta sejak 15 Maret 2020. Kementerian Keuangan menerbitkan PP No. 23/2024."></textarea> | |
| <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 disabled:opacity-50" type="button"> | |
| Deteksi Entitas | |
| </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> | |
| <!-- Legend warna --> | |
| <div> | |
| <p class="text-xs font-bold text-[#52606d] uppercase tracking-[0.5px] mb-2">Legend</p> | |
| <div id="legend" class="flex flex-wrap gap-[6px] text-xs font-semibold"></div> | |
| </div> | |
| </section> | |
| <!-- Panel Kanan: Output --> | |
| <section class="bg-white border border-line rounded-lg p-5 flex flex-col gap-4"> | |
| <!-- Teks dengan highlight --> | |
| <div> | |
| <p class="text-sm font-bold mb-2 m-0">Teks Ternotasi</p> | |
| <div id="highlightBox" class="min-h-[120px] p-4 bg-[#fbfdff] border border-line rounded-[6px] | |
| leading-[2] text-base [overflow-wrap:anywhere] text-[#1f2933]"> | |
| <span class="text-[#52606d] text-sm italic">Hasil highlight akan muncul di sini.</span> | |
| </div> | |
| </div> | |
| <!-- Daftar entitas --> | |
| <div id="entityListSection" hidden> | |
| <p class="text-sm font-bold mb-2 m-0"> | |
| Entitas Terdeteksi | |
| <span id="entityCount" class="ml-2 text-xs font-semibold text-[#52606d]"></span> | |
| </p> | |
| <div id="entityList" class="flex flex-col gap-3"></div> | |
| </div> | |
| <!-- Pesan error --> | |
| <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> | |
| ; | |
| // Konfigurasi | |
| const API_BASE = "http://127.0.0.1:8001"; | |
| const SAMPLE_TEXTS = [ | |
| "Ahmad Santoso bekerja di PT Teknologi Maju Indonesia di Jakarta sejak 15 Maret 2020.", | |
| "Kementerian Keuangan menerbitkan Peraturan Pemerintah No. 23/2024 tentang pajak UMKM.", | |
| "Prof. Budi Raharjo dari Universitas Gadjah Mada memenangkan penghargaan Rp 500 juta.", | |
| "KPK memeriksa pejabat OJK terkait kasus di Provinsi Jawa Barat pada bulan Oktober.", | |
| "Partai Golkar dan PDIP bersepakat dalam sidang MPR membahas Undang-Undang No. 12/2023.", | |
| ]; | |
| // Warna per tipe entitas (bg, text, border) | |
| const ENTITY_COLORS = { | |
| ORANG: { bg: "#dcfce7", color: "#14532d", border: "#86efac", label: "Orang" }, | |
| ORGANISASI: { bg: "#dbeafe", color: "#1e3a8a", border: "#93c5fd", label: "Organisasi" }, | |
| LOKASI: { bg: "#ede9fe", color: "#4c1d95", border: "#c4b5fd", label: "Lokasi" }, | |
| FASILITAS: { bg: "#ffedd5", color: "#7c2d12", border: "#fdba74", label: "Fasilitas" }, | |
| KEJADIAN: { bg: "#fef9c3", color: "#713f12", border: "#fde047", label: "Kejadian" }, | |
| PRODUK: { bg: "#ccfbf1", color: "#134e4a", border: "#5eead4", label: "Produk" }, | |
| KARYA: { bg: "#fce7f3", color: "#831843", border: "#f9a8d4", label: "Karya" }, | |
| PERATURAN: { bg: "#fee2e2", color: "#7f1d1d", border: "#fca5a5", label: "Peraturan" }, | |
| TANGGAL: { bg: "#f1f5f9", color: "#1e293b", border: "#cbd5e1", label: "Tanggal" }, | |
| WAKTU: { bg: "#e0e7ff", color: "#1e1b4b", border: "#a5b4fc", label: "Waktu" }, | |
| UANG: { bg: "#fef3c7", color: "#78350f", border: "#fcd34d", label: "Uang" }, | |
| AGAMA: { bg: "#d1fae5", color: "#064e3b", border: "#6ee7b7", label: "Agama" }, | |
| BAHASA: { bg: "#cffafe", color: "#164e63", border: "#67e8f9", label: "Bahasa" }, | |
| }; | |
| // Elemen DOM | |
| const $ = (id) => document.getElementById(id); | |
| const nerInput = $("nerInput"); | |
| const detectBtn = $("detectBtn"); | |
| const sampleBtn = $("sampleBtn"); | |
| const clearBtn = $("clearBtn"); | |
| const highlightBox = $("highlightBox"); | |
| const entityList = $("entityList"); | |
| const entityListSec = $("entityListSection"); | |
| const entityCount = $("entityCount"); | |
| const errorBox = $("errorBox"); | |
| const statusBadge = $("statusBadge"); | |
| const statusDot = $("statusDot"); | |
| const statusText = $("statusText"); | |
| const modelName = $("modelName"); | |
| const legend = $("legend"); | |
| // Legend | |
| function buildLegend() { | |
| for (const [key, c] of Object.entries(ENTITY_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); | |
| } | |
| } | |
| // Status server | |
| async function checkStatus() { | |
| try { | |
| const res = await fetch(API_BASE + "/api/status", { signal: AbortSignal.timeout(4000) }); | |
| const data = await res.json(); | |
| const ready = data.model_loaded; | |
| statusDot.className = `w-2 h-2 rounded-full ${ready ? "bg-emerald-500" : "bg-amber-400"}`; | |
| statusText.textContent = ready ? "Server siap" : "Model sedang dimuat…"; | |
| statusBadge.className = statusBadge.className | |
| .replace(/bg-\S+ text-\S+ border-\S+/g, "") | |
| .trim() + (ready | |
| ? " bg-emerald-50 text-emerald-700 border-emerald-200" | |
| : " bg-amber-50 text-amber-700 border-amber-200"); | |
| modelName.textContent = data.model || "—"; | |
| detectBtn.disabled = !ready; | |
| if (!ready) setTimeout(checkStatus, 3000); | |
| } catch { | |
| statusDot.className = "w-2 h-2 rounded-full bg-red-500"; | |
| statusText.textContent = "Server tidak terjangkau"; | |
| statusBadge.className = statusBadge.className | |
| .replace(/bg-\S+ text-\S+ border-\S+/g, "") | |
| .trim() + " bg-red-50 text-red-700 border-red-200"; | |
| modelName.textContent = "—"; | |
| detectBtn.disabled = true; | |
| setTimeout(checkStatus, 5000); | |
| } | |
| } | |
| // Teks helper | |
| function escapeHtml(str) { | |
| return str | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">"); | |
| } | |
| // Highlight teks | |
| function buildHighlight(text, entities) { | |
| const sorted = [...entities].sort((a, b) => a.start - b.start); | |
| let html = ""; | |
| let cursor = 0; | |
| for (const e of sorted) { | |
| if (e.start < cursor) continue; // entitas tumpang-tindih, skip | |
| html += escapeHtml(text.slice(cursor, e.start)); | |
| const c = ENTITY_COLORS[e.label] || { bg: "#f3f4f6", color: "#374151", border: "#d1d5db" }; | |
| const pct = Math.round(e.score * 100); | |
| const srcIcon = e.source === "rule" ? " ◆" : ""; | |
| 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="${e.label} — ${pct}% kepercayaan (${e.source})">` + | |
| escapeHtml(e.word) + | |
| `<sup style="font-size:9px;margin-left:3px;opacity:0.75;font-weight:700;">${e.label}${srcIcon}</sup></mark>`; | |
| cursor = e.end; | |
| } | |
| html += escapeHtml(text.slice(cursor)); | |
| return html || '<span class="text-[#52606d] text-sm italic">Tidak ada entitas terdeteksi.</span>'; | |
| } | |
| // Daftar entitas per grup | |
| function buildEntityList(entities) { | |
| const grouped = {}; | |
| for (const e of entities) { | |
| (grouped[e.label] ??= []).push(e); | |
| } | |
| entityList.innerHTML = ""; | |
| for (const [label, items] of Object.entries(grouped).sort()) { | |
| const c = ENTITY_COLORS[label] || { bg: "#f3f4f6", color: "#374151", border: "#d1d5db", label }; | |
| const section = document.createElement("div"); | |
| // Header grup | |
| const header = document.createElement("div"); | |
| header.className = "flex items-center justify-between mb-1"; | |
| header.innerHTML = ` | |
| <span class="text-xs font-bold text-[#1f2933]">${c.label || label}</span> | |
| <span class="text-xs text-[#52606d] font-semibold">${items.length} entitas</span>`; | |
| section.appendChild(header); | |
| // Badge entitas | |
| const badges = document.createElement("div"); | |
| badges.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;`; | |
| const srcIcon = e.source === "rule" ? " ◆" : ""; | |
| badge.title = `Skor: ${Math.round(e.score * 100)}% | Sumber: ${e.source}`; | |
| badge.innerHTML = escapeHtml(e.word) + srcIcon; | |
| badges.appendChild(badge); | |
| } | |
| section.appendChild(badges); | |
| // Pemisah antar grup (kecuali yang terakhir) | |
| entityList.appendChild(section); | |
| const divider = document.createElement("hr"); | |
| divider.className = "border-line"; | |
| entityList.appendChild(divider); | |
| } | |
| // Hapus divider terakhir | |
| entityList.lastElementChild?.remove(); | |
| } | |
| // Deteksi entitas | |
| async function detect() { | |
| const text = nerInput.value.trim(); | |
| if (!text) return; | |
| detectBtn.disabled = true; | |
| detectBtn.textContent = "Mendeteksi…"; | |
| errorBox.hidden = true; | |
| highlightBox.innerHTML = '<span class="text-[#52606d] text-sm">Memproses…</span>'; | |
| entityListSec.hidden = true; | |
| try { | |
| const res = await fetch(API_BASE + "/api/ner", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ text }), | |
| signal: AbortSignal.timeout(30000), | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| const entities = data.entities || []; | |
| highlightBox.innerHTML = buildHighlight(text, entities); | |
| if (entities.length > 0) { | |
| buildEntityList(entities); | |
| entityCount.textContent = `(${entities.length} total)`; | |
| entityListSec.hidden = false; | |
| } else { | |
| entityListSec.hidden = true; | |
| } | |
| } catch (err) { | |
| errorBox.textContent = `Gagal menghubungi server NER: ${err.message}`; | |
| errorBox.hidden = false; | |
| highlightBox.innerHTML = '<span class="text-[#52606d] text-sm italic">Hasil highlight akan muncul di sini.</span>'; | |
| } finally { | |
| detectBtn.disabled = false; | |
| detectBtn.textContent = "Deteksi Entitas"; | |
| checkStatus(); // perbarui status setelah request | |
| } | |
| } | |
| // Event binding | |
| detectBtn.addEventListener("click", detect); | |
| nerInput.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) detect(); | |
| }); | |
| sampleBtn.addEventListener("click", () => { | |
| nerInput.value = SAMPLE_TEXTS.join("\n"); | |
| }); | |
| clearBtn.addEventListener("click", () => { | |
| nerInput.value = ""; | |
| highlightBox.innerHTML = '<span class="text-[#52606d] text-sm italic">Hasil highlight akan muncul di sini.</span>'; | |
| entityListSec.hidden = true; | |
| errorBox.hidden = true; | |
| }); | |
| // Init | |
| buildLegend(); | |
| checkStatus(); | |
| </script> | |
| </body> | |
| </html> | |