rmdetect / static /app.js
reasonofmoon's picture
Upload folder using huggingface_hub
5a35b0b verified
Raw
History Blame Contribute Delete
5.85 kB
"use strict";
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();