参考文献作者堆叠
参考文献相关性检测 作者ai写作辅助检测工作
This commit is contained in:
849
application/api/view/aiTools/index.html
Normal file
849
application/api/view/aiTools/index.html
Normal file
@@ -0,0 +1,849 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>AI 辅助写作风险检测</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=Fraunces:opsz,wght@9..144,500;9..144,650&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--ink: #14212b;
|
||||
--muted: #5c6b76;
|
||||
--line: #d7e0e6;
|
||||
--paper: #f3f6f8;
|
||||
--panel: #ffffff;
|
||||
--accent: #0f766e;
|
||||
--accent-soft: #d9f3ef;
|
||||
--warn: #b45309;
|
||||
--danger: #b91c1c;
|
||||
--ok: #047857;
|
||||
--mid: #a16207;
|
||||
--high: #c2410c;
|
||||
--vhigh: #9f1239;
|
||||
--shadow: 0 1px 0 rgba(20, 33, 43, 0.04);
|
||||
--radius: 14px;
|
||||
--font: "DM Sans", system-ui, sans-serif;
|
||||
--display: "Fraunces", Georgia, serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; min-height: 100%; }
|
||||
body {
|
||||
font-family: var(--font);
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(1200px 500px at 10% -10%, #dff5f1 0%, transparent 55%),
|
||||
radial-gradient(900px 420px at 100% 0%, #e8eef3 0%, transparent 50%),
|
||||
var(--paper);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
width: min(1120px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 36px 0 64px;
|
||||
}
|
||||
|
||||
header.hero {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.brand {
|
||||
font-family: var(--display);
|
||||
font-size: clamp(1.8rem, 3.2vw, 2.6rem);
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
}
|
||||
.lead {
|
||||
margin: 0;
|
||||
max-width: 46rem;
|
||||
color: var(--muted);
|
||||
font-size: 1.02rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.start-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.start-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input[type="text"], input[type="url"], input[type="number"] {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font: inherit;
|
||||
color: var(--ink);
|
||||
background: #fff;
|
||||
outline: none;
|
||||
}
|
||||
input:focus {
|
||||
border-color: #7bbbb4;
|
||||
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.12);
|
||||
}
|
||||
|
||||
.btn {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
padding: 12px 18px;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform .12s ease, opacity .12s ease;
|
||||
}
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
min-width: 132px;
|
||||
}
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
border: 1px solid #b7ddd8;
|
||||
}
|
||||
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 18px;
|
||||
margin-top: 14px;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.meta-row strong { color: var(--ink); font-weight: 600; }
|
||||
|
||||
.progress-block { margin-top: 18px; display: none; }
|
||||
.progress-block.show { display: block; }
|
||||
.progress-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.bar {
|
||||
height: 12px;
|
||||
border-radius: 999px;
|
||||
background: #e7eef2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bar > i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #0f766e, #14968c);
|
||||
transition: width .35s ease;
|
||||
}
|
||||
.step-text { color: var(--muted); font-size: 0.88rem; margin-top: 8px; }
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
background: #eef3f6;
|
||||
color: var(--muted);
|
||||
}
|
||||
.status-pill.running { background: #e7f6f3; color: var(--accent); }
|
||||
.status-pill.done { background: #e7f7ef; color: var(--ok); }
|
||||
.status-pill.failed { background: #fdecec; color: var(--danger); }
|
||||
|
||||
.results { margin-top: 22px; display: none; }
|
||||
.results.show { display: block; }
|
||||
|
||||
body.mode-progress .start-panel-extra { opacity: .85; }
|
||||
body.mode-result .progress-block { margin-bottom: 8px; }
|
||||
body.mode-progress #results { display: none !important; }
|
||||
body.mode-result #results { display: block; }
|
||||
|
||||
.score-board {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.score-board { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.gauge {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
.gauge svg { width: 100%; height: 100%; transform: rotate(-90deg); }
|
||||
.gauge .track { fill: none; stroke: #e7eef2; stroke-width: 12; }
|
||||
.gauge .value {
|
||||
fill: none;
|
||||
stroke: var(--accent);
|
||||
stroke-width: 12;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 502.4;
|
||||
stroke-dashoffset: 502.4;
|
||||
transition: stroke-dashoffset .6s ease, stroke .3s ease;
|
||||
}
|
||||
.gauge-center {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.gauge-center .num {
|
||||
font-family: var(--display);
|
||||
font-size: 2.4rem;
|
||||
font-weight: 650;
|
||||
line-height: 1;
|
||||
}
|
||||
.gauge-center .lvl {
|
||||
margin-top: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.kv {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.kv .item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #fafcfd;
|
||||
}
|
||||
.kv .item .k { font-size: 0.78rem; color: var(--muted); }
|
||||
.kv .item .v { font-size: 1.05rem; font-weight: 650; margin-top: 2px; }
|
||||
|
||||
.section-title {
|
||||
font-family: var(--display);
|
||||
font-size: 1.25rem;
|
||||
margin: 26px 0 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.dims {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.dims { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
.dim {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.dim .name { font-size: 0.8rem; color: var(--muted); margin-bottom: 8px; }
|
||||
.dim .score { font-weight: 700; font-size: 1.15rem; }
|
||||
.dim .mini {
|
||||
margin-top: 8px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #e7eef2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dim .mini > i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.sections {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.sections { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.sections { grid-template-columns: 1fr; }
|
||||
}
|
||||
.sec {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
}
|
||||
.sec h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.95rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.sec .row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.sec .row b { color: var(--ink); }
|
||||
|
||||
.advice, .warnings {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.chip {
|
||||
border-left: 3px solid var(--accent);
|
||||
background: var(--accent-soft);
|
||||
padding: 10px 12px;
|
||||
border-radius: 0 10px 10px 0;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.chip.warn {
|
||||
border-left-color: var(--warn);
|
||||
background: #fff7ed;
|
||||
}
|
||||
|
||||
.issues {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.issue {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
.issue-top {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.tag {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 650;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #eef3f6;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tag.sev-high, .tag.sev-very_high { background: #fde8e8; color: var(--danger); }
|
||||
.tag.sev-middle { background: #fff4e5; color: var(--warn); }
|
||||
.tag.sev-low { background: #e8f6ef; color: var(--ok); }
|
||||
.issue .reason { font-weight: 600; margin: 0 0 6px; }
|
||||
.issue .suggestion, .issue .evidence {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.issue .evidence {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #f6f9fb;
|
||||
border-radius: 8px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.error-box {
|
||||
display: none;
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: #fdecec;
|
||||
color: var(--danger);
|
||||
border: 1px solid #f5c2c2;
|
||||
}
|
||||
.error-box.show { display: block; }
|
||||
|
||||
.footer-note {
|
||||
margin-top: 28px;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.level-low { color: var(--ok); }
|
||||
.level-middle { color: var(--mid); }
|
||||
.level-high { color: var(--high); }
|
||||
.level-very_high { color: var(--vhigh); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="hero">
|
||||
<h1 class="brand">AI 辅助写作风险检测</h1>
|
||||
<p class="lead">面向编辑的机器化写作风险辅助工具:模板化表达、机械化语言、空泛描述、章节逻辑与重复表达。不是 AI 生成百分比判断。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="start-grid">
|
||||
<div>
|
||||
<label for="url">稿件地址(.docx)</label>
|
||||
<input id="url" type="url" placeholder="https://.../manuscript.docx" />
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button id="btnStart" class="btn btn-primary" type="button">开始检测</button>
|
||||
<button id="btnRefresh" class="btn btn-ghost" type="button" disabled>刷新进度</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="start-grid" style="margin-top:12px; grid-template-columns: 1fr 1fr auto;">
|
||||
<div>
|
||||
<label for="taskId">任务 ID(可手动填入后刷新)</label>
|
||||
<input id="taskId" type="number" min="1" placeholder="task_id" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="taskNo">或任务号 task_no</label>
|
||||
<input id="taskNo" type="text" placeholder="awr_..." />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div class="meta-row" id="metaRow" style="display:none;">
|
||||
<span>状态 <span id="statusPill" class="status-pill">queued</span></span>
|
||||
<span>任务 <strong id="metaTask">-</strong></span>
|
||||
<span>更新 <strong id="metaUpdated">-</strong></span>
|
||||
</div>
|
||||
|
||||
<div class="progress-block" id="progressBlock">
|
||||
<div class="progress-head">
|
||||
<span id="progressMsg">排队中</span>
|
||||
<strong id="progressPct">0%</strong>
|
||||
</div>
|
||||
<div class="bar"><i id="progressBar"></i></div>
|
||||
<div class="step-text" id="progressStep">current_step: -</div>
|
||||
</div>
|
||||
|
||||
<div class="error-box" id="errorBox"></div>
|
||||
</section>
|
||||
|
||||
<section class="results" id="results">
|
||||
<div class="panel">
|
||||
<div class="score-board">
|
||||
<div class="gauge" aria-label="综合风险分">
|
||||
<svg viewBox="0 0 180 180">
|
||||
<circle class="track" cx="90" cy="90" r="80"></circle>
|
||||
<circle class="value" id="gaugeValue" cx="90" cy="90" r="80"></circle>
|
||||
</svg>
|
||||
<div class="gauge-center">
|
||||
<div class="num" id="overallScore">0</div>
|
||||
<div class="lvl" id="overallLevel">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kv" id="summaryKv"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">分维度风险</h2>
|
||||
<div class="dims" id="dims"></div>
|
||||
|
||||
<h2 class="section-title">章节评分</h2>
|
||||
<div class="sections" id="sections"></div>
|
||||
|
||||
<h2 class="section-title">编辑建议</h2>
|
||||
<div class="advice" id="advice"></div>
|
||||
|
||||
<h2 class="section-title" id="warnTitle" style="display:none;">警告</h2>
|
||||
<div class="warnings" id="warnings"></div>
|
||||
|
||||
<h2 class="section-title">重点问题</h2>
|
||||
<div class="issues" id="topIssues"></div>
|
||||
|
||||
<h2 class="section-title">全部明细</h2>
|
||||
<div class="issues" id="allDetails"></div>
|
||||
</section>
|
||||
|
||||
<p class="footer-note">进度页:<code>/api/aitools/realtimeWritingRiskProgress?task_id=任务ID</code>(未完成看进度,完成后看结果)。JSON 轮询请加 <code>format=json</code> 或使用 POST。</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const API_START = {$api_start_js};
|
||||
const API_PROGRESS = {$api_progress_js};
|
||||
const INIT_TASK_ID = {$init_task_id_js};
|
||||
const INIT_TASK_NO = {$init_task_no_js};
|
||||
const INIT_DATA = {$init_data_js};
|
||||
const PAGE_ERROR = {$page_error_js};
|
||||
const POLL_MS = 2500;
|
||||
|
||||
const el = (id) => document.getElementById(id);
|
||||
const levelLabel = {
|
||||
low: '低风险',
|
||||
middle: '中风险',
|
||||
high: '高风险',
|
||||
very_high: '极高风险',
|
||||
};
|
||||
const dimLabel = {
|
||||
template: '模板化',
|
||||
repetition: '短语重复',
|
||||
full_sentence: '完整句重复',
|
||||
sentence_structure: '句式重复',
|
||||
mechanical: '机械化',
|
||||
vague: '空泛描述',
|
||||
section_logic: '章节逻辑',
|
||||
};
|
||||
const statusMap = {
|
||||
0: 'queued',
|
||||
1: 'running',
|
||||
2: 'done',
|
||||
3: 'failed',
|
||||
};
|
||||
|
||||
let pollTimer = null;
|
||||
let currentTaskId = 0;
|
||||
let currentTaskNo = '';
|
||||
|
||||
function setPageMode(mode) {
|
||||
document.body.classList.remove('mode-progress', 'mode-result', 'mode-idle');
|
||||
document.body.classList.add(mode || 'mode-idle');
|
||||
}
|
||||
|
||||
function applyTaskState(data) {
|
||||
updateProgressUI(data || {});
|
||||
const status = Number((data && data.status) || 0);
|
||||
if (status === 2) {
|
||||
// 结果只落库,页面不渲染明细
|
||||
setPageMode('mode-progress');
|
||||
el('results').classList.remove('show');
|
||||
el('progressMsg').textContent = data.message || '检测完成,结果已写入数据库(页面不展示)';
|
||||
stopPoll();
|
||||
return 'done';
|
||||
}
|
||||
if (status === 3) {
|
||||
setPageMode('mode-progress');
|
||||
el('results').classList.remove('show');
|
||||
showError((data && (data.error_msg || data.message)) || '检测失败');
|
||||
stopPoll();
|
||||
return 'failed';
|
||||
}
|
||||
setPageMode('mode-progress');
|
||||
el('results').classList.remove('show');
|
||||
return 'running';
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const box = el('errorBox');
|
||||
box.textContent = msg || '未知错误';
|
||||
box.classList.add('show');
|
||||
}
|
||||
function clearError() {
|
||||
el('errorBox').classList.remove('show');
|
||||
el('errorBox').textContent = '';
|
||||
}
|
||||
|
||||
function setBusy(busy) {
|
||||
el('btnStart').disabled = busy;
|
||||
el('btnRefresh').disabled = !currentTaskId && !currentTaskNo;
|
||||
}
|
||||
|
||||
function setStatusPill(status, statusText) {
|
||||
const pill = el('statusPill');
|
||||
const key = statusText || statusMap[status] || 'queued';
|
||||
pill.textContent = key;
|
||||
pill.className = 'status-pill ' + (key === 'done' || key === 'failed' || key === 'running' ? key : '');
|
||||
}
|
||||
|
||||
function updateProgressUI(data) {
|
||||
el('metaRow').style.display = 'flex';
|
||||
el('progressBlock').classList.add('show');
|
||||
currentTaskId = data.task_id || currentTaskId;
|
||||
currentTaskNo = data.task_no || currentTaskNo;
|
||||
el('taskId').value = currentTaskId || '';
|
||||
el('taskNo').value = currentTaskNo || '';
|
||||
el('metaTask').textContent = (currentTaskNo || '-') + ' / #' + (currentTaskId || '-');
|
||||
el('metaUpdated').textContent = data.updated_at || data.created_at || '-';
|
||||
setStatusPill(data.status, data.status_text);
|
||||
|
||||
const pct = Number(data.progress_percent || 0);
|
||||
el('progressPct').textContent = pct.toFixed(1) + '%';
|
||||
el('progressBar').style.width = Math.max(0, Math.min(100, pct)) + '%';
|
||||
el('progressMsg').textContent = data.message || '处理中';
|
||||
el('progressStep').textContent = 'current_step: ' + (data.current_step || '-');
|
||||
el('btnRefresh').disabled = false;
|
||||
}
|
||||
|
||||
function levelClass(level) {
|
||||
return 'level-' + (level || 'low');
|
||||
}
|
||||
|
||||
function setGauge(score, level) {
|
||||
const circ = 2 * Math.PI * 80;
|
||||
const offset = circ * (1 - Math.max(0, Math.min(100, score)) / 100);
|
||||
const node = el('gaugeValue');
|
||||
node.style.strokeDasharray = String(circ);
|
||||
node.style.strokeDashoffset = String(offset);
|
||||
const colors = {
|
||||
low: '#047857',
|
||||
middle: '#a16207',
|
||||
high: '#c2410c',
|
||||
very_high: '#9f1239',
|
||||
};
|
||||
node.style.stroke = colors[level] || colors.middle;
|
||||
el('overallScore').textContent = String(score);
|
||||
el('overallLevel').textContent = levelLabel[level] || level || '-';
|
||||
el('overallLevel').className = 'lvl ' + levelClass(level);
|
||||
}
|
||||
|
||||
function renderSummary(result) {
|
||||
const items = [
|
||||
['规则分', result.rule_score ?? '-'],
|
||||
['模型分', result.model_score == null ? '不可用' : result.model_score],
|
||||
['模型覆盖', ((result.model_coverage || 0) * 100).toFixed(0) + '%'],
|
||||
['分节置信度', result.parse_confidence || '-'],
|
||||
];
|
||||
el('summaryKv').innerHTML = items.map(([k, v]) =>
|
||||
`<div class="item"><div class="k">${k}</div><div class="v">${v}</div></div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function renderDims(dimensions) {
|
||||
const dims = dimensions || {};
|
||||
el('dims').innerHTML = Object.keys(dimLabel).map((key) => {
|
||||
const d = dims[key] || { score: 0, level: 'low' };
|
||||
const score = Number(d.score || 0);
|
||||
return `<div class="dim">
|
||||
<div class="name">${dimLabel[key]}</div>
|
||||
<div class="score ${levelClass(d.level)}">${score}</div>
|
||||
<div class="mini"><i style="width:${Math.min(100, score)}%"></i></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderSections(sections) {
|
||||
const map = sections || {};
|
||||
el('sections').innerHTML = Object.keys(map).map((key) => {
|
||||
const s = map[key] || {};
|
||||
const model = s.model_available === false
|
||||
? (s.skipped ? '跳过' : '失败')
|
||||
: (s.model_score ?? '-');
|
||||
return `<div class="sec">
|
||||
<h4>${key}</h4>
|
||||
<div class="row"><span>综合</span><b class="${levelClass(s.level)}">${s.score ?? '-'}</b></div>
|
||||
<div class="row"><span>规则</span><b>${s.rule_score ?? '-'}</b></div>
|
||||
<div class="row"><span>模型</span><b>${model}</b></div>
|
||||
<div class="row"><span>等级</span><b>${levelLabel[s.level] || s.level || '-'}</b></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderList(targetId, items, soft) {
|
||||
const list = Array.isArray(items) ? items : [];
|
||||
const box = el(targetId);
|
||||
if (!list.length) {
|
||||
box.innerHTML = soft ? '' : '<div class="chip">暂无</div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = list.map((t) =>
|
||||
`<div class="chip${soft ? ' warn' : ''}">${escapeHtml(String(t))}</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function renderIssues(targetId, details) {
|
||||
const list = Array.isArray(details) ? details : [];
|
||||
const box = el(targetId);
|
||||
if (!list.length) {
|
||||
box.innerHTML = '<div class="chip">暂无命中项</div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = list.map((d) => {
|
||||
const sev = d.severity || 'low';
|
||||
const evidence = d.evidence || d.sentence || '';
|
||||
return `<article class="issue">
|
||||
<div class="issue-top">
|
||||
<span class="tag sev-${escapeHtml(sev)}">${escapeHtml(sev)}</span>
|
||||
<span class="tag">${escapeHtml(d.type || '')}</span>
|
||||
<span class="tag">${escapeHtml(d.section || '')}</span>
|
||||
${d.score != null && d.score !== '' ? `<span class="tag">score ${escapeHtml(String(d.score))}</span>` : ''}
|
||||
</div>
|
||||
<p class="reason">${escapeHtml(d.reason || '')}</p>
|
||||
${d.suggestion ? `<p class="suggestion">建议:${escapeHtml(d.suggestion)}</p>` : ''}
|
||||
${evidence ? `<p class="evidence">“${escapeHtml(evidence)}”</p>` : ''}
|
||||
</article>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderResult(result) {
|
||||
el('results').classList.add('show');
|
||||
setGauge(Number(result.overall_score || 0), result.overall_level || 'low');
|
||||
renderSummary(result);
|
||||
renderDims(result.dimensions || {});
|
||||
renderSections(result.sections || {});
|
||||
renderList('advice', result.editor_advice || []);
|
||||
const warnings = result.warnings || [];
|
||||
el('warnTitle').style.display = warnings.length ? 'block' : 'none';
|
||||
renderList('warnings', warnings, true);
|
||||
renderIssues('topIssues', result.top_issues || []);
|
||||
renderIssues('allDetails', result.details || []);
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
async function postForm(url, params) {
|
||||
const body = new URLSearchParams();
|
||||
Object.keys(params).forEach((k) => {
|
||||
if (params[k] !== '' && params[k] != null) body.append(k, params[k]);
|
||||
});
|
||||
body.append('format', 'json');
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: body.toString(),
|
||||
});
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startPoll() {
|
||||
stopPoll();
|
||||
pollTimer = setInterval(() => refreshProgress(true), POLL_MS);
|
||||
}
|
||||
|
||||
function syncUrl(taskId, taskNo) {
|
||||
try {
|
||||
const u = new URL(window.location.href);
|
||||
// 仅在进度页路径下同步 query,便于分享
|
||||
if (/realtimeWritingRiskProgress/i.test(u.pathname + u.search)) {
|
||||
if (taskId) u.searchParams.set('task_id', String(taskId));
|
||||
if (taskNo) u.searchParams.set('task_no', String(taskNo));
|
||||
window.history.replaceState({}, '', u.toString());
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function startDetect() {
|
||||
clearError();
|
||||
const url = el('url').value.trim();
|
||||
if (!url) {
|
||||
showError('请填写稿件地址');
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
el('results').classList.remove('show');
|
||||
try {
|
||||
const json = await postForm(API_START, { url });
|
||||
if (Number(json.code) !== 200) {
|
||||
throw new Error(json.msg || '开始检测失败');
|
||||
}
|
||||
const data = json.data || {};
|
||||
applyTaskState(data);
|
||||
syncUrl(data.task_id, data.task_no);
|
||||
// 跳转到可分享的进度页
|
||||
if (data.task_id) {
|
||||
const progressUrl = API_PROGRESS + (API_PROGRESS.indexOf('?') >= 0 ? '&' : '?') + 'task_id=' + encodeURIComponent(data.task_id);
|
||||
window.location.href = progressUrl;
|
||||
return;
|
||||
}
|
||||
startPoll();
|
||||
await refreshProgress(true);
|
||||
} catch (e) {
|
||||
showError(e.message || String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshProgress(silent) {
|
||||
clearError();
|
||||
const taskId = el('taskId').value.trim() || currentTaskId;
|
||||
const taskNo = el('taskNo').value.trim() || currentTaskNo;
|
||||
if (!taskId && !taskNo) {
|
||||
if (!silent) showError('请先开始检测,或填写 task_id / task_no');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const json = await postForm(API_PROGRESS, {
|
||||
task_id: taskId || '',
|
||||
task_no: taskNo || '',
|
||||
});
|
||||
if (Number(json.code) !== 200) {
|
||||
throw new Error(json.msg || '刷新进度失败');
|
||||
}
|
||||
const data = json.data || {};
|
||||
const state = applyTaskState(data);
|
||||
syncUrl(data.task_id, data.task_no);
|
||||
if (state === 'running' && !pollTimer) {
|
||||
startPoll();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!silent) showError(e.message || String(e));
|
||||
}
|
||||
}
|
||||
|
||||
el('btnStart').addEventListener('click', startDetect);
|
||||
el('btnRefresh').addEventListener('click', () => refreshProgress(false));
|
||||
el('url').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') startDetect();
|
||||
});
|
||||
|
||||
// 进度页直开:未完成显示进度样式,完成后显示结果样式
|
||||
if (PAGE_ERROR) {
|
||||
showError(PAGE_ERROR);
|
||||
}
|
||||
if (INIT_TASK_ID) {
|
||||
currentTaskId = Number(INIT_TASK_ID) || 0;
|
||||
el('taskId').value = currentTaskId || '';
|
||||
}
|
||||
if (INIT_TASK_NO) {
|
||||
currentTaskNo = String(INIT_TASK_NO);
|
||||
el('taskNo').value = currentTaskNo;
|
||||
}
|
||||
if (INIT_DATA && typeof INIT_DATA === 'object') {
|
||||
const state = applyTaskState(INIT_DATA);
|
||||
if (state === 'running') {
|
||||
startPoll();
|
||||
}
|
||||
} else if (currentTaskId || currentTaskNo) {
|
||||
setPageMode('mode-progress');
|
||||
refreshProgress(true);
|
||||
} else {
|
||||
setPageMode('mode-idle');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user