Spaces:
Running
Running
| ; | |
| const $ = (id) => document.getElementById(id); | |
| const GAUGE_CIRC = 2 * Math.PI * 52; // r=52 | |
| const SAMPLE = `작년 여름, 인턴 때 갑자기 프로젝트가 뒤집혔어요. 진짜 멘붕이었죠 ㅋㅋ | |
| 이에 따라 체계적인 대안을 수립하고, 이해관계자들과의 원활한 소통을 통해 종합적인 해결 방안을 도출하였으며, 최종적으로 우수한 성과를 달성하였습니다. | |
| 결국 마감 하루 전에 완성했는데, 팀장님이 '너 진짜 미쳤다' 하면서 웃었음 ㅋㅋ 그때 그 표정 아직도 기억남.`; | |
| function colorFor(p) { | |
| // green (human) -> yellow (mixed) -> red (AI) | |
| if (p >= 0.6) return "var(--ai)"; | |
| if (p >= 0.4) return "var(--mixed)"; | |
| return "var(--human)"; | |
| } | |
| function labelKo(v) { | |
| return { "AI-generated": "AI 생성", "Likely AI": "AI 의심", | |
| "Mixed": "혼합", "Likely human": "인간 추정", | |
| "Human-written": "인간 작성" }[v] || v; | |
| } | |
| // human-readable feature names | |
| const FEAT_KO = { | |
| burstiness: "문장 길이 변동성", cv_sent_len: "문장 길이 편차", | |
| mean_sent_len: "평균 문장 길이", connective_per100: "논리 접속어 밀도", | |
| informal_per100: "구어체 표현", ko_formal_per100: "격식체 어미", | |
| ko_casual_per100: "구어체 어미", ellipsis_per100: "말줄임표", | |
| quote_per100: "인용부호", excl_per100: "느낌표", ques_per100: "물음표", | |
| emoji_per100: "이모지", punct_variety: "문장부호 다양성", | |
| n_sents: "문장 수", avg_word_len: "평균 단어 길이", | |
| type_token_ratio: "어휘 다양성", rep_bigram_rate: "반복 표현", | |
| word_entropy: "어휘 엔트로피", comma_per100: "쉼표", | |
| ppl: "복잡도(perplexity)", log_ppl: "복잡도", topk_hit_rate: "예측 적중률", | |
| std_logprob: "예측 변동성", mean_logprob: "평균 예측도", median_logprob: "예측도(중앙)", | |
| }; | |
| const featName = (f) => FEAT_KO[f] || f; | |
| function setGauge(p) { | |
| const fg = $("gaugeFg"); | |
| fg.style.strokeDashoffset = String(GAUGE_CIRC * (1 - p)); | |
| fg.style.stroke = colorFor(p); | |
| $("gaugeNum").textContent = Math.round(p * 100) + "%"; | |
| } | |
| function render(res) { | |
| $("resultPane").hidden = false; | |
| $("error").hidden = true; | |
| setGauge(res.overall_ai_probability); | |
| $("verdict").textContent = `${labelKo(res.verdict)} · ${res.verdict}`; | |
| $("verdict").style.color = colorFor(res.overall_ai_probability); | |
| $("meta").textContent = | |
| `언어 ${res.language.toUpperCase()} · 문단 ${res.n_paragraphs}개 · ` + | |
| `의심 문단 ${res.n_flagged}개 · ${res.elapsed_ms} ms`; | |
| // document-level top signals | |
| const ds = $("docSignals"); ds.innerHTML = ""; | |
| res.top_features.slice(0, 5).forEach((f) => { | |
| const s = document.createElement("span"); | |
| s.className = "signal " + (f.direction === "AI" ? "ai" : "human"); | |
| s.textContent = `${featName(f.feature)} ${f.direction === "AI" ? "▲AI" : "▼인간"}`; | |
| ds.appendChild(s); | |
| }); | |
| // paragraphs | |
| const box = $("paragraphs"); box.innerHTML = ""; | |
| res.paragraphs.forEach((p) => { | |
| const div = document.createElement("div"); | |
| div.className = "para" + (p.low_confidence ? " lowconf" : ""); | |
| div.style.borderLeftColor = colorFor(p.ai_probability); | |
| const txt = document.createElement("div"); | |
| txt.className = "ptext"; | |
| txt.textContent = p.text; | |
| div.appendChild(txt); | |
| const bar = document.createElement("div"); | |
| bar.className = "pbar"; | |
| const lc = p.low_confidence ? " (짧아 신뢰도 낮음)" : ""; | |
| bar.innerHTML = `<span>문단 ${p.index + 1} · ${p.language.toUpperCase()}${lc}</span>` + | |
| `<span class="pscore" style="color:${colorFor(p.ai_probability)}">` + | |
| `AI ${Math.round(p.ai_probability * 100)}% · ${labelKo(p.verdict)}</span>`; | |
| div.appendChild(bar); | |
| const det = document.createElement("div"); | |
| det.className = "para-details"; | |
| det.innerHTML = "<b>주요 신호:</b> " + p.top_features.slice(0, 4).map((f) => | |
| `<span class="feat"><b>${featName(f.feature)}</b> ` + | |
| `${f.direction === "AI" ? "▲AI" : "▼인간"} (${f.contribution > 0 ? "+" : ""}${f.contribution})</span>` | |
| ).join(""); | |
| div.appendChild(det); | |
| div.addEventListener("click", () => div.classList.toggle("open")); | |
| box.appendChild(div); | |
| }); | |
| $("resultPane").scrollIntoView({ behavior: "smooth", block: "start" }); | |
| } | |
| async function analyze() { | |
| const text = $("text").value.trim(); | |
| if (!text) { showError("텍스트를 입력하세요."); return; } | |
| const btn = $("analyzeBtn"); | |
| btn.disabled = true; | |
| const orig = btn.innerHTML; | |
| btn.innerHTML = '<span class="spinner"></span>분석 중…'; | |
| try { | |
| const lang = $("lang").value || null; | |
| const r = await fetch("/detect", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ text, lang }), | |
| }); | |
| if (!r.ok) { | |
| const e = await r.json().catch(() => ({ detail: r.statusText })); | |
| throw new Error(e.detail || `HTTP ${r.status}`); | |
| } | |
| render(await r.json()); | |
| } catch (err) { | |
| showError("분석 실패: " + err.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = orig; | |
| } | |
| } | |
| function showError(msg) { | |
| const e = $("error"); | |
| e.textContent = msg; | |
| e.hidden = false; | |
| $("resultPane").hidden = true; | |
| } | |
| function updateCount() { | |
| $("charcount").textContent = $("text").value.length.toLocaleString() + " 자"; | |
| } | |
| $("analyzeBtn").addEventListener("click", analyze); | |
| $("sampleBtn").addEventListener("click", () => { $("text").value = SAMPLE; updateCount(); }); | |
| $("clearBtn").addEventListener("click", () => { $("text").value = ""; updateCount(); $("resultPane").hidden = true; }); | |
| $("text").addEventListener("input", updateCount); | |
| document.addEventListener("keydown", (e) => { | |
| if ((e.metaKey || e.ctrlKey) && e.key === "Enter") analyze(); | |
| }); | |
| updateCount(); | |