添加期刊引用分析
This commit is contained in:
406
src/utils/manuscriptCitationRelevance.js
Normal file
406
src/utils/manuscriptCitationRelevance.js
Normal file
@@ -0,0 +1,406 @@
|
||||
/** 正文引用标记,如 [1]、[1-7]、<blue>[4,6,8]</blue> */
|
||||
const CITATION_BRACKET_RE = /(?:<blue>\s*)?\[([^\]]+)\](?:\s*<\/blue>)?/gi;
|
||||
|
||||
function decodeHtmlEntities(text) {
|
||||
return String(text || '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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')
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user