Spaces:
Running
Running
| <html lang="id"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Syntax Detector 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> | |
| mark.flag { | |
| background: #fff3bf; | |
| border-bottom: 2px solid #f59f00; | |
| border-radius: 2px; | |
| padding: 0 1px; | |
| } | |
| </style> | |
| </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">Syntax Detector Test</h1> | |
| <p class="mt-1 text-sm text-[#52606d] max-w-xl"> | |
| Deteksi urutan kata janggal per kalimat memakai pseudo-log-likelihood IndoBERT. | |
| Kalimat yang susunan katanya jauh lebih tidak wajar daripada permutasi acaknya | |
| ditandai sebagai UNUSUAL_WORD_ORDER (severity LOW). | |
| </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 beberapa kalimat untuk diperiksa urutan katanya. Contoh: Laporan keuangan perusahaan untuk membuat ingin saya."></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> | |
| <!-- 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">Catatan:</span> | |
| Deteksi ini hanya menyorot kalimat dengan urutan kata yang tidak wajar β sifatnya saran, | |
| bukan koreksi otomatis. Detektor butuh model IndoBERT aktif; bila model nonaktif | |
| (mode <code>--no-syntax-ml</code> / <code>--fast</code>), tidak ada temuan yang dihasilkan. | |
| </div> | |
| </section> | |
| <!-- ββ Panel Kanan β Output ββ --> | |
| <section class="bg-white border border-line rounded-lg p-5 flex flex-col gap-4 min-h-[400px]"> | |
| <div class="flex items-center justify-between gap-2"> | |
| <h2 class="m-0 text-sm font-bold">Kalimat Janggal</h2> | |
| <span id="findingCount" | |
| class="px-2 py-1 bg-slate-100 text-slate-700 text-xs font-bold rounded-full"> | |
| 0 temuan | |
| </span> | |
| </div> | |
| <!-- Teks dengan sorotan --> | |
| <div id="highlightBox" | |
| class="hidden text-sm leading-[1.7] p-3 bg-[#f8f9fa] border border-line rounded-[6px] whitespace-pre-wrap"> | |
| </div> | |
| <div id="findingsPanel" | |
| class="flex-1 overflow-y-auto space-y-3"> | |
| <p class="text-sm text-[#52606d] italic">Periksa teks di sebelah kiri untuk melihat temuanβ¦</p> | |
| </div> | |
| </section> | |
| </div> | |
| </main> | |
| <script> | |
| const API_URL = "http://127.0.0.1:8008/api/syntax"; | |
| const STATUS_URL = "http://127.0.0.1:8008/api/status"; | |
| const $ = id => document.getElementById(id); | |
| const ACCENT = "#f59f00"; | |
| const SAMPLES = [ | |
| "Saya ingin membuat laporan keuangan untuk perusahaan.", | |
| "Laporan keuangan perusahaan untuk membuat ingin saya.", | |
| "Tolong buatkan ringkasan singkat dari artikel berita ini.", | |
| "Ini berita artikel dari singkat ringkasan buatkan tolong.", | |
| "Jelaskan langkah-langkah membuat kopi susu yang enak.", | |
| "Enak yang susu kopi membuat langkah-langkah jelaskan.", | |
| ]; | |
| const escapeHtml = s => s.replace(/[&<>"']/g, c => | |
| ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); | |
| let lastFindings = []; | |
| let lastText = ""; | |
| // ββ Server status check ββ | |
| async function checkServer() { | |
| try { | |
| const res = await fetch(STATUS_URL, { signal: AbortSignal.timeout(3000) }); | |
| if (res.ok) { | |
| const data = await res.json().catch(() => ({})); | |
| const mlOn = data.ml_active !== false; | |
| $("statusDot").className = `w-2 h-2 rounded-full ${mlOn ? "bg-emerald-500" : "bg-amber-500"}`; | |
| $("statusText").textContent = mlOn ? "Server siap (model aktif)" : "Server siap (model nonaktif)"; | |
| $("statusBadge").className = | |
| "inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm font-semibold " + | |
| (mlOn | |
| ? "bg-emerald-50 text-emerald-700 border border-emerald-200" | |
| : "bg-amber-50 text-amber-700 border border-amber-200"); | |
| return true; | |
| } | |
| } catch (e) {} | |
| $("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"; | |
| return false; | |
| } | |
| // ββ Check text ββ | |
| async function check() { | |
| const text = $("inputText").value; | |
| if (!text.trim()) { | |
| alert("Masukkan teks terlebih dahulu"); | |
| return; | |
| } | |
| try { | |
| const res = await fetch(API_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ text }), | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| lastFindings = data.findings || []; | |
| lastText = text; | |
| renderFindings(); | |
| } catch (e) { | |
| alert("Error: " + e.message); | |
| } | |
| } | |
| // ββ Bangun teks bersorot dari offset temuan ββ | |
| function renderHighlight() { | |
| const box = $("highlightBox"); | |
| if (!lastText || !lastFindings.length) { | |
| box.classList.add("hidden"); | |
| box.innerHTML = ""; | |
| return; | |
| } | |
| const spans = [...lastFindings] | |
| .filter(f => Number.isInteger(f.start) && Number.isInteger(f.end) && f.end > f.start) | |
| .sort((a, b) => a.start - b.start); | |
| let html = "", cursor = 0; | |
| for (const f of spans) { | |
| if (f.start < cursor) continue; // lewati tumpang-tindih | |
| html += escapeHtml(lastText.slice(cursor, f.start)); | |
| html += `<mark class="flag">${escapeHtml(lastText.slice(f.start, f.end))}</mark>`; | |
| cursor = f.end; | |
| } | |
| html += escapeHtml(lastText.slice(cursor)); | |
| box.innerHTML = html; | |
| box.classList.remove("hidden"); | |
| } | |
| // ββ Render findings ββ | |
| function renderFindings() { | |
| renderHighlight(); | |
| const panel = $("findingsPanel"); | |
| if (!lastFindings.length) { | |
| panel.innerHTML = `<p class="text-sm text-[#52606d] italic">(Tidak ada kalimat janggal terdeteksi) β</p>`; | |
| $("findingCount").textContent = "0 temuan"; | |
| return; | |
| } | |
| $("findingCount").textContent = lastFindings.length + " temuan"; | |
| panel.innerHTML = lastFindings.map(f => ` | |
| <div class="p-3 rounded-[6px] border-l-4" style="background:#f8f9fa;border-left-color:${ACCENT}"> | |
| <div class="flex items-start justify-between gap-2"> | |
| <div class="flex-1 min-w-0"> | |
| <div class="font-bold text-sm" style="color:${ACCENT}">UNUSUAL_WORD_ORDER</div> | |
| <div class="text-sm text-[#1f2933] mt-1 leading-[1.5] italic">"${escapeHtml(f.sentence)}"</div> | |
| <div class="text-xs text-[#52606d] mt-2 leading-[1.5]">${escapeHtml(f.reason || "")}</div> | |
| <div class="mt-2 text-xs font-mono bg-white p-2 rounded border border-line overflow-auto"> | |
| <span class="text-[#666]">Offset:</span> ${f.start}β${f.end} | |
| <span class="text-[#666] ml-3">PLL:</span> ${f.score} | |
| <span class="text-[#666] ml-3">Confidence:</span> ${(f.confidence * 100).toFixed(0)}% | |
| </div> | |
| </div> | |
| <span class="px-2 py-1 text-xs font-bold rounded flex-shrink-0" style="background:${ACCENT};color:#fff"> | |
| LOW | |
| </span> | |
| </div> | |
| </div> | |
| `).join(""); | |
| } | |
| // ββ Sample text ββ | |
| function loadSample() { | |
| $("inputText").value = SAMPLES[Math.floor(Math.random() * SAMPLES.length)]; | |
| check(); | |
| } | |
| // ββ Events ββ | |
| $("checkBtn").addEventListener("click", check); | |
| $("sampleBtn").addEventListener("click", loadSample); | |
| $("clearBtn").addEventListener("click", () => { | |
| $("inputText").value = ""; | |
| lastFindings = []; | |
| lastText = ""; | |
| renderFindings(); | |
| }); | |
| // ββ Init: retry server check setiap 2 detik sampai siap ββ | |
| (async () => { | |
| while (true) { | |
| const ok = await checkServer(); | |
| if (ok) break; | |
| await new Promise(resolve => setTimeout(resolve, 2000)); | |
| } | |
| renderFindings(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |