feat: 프론트에 AI 생성 의심도 표시
This commit is contained in:
parent
725e44e999
commit
f4461f39c7
@ -92,6 +92,24 @@
|
|||||||
.scorebar-row .label { color: var(--muted); }
|
.scorebar-row .label { color: var(--muted); }
|
||||||
.scorebar-row .value { text-align: right; font-variant-numeric: tabular-nums; }
|
.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 {
|
.match-card {
|
||||||
background: var(--panel-2); border: 1px solid var(--border); border-radius: 6px;
|
background: var(--panel-2); border: 1px solid var(--border); border-radius: 6px;
|
||||||
padding: 14px; margin-top: 10px;
|
padding: 14px; margin-top: 10px;
|
||||||
@ -281,6 +299,9 @@
|
|||||||
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">점수 분석 (삼중 유사도 결합)</h3>
|
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">점수 분석 (삼중 유사도 결합)</h3>
|
||||||
<div id="score-breakdown"></div>
|
<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>
|
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">매칭된 레퍼런스</h3>
|
||||||
<div id="matches"></div>
|
<div id="matches"></div>
|
||||||
|
|
||||||
@ -468,18 +489,21 @@ async function runDetect() {
|
|||||||
|
|
||||||
function renderResult(data, originalText) {
|
function renderResult(data, originalText) {
|
||||||
const verdict = document.getElementById("verdict");
|
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) {
|
if (data.is_infringement) {
|
||||||
verdict.className = "verdict infringement";
|
verdict.className = "verdict infringement";
|
||||||
verdict.innerHTML = `
|
verdict.innerHTML = `
|
||||||
<h3>⚠ 저작권 침해 가능성 확인</h3>
|
<h3>⚠ 저작권 침해 가능성 확인</h3>
|
||||||
<div class="conf-line">결합 유사도</div>
|
<div class="conf-line">보정 유사도</div>
|
||||||
<div class="conf-number">${(data.confidence * 100).toFixed(2)}%</div>`;
|
<div class="conf-number">${displaySimilarity}%</div>`;
|
||||||
} else {
|
} else {
|
||||||
verdict.className = "verdict clean";
|
verdict.className = "verdict clean";
|
||||||
verdict.innerHTML = `
|
verdict.innerHTML = `
|
||||||
<h3>✓ 침해 신호 없음</h3>
|
<h3>✓ 침해 신호 없음</h3>
|
||||||
<div class="conf-line">최상위 유사도</div>
|
<div class="conf-line">보정 유사도 · 검색 후보 점수는 침해 확률이 아님</div>
|
||||||
<div class="conf-number">${(data.confidence * 100).toFixed(2)}%</div>`;
|
<div class="conf-number">${displaySimilarity}%</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("result-body").style.display = "block";
|
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>';
|
breakdownEl.innerHTML = '<div style="color: var(--muted); font-size: 12px;">매칭된 레퍼런스가 없어 점수 분석을 표시할 수 없습니다.</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderAiGeneration(data.ai_generation, originalText);
|
||||||
|
|
||||||
// 매칭 카드
|
// 매칭 카드
|
||||||
const matchesEl = document.getElementById("matches");
|
const matchesEl = document.getElementById("matches");
|
||||||
if (!data.matches || data.matches.length === 0) {
|
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);
|
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) {
|
function renderEvidence(originalText, spans) {
|
||||||
if (!spans || spans.length === 0) return "";
|
if (!spans || spans.length === 0) return "";
|
||||||
// span 정렬 후 마킹
|
// 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