Prompt-Builder / web /filler-test.html
ArielJoe's picture
feat: strengthen all detectors and simplify to Indonesia-only
aa16b4b
Raw
History Blame Contribute Delete
22.8 kB
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Filler Checker 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-start justify-between flex-wrap gap-3">
<div>
<h1 class="m-0 text-[28px] font-extrabold leading-tight">Filler Checker Test</h1>
<p class="mt-1 text-sm text-[#52606d] max-w-xl">
Deteksi frasa basa-basi yang tidak menambah informasi ke prompt AI β€”
sapaan, ucapan terima kasih, permintaan maaf, dan pembuka kosong yang hanya membuang token.
</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>
<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="inputText">Teks Prompt</label>
<textarea id="inputText" rows="10"
class="w-full box-border px-[10px] py-[10px] border border-input rounded-[6px]
font-[inherit] text-base resize-none leading-[1.6]
focus:outline-none focus:border-teal focus:ring-[3px] focus:ring-teal/[.14]"
placeholder="Masukkan prompt untuk mencari sapaan, basa-basi, atau frasa yang tidak menambah instruksi.&#10;Contoh: &quot;Halo ChatGPT, terima kasih sebelumnya ya! Tolong bantu buatkan ringkasan.&quot;"></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 Kategori -->
<div>
<p class="text-xs font-bold text-[#52606d] uppercase tracking-[0.5px] mb-2">Kategori</p>
<div class="flex flex-col gap-1.5 text-xs font-semibold">
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-sm flex-shrink-0" style="background:#fde68a;border:1px solid #f59e0b"></span><span>Sapaan ke AI</span></div>
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-sm flex-shrink-0" style="background:#fef3c7;border:1px solid #fcd34d"></span><span>Sapaan Kosong</span></div>
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-sm flex-shrink-0" style="background:#fed7aa;border:1px solid #fb923c"></span><span>Terima Kasih Tidak Perlu</span></div>
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-sm flex-shrink-0" style="background:#fde68a;border:1px solid #d97706"></span><span>Permintaan Maaf Tidak Perlu</span></div>
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-sm flex-shrink-0" style="background:#e9d5ff;border:1px solid #c084fc"></span><span>Pembuka Tanpa Konten</span></div>
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-sm flex-shrink-0" style="background:#bfdbfe;border:1px solid #93c5fd"></span><span>Basa-basi Emosional</span></div>
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-sm flex-shrink-0" style="background:#a7f3d0;border:1px solid #34d399"></span><span>Referensi Tidak Jelas</span></div>
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-sm flex-shrink-0" style="background:#e5e7eb;border:1px solid #9ca3af"></span><span>Partikel Tanpa Makna</span></div>
</div>
</div>
<!-- Tips Box -->
<div class="p-3 bg-amber-50 border border-amber-200 rounded-[6px] text-xs text-amber-900 leading-[1.6]">
<span class="font-bold">Tips:</span>
Prompt yang efektif langsung ke inti permintaan tanpa sapaan atau basa-basi.
Setiap karakter yang dihemat = lebih banyak ruang untuk informasi yang benar-benar dibutuhkan AI.
</div>
</section>
<!-- ── Panel Kanan β€” Output ── -->
<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 flex-shrink-0">
<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 Teranotasi</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>
<button data-tab="clean"
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">Prompt Bersih</button>
</div>
<!-- Tab: Teks Teranotasi -->
<div id="tab-highlight" class="tab-panel flex flex-col gap-3">
<div id="highlightBox"
class="min-h-[140px] p-4 bg-[#fbfdff] border border-line rounded-[6px]
leading-[2.4] text-base break-words whitespace-pre-wrap">
<span class="text-[#52606d] text-sm italic">Hasil akan muncul di sini setelah Anda klik Periksa.</span>
</div>
<!-- Statistik token -->
<div id="tokenStats" hidden
class="flex gap-3 flex-wrap text-xs font-semibold">
<span class="px-3 py-1.5 bg-[#f6f8fb] border border-line rounded-full text-[#52606d]">
Total karakter: <span id="statTotal" class="text-[#1f2933]">β€”</span>
</span>
<span class="px-3 py-1.5 bg-amber-50 border border-amber-200 rounded-full text-amber-800">
Filler: <span id="statFiller">β€”</span> karakter
(~<span id="statTokens">β€”</span> token terbuang)
</span>
<span class="px-3 py-1.5 bg-emerald-50 border border-emerald-200 rounded-full text-emerald-800">
Bisa dihemat: <span id="statSaved">β€”</span>%
</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 font-medium"></p>
<div id="findingList" class="flex flex-col gap-2"></div>
<p id="noFindings" class="text-sm text-[#52606d] italic hidden">
Tidak ada frasa tidak efektif terdeteksi. Prompt sudah ringkas!
</p>
</div>
<!-- Tab: Prompt Bersih -->
<div id="tab-clean" class="tab-panel hidden flex-col gap-3">
<p class="text-xs text-[#52606d] m-0">
Teks setelah semua frasa tidak efektif dihapus. Salin dan bandingkan dengan teks asli.
</p>
<div id="cleanBox"
class="min-h-[140px] p-4 bg-[#fbfdff] border border-line rounded-[6px]
text-base leading-[1.7] break-words whitespace-pre-wrap text-[#1f2933]">
<span class="text-[#52606d] text-sm italic">Prompt bersih akan muncul di sini.</span>
</div>
<div class="flex justify-end">
<button id="copyCleanBtn"
class="px-4 py-[9px] bg-teal-light text-teal rounded-[6px] font-bold border-0
cursor-pointer hover:bg-teal-lighter transition-colors text-sm"
type="button">Salin Prompt Bersih</button>
</div>
</div>
<div id="errorBox" hidden
class="p-4 bg-red-50 border border-red-200 rounded-[6px] text-sm text-red-700 font-medium flex-shrink-0"></div>
</section>
</div>
</main>
<script>
"use strict";
const API = "http://127.0.0.1:8006";
// Contoh teks yang menunjukkan berbagai kategori filler
const SAMPLES = [
"Halo ChatGPT! Tolong buatkan saya artikel tentang fotosintesis untuk siswa kelas 7.",
"Hi Claude, terima kasih sebelumnya ya atas bantuannya. Bisakah kamu jelaskan konsep machine learning?",
"Maaf mengganggu, boleh saya tanya sesuatu? Bagaimana cara kerja neural network?",
"Kamu adalah AI yang sangat hebat dan pintar. Saya harap kamu bisa membantu saya membuat laporan keuangan bulanan.",
"Dear AI, dengan segala hormat, saya ingin bertanya sesuatu tentang Python.",
"Perkenankan saya untuk bertanya mengenai cara optimasi database PostgreSQL.",
"Buatkan ringkasan artikel jurnal ini dalam 3 poin utama.",
].join("\n\n");
// Warna dan label per kategori
const CAT_STYLE = {
GREETING_AI: { bg: "#fde68a", color: "#92400e", border: "#f59e0b", label: "Sapaan ke AI" },
GREETING_ONLY: { bg: "#fef3c7", color: "#92400e", border: "#fcd34d", label: "Sapaan Kosong" },
THANKS: { bg: "#fed7aa", color: "#9a3412", border: "#fb923c", label: "Terima Kasih" },
APOLOGY: { bg: "#fde68a", color: "#b45309", border: "#d97706", label: "Permintaan Maaf" },
EMPTY_OPENER: { bg: "#e9d5ff", color: "#6b21a8", border: "#c084fc", label: "Pembuka Kosong" },
EMOTIONAL_FILLER: { bg: "#bfdbfe", color: "#1e40af", border: "#93c5fd", label: "Basa-basi Emosional" },
VAGUE_REFERENCE: { bg: "#a7f3d0", color: "#065f46", border: "#34d399", label: "Referensi Tidak Jelas" },
VAGUE_PARTICLE: { bg: "#e5e7eb", color: "#374151", border: "#9ca3af", label: "Partikel Tanpa Makna" },
};
function catStyle(cat) {
return CAT_STYLE[cat] || { bg: "#fef3c7", color: "#92400e", border: "#fcd34d", label: cat };
}
const $ = id => document.getElementById(id);
// ── Tab ──────────────────────────────────────────────────────────────────────
document.querySelectorAll(".tab-btn").forEach(btn =>
btn.addEventListener("click", () => {
document.querySelectorAll(".tab-panel").forEach(p => {
p.classList.add("hidden");
p.classList.remove("flex");
});
document.querySelectorAll(".tab-btn").forEach(b => {
const active = b.dataset.tab === btn.dataset.tab;
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-" + btn.dataset.tab);
panel.classList.remove("hidden");
panel.classList.add("flex");
})
);
// ── Escape HTML ───────────────────────────────────────────────────────────────
function esc(s) {
return String(s ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
// ── Estimasi token (pendekatan sederhana: 1 token β‰ˆ 4 karakter) ─────────────
function estTokens(chars) { return Math.round(chars / 4); }
// ── Teks Teranotasi ───────────────────────────────────────────────────────────
function buildHighlight(text, findings) {
if (!findings.length) {
return '<span class="text-emerald-600 text-sm font-semibold">βœ“ Tidak ada frasa tidak efektif β€” prompt sudah ringkas!</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;
// Teks di antara temuan (diperlihatkan normal)
if (f.start > cursor) {
html += `<span class="text-[#1f2933]">${esc(text.slice(cursor, f.start))}</span>`;
}
const s = catStyle(f.category);
html +=
`<mark title="${esc(f.reason)}"
style="background:${s.bg};color:${s.color};
border:1px solid ${s.border};border-radius:4px;
padding:1px 5px;cursor:help;display:inline;"
>${esc(f.word)}<sup
style="font-size:8px;margin-left:3px;opacity:.8;font-weight:800;vertical-align:super;"
>${esc(s.label)}</sup></mark>`;
cursor = f.end;
}
if (cursor < text.length) {
html += `<span class="text-[#1f2933]">${esc(text.slice(cursor))}</span>`;
}
return html;
}
// ── Prompt Bersih (hapus temuan dari teks) ────────────────────────────────────
function buildClean(text, findings) {
if (!findings.length) return text;
const sorted = [...findings].sort((a, b) => a.start - b.start);
let result = "", cursor = 0;
for (const f of sorted) {
if (f.start < cursor) continue;
result += text.slice(cursor, f.start);
cursor = f.end;
}
result += text.slice(cursor);
// Bersihkan spasi dan tanda baca ganda yang tersisa
return result
.replace(/^[\s,!.]+/, "") // hapus tanda baca di awal
.replace(/\n{3,}/g, "\n\n") // max 2 baris kosong berturutan
.replace(/[ \t]{2,}/g, " ") // hapus spasi ganda
.trim();
}
// ── Statistik Token ───────────────────────────────────────────────────────────
function updateStats(text, findings) {
const totalChars = text.length;
const fillerChars = findings.reduce((sum, f) => sum + (f.end - f.start), 0);
const savedPct = totalChars > 0 ? Math.round((fillerChars / totalChars) * 100) : 0;
$("statTotal").textContent = totalChars.toLocaleString("id");
$("statFiller").textContent = fillerChars.toLocaleString("id");
$("statTokens").textContent = estTokens(fillerChars).toLocaleString("id");
$("statSaved").textContent = savedPct;
$("tokenStats").hidden = false;
}
// ── Daftar Temuan ─────────────────────────────────────────────────────────────
function buildList(findings) {
$("findingList").innerHTML = "";
if (!findings.length) {
$("noFindings").classList.remove("hidden");
$("listSummary").textContent = "";
return;
}
$("noFindings").classList.add("hidden");
// Hitung per kategori
const counts = {};
findings.forEach(f => { counts[f.category] = (counts[f.category] || 0) + 1; });
const catSummary = Object.entries(counts)
.map(([cat, n]) => `${n}Γ— ${catStyle(cat).label}`)
.join(", ");
$("listSummary").textContent =
`${findings.length} temuan: ${catSummary}.`;
findings.forEach(f => {
const s = catStyle(f.category);
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;line-height:1.6;
background:${s.bg};color:${s.color};border:1px solid ${s.border};
margin-top:1px;white-space:nowrap;">${esc(s.label)}</span>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold text-[#1f2933] mb-1 overflow-hidden text-ellipsis whitespace-nowrap"
title="${esc(f.word)}">"${esc(f.word)}"</div>
<div class="text-xs text-[#52606d] leading-[1.55]">${esc(f.reason)}</div>
</div>
<div class="flex-shrink-0 text-right">
<div class="text-xs font-bold tabular-nums" style="color:${s.color}">
${Math.round(f.confidence * 100)}%
</div>
<div class="text-[10px] text-[#52606d] tabular-nums mt-0.5">
~${estTokens(f.end - f.start)} token
</div>
</div>`;
$("findingList").appendChild(card);
});
}
// ── Status Server ─────────────────────────────────────────────────────────────
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.pattern_count ?? "?"} pola`;
$("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);
}
}
// ── Pemeriksaan Utama ─────────────────────────────────────────────────────────
let _lastFindings = [];
let _lastText = "";
async function check() {
const text = $("inputText").value.trim();
if (!text) return;
$("checkBtn").disabled = true;
$("checkBtn").textContent = "Memeriksa…";
$("errorBox").hidden = true;
$("tokenStats").hidden = true;
try {
const res = await fetch(API + "/api/filler", {
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 || [];
_lastFindings = findings;
_lastText = text;
$("highlightBox").innerHTML = buildHighlight(text, findings);
buildList(findings);
updateStats(text, findings);
const clean = buildClean(text, findings);
$("cleanBox").textContent = clean || "(teks kosong setelah semua filler dihapus)";
} catch (err) {
$("errorBox").textContent = `Gagal: ${err.message}`;
$("errorBox").hidden = false;
} finally {
$("checkBtn").disabled = false;
$("checkBtn").textContent = "Periksa";
checkStatus();
}
}
// ── Event Listeners ───────────────────────────────────────────────────────────
$("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 = "";
_lastFindings = [];
_lastText = "";
$("highlightBox").innerHTML =
'<span class="text-[#52606d] text-sm italic">Hasil akan muncul di sini setelah Anda klik Periksa.</span>';
$("cleanBox").textContent = "";
$("cleanBox").innerHTML =
'<span class="text-[#52606d] text-sm italic">Prompt bersih akan muncul di sini.</span>';
$("findingList").innerHTML = "";
$("listSummary").textContent = "";
$("noFindings").classList.add("hidden");
$("errorBox").hidden = true;
$("tokenStats").hidden = true;
});
$("copyCleanBtn").addEventListener("click", () => {
const text = $("cleanBox").textContent.trim();
if (!text || text.startsWith("Prompt bersih")) return;
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(text).then(() => {
const btn = $("copyCleanBtn");
const orig = btn.textContent;
btn.textContent = "Tersalin!";
setTimeout(() => { btn.textContent = orig; }, 2000);
});
}
});
// ── Init ──────────────────────────────────────────────────────────────────────
checkStatus();
$("tab-highlight").classList.add("flex");
</script>
</body>
</html>