From f4461f39c7e44551cf0991eddd646a8c24db1f33 Mon Sep 17 00:00:00 2001 From: hbyang Date: Tue, 11 Aug 2026 09:02:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?AI=20=EC=83=9D=EC=84=B1=20=EC=9D=98=EC=8B=AC=EB=8F=84=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/index.html | 96 +++++++++++++++++++++++++++++++++++++++-- tests/test_static_ui.py | 24 +++++++++++ 2 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 tests/test_static_ui.py diff --git a/app/static/index.html b/app/static/index.html index 1f4fb3e..eade67a 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -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 @@

점수 분석 (삼중 유사도 결합)

+

AI 생성 의심도

+
+

매칭된 레퍼런스

@@ -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 = `

⚠ 저작권 침해 가능성 확인

-
결합 유사도
-
${(data.confidence * 100).toFixed(2)}%
`; +
보정 유사도
+
${displaySimilarity}%
`; } else { verdict.className = "verdict clean"; verdict.innerHTML = `

✓ 침해 신호 없음

-
최상위 유사도
-
${(data.confidence * 100).toFixed(2)}%
`; +
보정 유사도 · 검색 후보 점수는 침해 확률이 아님
+
${displaySimilarity}%
`; } document.getElementById("result-body").style.display = "block"; @@ -497,6 +521,8 @@ function renderResult(data, originalText) { breakdownEl.innerHTML = '
매칭된 레퍼런스가 없어 점수 분석을 표시할 수 없습니다.
'; } + 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 = ` +
+
+ 판정 불가 · 모델 준비 전 + ${escapeHtml(version)} +
+
${escapeHtml(note)}
+ ${renderAiWarnings(ai && ai.warnings)} +
`; + 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 + ? '미검증 휴리스틱' + : `${escapeHtml(ai.model_version || "model")}`; + 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 `
+
구간 ${start}–${end}자 · 의심도 ${(s.score * 100).toFixed(1)}%
+ ${escapeHtml(snippet.length > 160 ? snippet.slice(0, 160) + "…" : snippet)} +
`; + }).join(""); + + el.innerHTML = ` +
+
+
AI 생성 의심도 ${labels[level]}${stub}
+ ${score} +
+
검토 우선순위 신호이며 AI 작성 여부를 확정하지 않습니다.${provenance}
+ ${ai.note ? `
${escapeHtml(ai.note)}
` : ""} + ${renderAiWarnings(ai.warnings)} + ${segments} +
`; +} + +function renderAiWarnings(warnings) { + if (!warnings || warnings.length === 0) return ""; + return warnings.map(w => `
⚠ ${escapeHtml(w)}
`).join(""); +} + function renderEvidence(originalText, spans) { if (!spans || spans.length === 0) return ""; // span 정렬 후 마킹 diff --git a/tests/test_static_ui.py b/tests/test_static_ui.py new file mode 100644 index 0000000..541809b --- /dev/null +++ b/tests/test_static_ui.py @@ -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