Files
tougao_web/src/utils/manuscriptCitationRelevance.js
2026-06-17 09:35:15 +08:00

407 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 正文引用标记,如 [1]、[1-7]、<blue>[4,6,8]</blue> */
const CITATION_BRACKET_RE = /(?:<blue>\s*)?\[([^\]]+)\](?:\s*<\/blue>)?/gi;
function decodeHtmlEntities(text) {
return String(text || '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"');
}
function escapeHtml(text) {
return String(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
export function htmlToPlainText(html) {
const raw = String(html || '');
if (typeof document !== 'undefined') {
const node = document.createElement('div');
node.innerHTML = raw;
return (node.textContent || node.innerText || '').replace(/\u00a0/g, ' ').trim();
}
return decodeHtmlEntities(raw.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, '')).trim();
}
export function expandCitationBracket(inner) {
const result = [];
const text = String(inner || '')
.replace(/<[^>]+>/g, '')
.trim();
if (!text) {
return result;
}
text.split(/\s*,\s*/).forEach(function (part) {
const token = part.trim();
const range = token.match(/^(\d+)\s*[\-]\s*(\d+)$/);
if (range) {
const a = Number(range[1]);
const b = Number(range[2]);
const lo = Math.min(a, b);
const hi = Math.max(a, b);
let i = 0;
for (i = lo; i <= hi; i += 1) {
result.push(i);
}
return;
}
if (/^\d+$/.test(token)) {
result.push(Number(token));
}
});
return result;
}
/** 从段落 HTML 提取引用编号(按出现顺序,段内去重) */
export function parseCitationNumbersFromHtml(html) {
const nums = [];
const seen = {};
const re = new RegExp(CITATION_BRACKET_RE.source, 'gi');
let match = null;
while ((match = re.exec(String(html || ''))) !== null) {
expandCitationBracket(match[1]).forEach(function (num) {
if (!seen[num]) {
seen[num] = true;
nums.push(num);
}
});
}
return nums;
}
export function stripHtmlTags(html) {
return htmlToPlainText(html);
}
export function formatReferencePlainText(ref) {
if (!ref) {
return '';
}
if (ref.refer_frag) {
const frag = stripHtmlTags(ref.refer_frag);
if (frag) {
return frag;
}
}
const parts = [];
if (ref.author) {
parts.push(stripHtmlTags(ref.author));
}
if (ref.title) {
parts.push(stripHtmlTags(ref.title));
}
if (ref.joura) {
parts.push(stripHtmlTags(ref.joura));
}
if (ref.dateno) {
parts.push(stripHtmlTags(ref.dateno));
}
if (ref.doilink) {
parts.push(String(ref.doilink).trim());
}
if (ref.isbn) {
parts.push('ISBN: ' + String(ref.isbn).trim());
}
return parts.filter(Boolean).join('. ').replace(/\.\s*\./g, '.').trim();
}
function isTextParagraphItem(item) {
if (!item) {
return false;
}
if (item.type == 1 || item.type == 2) {
return false;
}
const html = String(item.content || item.text || '').trim();
return !!html;
}
function buildCitationEntry(citeNum, refList) {
const ref = refList[citeNum - 1] || null;
return {
citeNum: citeNum,
reference: ref,
referenceText: formatReferencePlainText(ref),
referenceMissing: !ref
};
}
export function formatCitationNumsLabel(citeNums) {
return (citeNums || []).map(function (n) {
return '[' + n + ']';
}).join(' ');
}
export function getCitationReviewGroupMeta(item, index, labels) {
const l = labels || {};
const groupNo = index + 1;
const paragraphNo = item.paragraphIndex + 1;
const citeNumsLabel = formatCitationNumsLabel(item.citeNums);
const citeNumsPlain = (item.citeNums || []).join('、');
let title = '正文段落 ' + paragraphNo + ' · ' + citeNumsLabel;
if (l.groupLabel) {
title = l.groupLabel
.replace('{group}', groupNo)
.replace('{paragraph}', paragraphNo)
.replace('{refs}', citeNumsLabel);
}
return {
id: 'cite-group-' + groupNo,
groupNo: groupNo,
paragraphNo: paragraphNo,
citeNumsLabel: citeNumsLabel,
citeNumsPlain: citeNumsPlain,
title: title
};
}
/**
* 构建核查队列:每个含引文的正文段落一组,该段全部参考文献一并询问;
* 同一条文献出现在不同段落时,会在各段落中分别询问。
*/
export function buildCitationReviewQueue(wordList, references) {
const items = [];
const refList = references || [];
(wordList || []).forEach(function (item, paragraphIndex) {
if (!isTextParagraphItem(item)) {
return;
}
const html = item.content || item.text || '';
const citeNums = parseCitationNumbersFromHtml(html);
if (!citeNums.length) {
return;
}
const paragraphText = htmlToPlainText(html);
const citations = citeNums.map(function (citeNum) {
return buildCitationEntry(citeNum, refList);
});
items.push({
paragraphIndex: paragraphIndex,
amId: item.am_id,
paragraphHtml: html,
paragraphText: paragraphText,
citeNums: citeNums.slice(),
citations: citations,
hasMissingReference: citations.some(function (c) {
return c.referenceMissing;
})
});
});
return items;
}
export function buildWenaiRelevancePrompt(item) {
if (!item) {
return '';
}
const paragraphText = item.paragraphText || '';
const citations = item.citations || [];
const citeLabels = citations.map(function (c) {
return '[' + c.citeNum + ']';
});
let refBlock = '';
citations.forEach(function (c) {
const text = c.referenceText || '(未找到该条参考文献)';
refBlock += '[' + c.citeNum + ']\n' + text + '\n\n';
});
const labelList = citeLabels.join('、') || '';
return (
'请审读以下正文段落及其引用文献,结合该段落的论述内容,判断各文献与正文的相关程度。\n\n' +
'【正文段落】\n' +
paragraphText +
'\n\n' +
'【参考文献】\n' +
refBlock.trim() +
'\n\n' +
'【输出要求】\n' +
'请以期刊编辑审稿批注的语气,为作者撰写一段文件批注(不要用邮件格式,不要称呼、落款、主题行或分条邮件体)。\n' +
'请按文献序号 ' +
labelList +
' 依次说明,保留原文序号且顺序连续,不要重新编号或跳号。\n' +
'对每条文献分别给出关联判断:直接相关 / 强相关 / 弱相关 / 不相关,并用一句话简要说明理由。\n' +
'将全部内容合并写成一段连贯的批注文字,语气客观、专业、克制,可直接作为稿件文件批注使用。'
);
}
/** 合并全部批次提示词 */
export function buildAllWenaiRelevancePrompts(reviewItems, labels) {
return (reviewItems || [])
.map(function (item, index) {
const meta = getCitationReviewGroupMeta(item, index, labels);
const header = '========== ' + meta.title + ' ==========';
return header + '\n\n' + buildWenaiRelevancePrompt(item);
})
.filter(Boolean)
.join('\n\n\n');
}
/** 生成可独立打开的 HTML 文档(含锚点导航与复制) */
export function buildCitationReviewStandaloneHtml(reviewItems, labels) {
const l = labels || {};
const items = reviewItems || [];
const total = items.length;
const pageTitle = l.pageTitle || '引文相关性核查';
const groupTotalText = (l.groupTotal || '共 {n} 组').replace('{n}', total);
const jumpTitle = l.jumpTitle || '快速跳转';
const copyGroupText = l.copyGroup || '复制本组';
const copyAllText = l.copyAll || '复制全部';
const copySuccessText = l.copySuccess || '已复制';
const copyFailText = l.copyFail || '复制失败';
let navHtml = '';
let bodyHtml = '';
let i = 0;
for (i = 0; i < items.length; i += 1) {
const item = items[i];
const meta = getCitationReviewGroupMeta(item, i, labels);
const prompt = buildWenaiRelevancePrompt(item);
const promptId = 'cite-prompt-' + meta.groupNo;
navHtml +=
'<a class="nav-link" href="#' +
meta.id +
'">' +
escapeHtml(meta.title) +
'</a>';
bodyHtml +=
'<section class="group-card" id="' +
meta.id +
'">' +
'<div class="group-head">' +
'<h3>' +
escapeHtml(meta.title) +
'</h3>' +
'<button type="button" class="copy-btn" data-target="' +
promptId +
'">' +
escapeHtml(copyGroupText) +
'</button>' +
'</div>' +
'<pre class="group-prompt" id="' +
promptId +
'">' +
escapeHtml(prompt) +
'</pre>' +
'</section>';
}
const allPrompt = escapeHtml(buildAllWenaiRelevancePrompts(items, labels));
return (
'<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">' +
'<meta name="viewport" content="width=device-width,initial-scale=1">' +
'<title>' +
escapeHtml(pageTitle) +
'</title>' +
'<style>' +
'*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#f5f7fa;color:#303133;line-height:1.5}' +
'.page{max-width:960px;margin:0 auto;padding:20px}' +
'.header{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
'.header h1{margin:0 0 8px;font-size:20px}' +
'.summary{color:#606266;font-size:14px;margin-bottom:12px}' +
'.actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}' +
'.btn{border:1px solid #409eff;background:#409eff;color:#fff;border-radius:4px;padding:6px 14px;font-size:13px;cursor:pointer}' +
'.btn.plain{background:#ecf5ff;color:#409eff}' +
'.nav{background:#fff;border-radius:8px;padding:12px 16px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
'.nav-title{font-weight:600;margin-bottom:8px;font-size:14px}' +
'.nav-links{display:flex;flex-wrap:wrap;gap:8px}' +
'.nav-link{display:inline-block;padding:4px 10px;background:#f4f4f5;border-radius:4px;color:#409eff;text-decoration:none;font-size:12px}' +
'.nav-link:hover{background:#ecf5ff}' +
'.group-card{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08);scroll-margin-top:16px}' +
'.group-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}' +
'.group-head h3{margin:0;font-size:15px;color:#303133}' +
'.copy-btn{border:1px solid #409eff;background:#ecf5ff;color:#409eff;border-radius:4px;padding:4px 12px;font-size:12px;cursor:pointer;white-space:nowrap}' +
'.copy-btn:hover{background:#409eff;color:#fff}' +
'.group-prompt{margin:0;padding:12px;background:#fafafa;border:1px solid #ebeef5;border-radius:4px;white-space:pre-wrap;word-break:break-word;font-size:13px;font-family:Consolas,"Courier New",monospace}' +
'#all-prompt{display:none}' +
'</style></head><body><div class="page">' +
'<div class="header"><h1>' +
escapeHtml(pageTitle) +
'</h1>' +
'<div class="summary">' +
escapeHtml(groupTotalText) +
'</div>' +
'<div class="actions">' +
'<button type="button" class="btn plain" id="copy-all-btn">' +
escapeHtml(copyAllText) +
'</button></div></div>' +
'<div class="nav"><div class="nav-title">' +
escapeHtml(jumpTitle) +
'</div><div class="nav-links">' +
navHtml +
'</div></div>' +
bodyHtml +
'<pre id="all-prompt">' +
allPrompt +
'</pre></div>' +
'<script>(function(){' +
'var okMsg=' +
JSON.stringify(copySuccessText) +
';var failMsg=' +
JSON.stringify(copyFailText) +
';function copyFromEl(el,btn){var text=el?(el.innerText||el.textContent||""):"";if(!text){alert(failMsg);return;}' +
'function done(){if(btn){var old=btn.innerText;btn.innerText=okMsg;setTimeout(function(){btn.innerText=old;},1200);}else{alert(okMsg);}}' +
'if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(text).then(done).catch(function(){fallback(text,done);});return;}' +
'fallback(text,done);}' +
'function fallback(text,done){var ta=document.createElement("textarea");ta.value=text;ta.style.position="fixed";ta.style.opacity="0";document.body.appendChild(ta);ta.select();try{document.execCommand("copy");done();}catch(e){alert(failMsg);}document.body.removeChild(ta);}' +
'document.querySelectorAll(".copy-btn").forEach(function(btn){btn.addEventListener("click",function(){var id=btn.getAttribute("data-target");copyFromEl(document.getElementById(id),btn);});});' +
'var allBtn=document.getElementById("copy-all-btn");if(allBtn){allBtn.addEventListener("click",function(){copyFromEl(document.getElementById("all-prompt"),allBtn);});}' +
'})();<' +
'/script></body></html>'
);
}
export function downloadCitationReviewHtml(reviewItems, labels, fileName) {
const html = buildCitationReviewStandaloneHtml(reviewItems, labels);
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName || 'citation-relevance.html';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
export function buildCitationReviewHtmlLabels(translate) {
const t = translate || function (key) {
return key;
};
return {
pageTitle: t('commonTable.citeRelevanceTitle'),
groupTotal: t('commonTable.citeRelevanceGroupTotal', { n: '{n}' }),
jumpTitle: t('commonTable.citeRelevanceJumpTo'),
copyGroup: t('commonTable.citeRelevanceCopyGroup'),
copyAll: t('commonTable.citeRelevanceCopyAllWenai'),
copySuccess: t('commonTable.citeRelevanceCopySuccess'),
copyFail: t('commonTable.citeRelevanceCopyFail'),
groupLabel: t('commonTable.citeRelevanceGroupNavLabel')
};
}