feat: 프론트에 AI 생성 의심도 표시
This commit is contained in:
parent
725e44e999
commit
f4461f39c7
@ -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
24
tests/test_static_ui.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user