feat: 프론트에 AI 생성 의심도 표시

This commit is contained in:
hbyang 2026-08-11 09:02:04 +09:00
parent 725e44e999
commit f4461f39c7
2 changed files with 116 additions and 4 deletions

View File

@ -92,6 +92,24 @@
.scorebar-row .label { color: var(--muted); }
.scorebar-row .value { text-align: right; font-variant-numeric: tabular-nums; }
.ai-card {
background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
padding: 14px; margin-top: 8px;
}
.ai-card.ai-high { border-color: var(--danger); background: rgba(248, 81, 73, 0.08); }
.ai-card.ai-medium { border-color: var(--warning); background: rgba(210, 153, 34, 0.08); }
.ai-card.ai-low { border-color: var(--success); background: rgba(63, 185, 80, 0.08); }
.ai-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.ai-title { font-size: 14px; font-weight: 600; }
.ai-score { font-size: 24px; font-weight: 700; font-variant-numeric: tabular-nums; }
.ai-note { color: var(--muted); font-size: 11px; margin-top: 8px; line-height: 1.6; }
.ai-warning { color: var(--warning); font-size: 11px; margin-top: 6px; }
.ai-segment {
margin-top: 8px; padding: 8px 10px; background: var(--bg); border-radius: 4px;
font-size: 11px; border-left: 3px solid var(--warning);
}
.ai-segment .meta { color: var(--muted); margin-bottom: 3px; }
.match-card {
background: var(--panel-2); border: 1px solid var(--border); border-radius: 6px;
padding: 14px; margin-top: 10px;
@ -281,6 +299,9 @@
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">점수 분석 (삼중 유사도 결합)</h3>
<div id="score-breakdown"></div>
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">AI 생성 의심도</h3>
<div id="ai-generation"></div>
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">매칭된 레퍼런스</h3>
<div id="matches"></div>
@ -468,18 +489,21 @@ async function runDetect() {
function renderResult(data, originalText) {
const verdict = document.getElementById("verdict");
const displaySimilarity = data.review_summary && Number.isFinite(data.review_summary.similarity_percent)
? data.review_summary.similarity_percent
: Math.round(data.confidence * 100);
if (data.is_infringement) {
verdict.className = "verdict infringement";
verdict.innerHTML = `
<h3>⚠ 저작권 침해 가능성 확인</h3>
<div class="conf-line">결합 유사도</div>
<div class="conf-number">${(data.confidence * 100).toFixed(2)}%</div>`;
<div class="conf-line">보정 유사도</div>
<div class="conf-number">${displaySimilarity}%</div>`;
} else {
verdict.className = "verdict clean";
verdict.innerHTML = `
<h3>✓ 침해 신호 없음</h3>
<div class="conf-line">최상위 유사도</div>
<div class="conf-number">${(data.confidence * 100).toFixed(2)}%</div>`;
<div class="conf-line">보정 유사도 · 검색 후보 점수는 침해 확률이 아님</div>
<div class="conf-number">${displaySimilarity}%</div>`;
}
document.getElementById("result-body").style.display = "block";
@ -497,6 +521,8 @@ function renderResult(data, originalText) {
breakdownEl.innerHTML = '<div style="color: var(--muted); font-size: 12px;">매칭된 레퍼런스가 없어 점수 분석을 표시할 수 없습니다.</div>';
}
renderAiGeneration(data.ai_generation, originalText);
// 매칭 카드
const matchesEl = document.getElementById("matches");
if (!data.matches || data.matches.length === 0) {
@ -541,6 +567,68 @@ function renderResult(data, originalText) {
document.getElementById("raw-json").textContent = JSON.stringify(data, null, 2);
}
function renderAiGeneration(ai, originalText) {
const el = document.getElementById("ai-generation");
if (!ai || !ai.available) {
const note = ai && ai.note
? ai.note
: "학습된 AI 생성 탐지 모델이 없어 의심도를 산출하지 않습니다.";
const version = ai && ai.model_version ? ai.model_version : "unavailable";
el.innerHTML = `
<div class="ai-card">
<div class="ai-head">
<span class="ai-title">판정 불가 · 모델 준비 전</span>
<span class="type-badge type-unknown">${escapeHtml(version)}</span>
</div>
<div class="ai-note">${escapeHtml(note)}</div>
${renderAiWarnings(ai && ai.warnings)}
</div>`;
return;
}
const level = ["low", "medium", "high"].includes(ai.suspicion_level)
? ai.suspicion_level
: "unknown";
const labels = { low: "낮음", medium: "중간", high: "높음", unknown: "판정 불가" };
const score = Number.isFinite(ai.score) ? `${(ai.score * 100).toFixed(1)}%` : "—";
const stub = ai.is_stub
? '<span class="type-badge type-plot">미검증 휴리스틱</span>'
: `<span class="type-badge type-unknown">${escapeHtml(ai.model_version || "model")}</span>`;
const provenance = ai.provenance && ai.provenance !== "unknown"
? ` · 추정 유형 ${escapeHtml(ai.provenance)}`
: "";
const segments = (ai.segments || [])
.filter(s => s.scored && Number.isFinite(s.score))
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(s => {
const start = Math.max(0, Number(s.start) || 0);
const end = Math.max(start, Number(s.end) || start);
const snippet = originalText.substring(start, end).trim();
return `<div class="ai-segment">
<div class="meta">구간 ${start}${end}자 · 의심도 ${(s.score * 100).toFixed(1)}%</div>
${escapeHtml(snippet.length > 160 ? snippet.slice(0, 160) + "…" : snippet)}
</div>`;
}).join("");
el.innerHTML = `
<div class="ai-card ai-${level}">
<div class="ai-head">
<div><span class="ai-title">AI 생성 의심도 ${labels[level]}</span>${stub}</div>
<span class="ai-score">${score}</span>
</div>
<div class="ai-note">검토 우선순위 신호이며 AI 작성 여부를 확정하지 않습니다.${provenance}</div>
${ai.note ? `<div class="ai-note">${escapeHtml(ai.note)}</div>` : ""}
${renderAiWarnings(ai.warnings)}
${segments}
</div>`;
}
function renderAiWarnings(warnings) {
if (!warnings || warnings.length === 0) return "";
return warnings.map(w => `<div class="ai-warning">⚠ ${escapeHtml(w)}</div>`).join("");
}
function renderEvidence(originalText, spans) {
if (!spans || spans.length === 0) return "";
// span 정렬 후 마킹

24
tests/test_static_ui.py Normal file
View File

@ -0,0 +1,24 @@
from pathlib import Path
HTML = (Path(__file__).resolve().parents[1] / "app" / "static" / "index.html").read_text(
encoding="utf-8"
)
def test_ai_generation_result_has_visible_frontend_section():
assert 'id="ai-generation"' in HTML
assert "renderAiGeneration(data.ai_generation, originalText)" in HTML
assert "판정 불가 · 모델 준비 전" in HTML
assert "AI 생성 의심도" in HTML
def test_ai_generation_ui_does_not_present_score_as_a_certain_verdict():
assert "AI 작성 여부를 확정하지 않습니다" in HTML
assert "미검증 휴리스틱" in HTML
assert "ai.available" in HTML
def test_clean_verdict_uses_calibrated_review_summary_percentage():
assert "data.review_summary.similarity_percent" in HTML
assert "검색 후보 점수는 침해 확률이 아님" in HTML