Spaces:
Sleeping
Sleeping
| <html lang="id"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Profanity 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)]"> | |
| <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">Profanity Test</h1> | |
| <p class="mt-1 text-sm text-[#52606d]">Deteksi kata kasar dan tidak pantas 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 animate-pulse"></span> | |
| <span id="statusText">Menghubungi server…</span> | |
| </span> | |
| </div> | |
| <div class="grid grid-cols-2 gap-6 mobile:grid-cols-1"> | |
| <!-- Panel Kiri --> | |
| <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="inputText">Teks Input</label> | |
| <textarea id="inputText" rows="9" 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]" | |
| placeholder="Masukkan teks untuk mengecek kata kasar atau ungkapan yang perlu diperhalus. Contoh: Tolong ubah kalimat marah ini menjadi lebih sopan dan profesional."></textarea> | |
| <div class="flex gap-2 flex-wrap"> | |
| <button id="checkBtn" class="px-4 py-[10px] bg-teal text-white rounded-[6px] font-bold border-0 | |
| cursor-pointer hover:bg-teal-dark transition-colors" type="button">Periksa</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" 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" type="button">Hapus</button> | |
| </div> | |
| <!-- Legenda --> | |
| <div> | |
| <p class="text-xs font-bold text-[#52606d] uppercase tracking-[0.5px] mb-2">Legenda</p> | |
| <div class="flex flex-wrap gap-2 text-xs font-semibold leading-[2.2]"> | |
| <span class="px-2 py-0.5 rounded bg-red-100 text-red-800 border border-red-200">HIGH — vulgar/seksual | |
| eksplisit</span> | |
| <span class="px-2 py-0.5 rounded bg-orange-100 text-orange-800 border border-orange-200">MEDIUM — | |
| umpatan/makian</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Panel Kanan --> | |
| <section class="bg-white border border-line rounded-lg p-5 flex flex-col gap-4"> | |
| <!-- Tab --> | |
| <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" type="button">Teks | |
| Ternotasi</button> | |
| <button data-tab="list" class="tab-btn px-4 py-2 text-sm font-bold border-b-2 border-transparent | |
| text-[#52606d] -mb-px hover:text-[#1f2933]" type="button">Daftar Temuan</button> | |
| </div> | |
| <!-- Tab: Highlight --> | |
| <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.2] text-base break-words"> | |
| <span class="text-[#52606d] text-sm italic">Hasil akan muncul di sini.</span> | |
| </div> | |
| </div> | |
| <!-- Tab: Daftar --> | |
| <div id="tab-list" class="tab-panel hidden flex-col gap-2"> | |
| <p id="listSummary" class="text-xs text-[#52606d] m-0"></p> | |
| <div id="findingList" class="flex flex-col gap-2"></div> | |
| <p id="noFindings" class="text-sm text-[#52606d] italic hidden">Tidak ada kata kasar terdeteksi.</p> | |
| </div> | |
| <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 = "http://127.0.0.1:8005"; | |
| const SAMPLES = [ | |
| "Tolong jelaskan materi fotosintesis", | |
| "anjing lu, laporan ini bangsat banget", | |
| "kontol! memek! itu kata vulgar yang tidak pantas", | |
| "bego banget si brengsek itu, goblok", | |
| "Pak Andi adalah guru yang sangat baik dan sabar", | |
| "anjiiing susah banget soal ini, brengsek!", | |
| ].join("\n"); | |
| const SEV_STYLE = { | |
| HIGH: { bg: "#fee2e2", color: "#991b1b", border: "#fca5a5", label: "HIGH" }, | |
| MEDIUM: { bg: "#ffedd5", color: "#9a3412", border: "#fdba74", label: "MEDIUM" }, | |
| }; | |
| const $ = id => document.getElementById(id); | |
| // Tab | |
| document.querySelectorAll(".tab-btn").forEach(b => | |
| b.addEventListener("click", () => { | |
| document.querySelectorAll(".tab-panel").forEach(p => p.classList.add("hidden")); | |
| document.querySelectorAll(".tab-btn").forEach(x => { | |
| x.classList.toggle("border-teal", x.dataset.tab === b.dataset.tab); | |
| x.classList.toggle("text-teal", x.dataset.tab === b.dataset.tab); | |
| x.classList.toggle("border-transparent", x.dataset.tab !== b.dataset.tab); | |
| x.classList.toggle("text-[#52606d]", x.dataset.tab !== b.dataset.tab); | |
| }); | |
| $("tab-" + b.dataset.tab).classList.remove("hidden"); | |
| $("tab-" + b.dataset.tab).classList.add("flex"); | |
| }) | |
| ); | |
| // Status | |
| async function checkStatus() { | |
| try { | |
| const r = await fetch(API + "/api/status", { signal: AbortSignal.timeout(3000) }); | |
| const d = await r.json(); | |
| $("statusDot").className = "w-2 h-2 rounded-full bg-emerald-500"; | |
| $("statusText").textContent = `Siap — ${d.lexicon_size ?? "?"} kata`; | |
| $("statusBadge").className = "inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200"; | |
| } catch { | |
| $("statusDot").className = "w-2 h-2 rounded-full bg-red-500"; | |
| $("statusText").textContent = "Server tidak terjangkau"; | |
| $("statusBadge").className = "inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm font-semibold bg-red-50 text-red-700 border border-red-200"; | |
| setTimeout(checkStatus, 5000); | |
| } | |
| } | |
| function esc(s) { | |
| return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); | |
| } | |
| // Highlight | |
| function buildHighlight(text, findings) { | |
| if (!findings.length) return '<span class="text-[#52606d] text-sm italic">Tidak ada kata kasar terdeteksi.</span>'; | |
| const sorted = [...findings].sort((a, b) => a.start - b.start); | |
| let html = "", cursor = 0; | |
| for (const f of sorted) { | |
| if (f.start < cursor) continue; | |
| html += esc(text.slice(cursor, f.start)); | |
| const s = SEV_STYLE[f.severity] || SEV_STYLE.MEDIUM; | |
| html += `<mark style="background:${s.bg};color:${s.color};border:1px solid ${s.border}; | |
| border-radius:4px;padding:1px 6px;cursor:default;" | |
| title="${esc(s.label)} (${Math.round(f.confidence * 100)}%)">${esc(f.word)}<sup | |
| style="font-size:9px;margin-left:3px;opacity:.7;font-weight:700;">${esc(s.label)}</sup></mark>`; | |
| cursor = f.end; | |
| } | |
| html += esc(text.slice(cursor)); | |
| return html; | |
| } | |
| // Finding list | |
| function buildList(findings) { | |
| $("findingList").innerHTML = ""; | |
| if (!findings.length) { $("noFindings").classList.remove("hidden"); return; } | |
| $("noFindings").classList.add("hidden"); | |
| const counts = { HIGH: 0, MEDIUM: 0 }; | |
| findings.forEach(f => counts[f.severity]++); | |
| $("listSummary").textContent = | |
| `${findings.length} temuan: ${counts.HIGH} HIGH, ${counts.MEDIUM} MEDIUM.`; | |
| findings.forEach(f => { | |
| const s = SEV_STYLE[f.severity] || SEV_STYLE.MEDIUM; | |
| const card = document.createElement("div"); | |
| card.className = "flex items-start gap-3 p-3 rounded-[6px] border border-line bg-[#fbfdff]"; | |
| card.innerHTML = ` | |
| <span style="flex-shrink:0;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:800; | |
| background:${s.bg};color:${s.color};border:1px solid ${s.border};margin-top:1px;">${esc(s.label)}</span> | |
| <div class="flex-1"> | |
| <div class="text-sm font-bold text-[#1f2933] mb-0.5">"${esc(f.word)}" | |
| <span class="text-xs font-normal text-[#52606d]">(${esc(f.normalized)})</span></div> | |
| <div class="text-xs text-[#52606d] leading-[1.5]">${esc(f.reason)}</div> | |
| </div> | |
| <div class="flex-shrink-0 text-xs font-bold tabular-nums" style="color:${s.color}"> | |
| ${Math.round(f.confidence * 100)}% | |
| </div>`; | |
| $("findingList").appendChild(card); | |
| }); | |
| } | |
| // Check | |
| async function check() { | |
| const text = $("inputText").value.trim(); | |
| if (!text) return; | |
| $("checkBtn").disabled = true; | |
| $("checkBtn").textContent = "Memeriksa…"; | |
| $("errorBox").hidden = true; | |
| try { | |
| const res = await fetch(API + "/api/profanity", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ text }), | |
| signal: AbortSignal.timeout(10000), | |
| }); | |
| if (!res.ok) throw new Error("HTTP " + res.status); | |
| const data = await res.json(); | |
| const findings = data.findings || []; | |
| $("highlightBox").innerHTML = buildHighlight(text, findings); | |
| buildList(findings); | |
| } catch (err) { | |
| $("errorBox").textContent = `Gagal: ${err.message}`; | |
| $("errorBox").hidden = false; | |
| } finally { | |
| $("checkBtn").disabled = false; | |
| $("checkBtn").textContent = "Periksa"; | |
| checkStatus(); | |
| } | |
| } | |
| $("checkBtn").addEventListener("click", check); | |
| $("inputText").addEventListener("keydown", e => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) check(); }); | |
| $("sampleBtn").addEventListener("click", () => { $("inputText").value = SAMPLES; }); | |
| $("clearBtn").addEventListener("click", () => { | |
| $("inputText").value = ""; | |
| $("highlightBox").innerHTML = '<span class="text-[#52606d] text-sm italic">Hasil akan muncul di sini.</span>'; | |
| $("findingList").innerHTML = ""; | |
| $("listSummary").textContent = ""; | |
| $("noFindings").classList.add("hidden"); | |
| $("errorBox").hidden = true; | |
| }); | |
| checkStatus(); | |
| // Init tab state | |
| $("tab-highlight").classList.add("flex"); | |
| </script> | |
| </body> | |
| </html> | |