Browse Source

merge: 发现智能体新增AI查重;双屏联动变化

master
Senor-Liu 7 hours ago
parent
commit
9e6c1bbcf6
  1. 96
      web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.jsx
  2. 20
      web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.module.less
  3. 23
      web/client/src/sections/superAgent/components/AgentSquare.jsx

96
web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.jsx

@ -79,6 +79,7 @@ export default function AiPlagiarismCheck() {
const processedRef = useRef(new Set());
const warnedRef = useRef(new Set());
const nameMapRef = useRef({});
const scrollSyncRef = useRef(false);
/* ---- 派生 ---- */
const visibleIndices = useMemo(
@ -308,6 +309,82 @@ export default function AiPlagiarismCheck() {
setEditContent(block.htmlContent || block.plainText);
}, [taskFiles, fileBlocksCache, taskId]);
//
const jumpToCounterpart = useCallback((docIdx, idx) => {
const related = matches.filter(
(m) =>
(m.leftIdx === docIdx && m.leftPara === idx) ||
(m.rightIdx === docIdx && m.rightPara === idx),
);
if (!related.length) return;
const match = related[0];
setCurrentFocusId(match.id);
const targetDocIdx = match.leftIdx === docIdx ? match.rightIdx : match.leftIdx;
const targetPara = match.leftIdx === docIdx ? match.rightPara : match.leftPara;
const targetId = "doc-container-" + targetDocIdx + "-" + targetPara;
// DOM pane scrollIntoView
if (hiddenFiles.has(targetDocIdx)) {
setHiddenFiles((prev) => {
const next = new Set(prev);
next.delete(targetDocIdx);
return next;
});
}
requestAnimationFrame(() => {
const targetEl = document.getElementById(targetId);
const targetPane = targetEl?.closest("." + styles.pane);
if (!targetEl || !targetPane) return;
const paneRect = targetPane.getBoundingClientRect();
const targetRect = targetEl.getBoundingClientRect();
const targetTop = targetPane.scrollTop + targetRect.top - paneRect.top;
const centeredTop = targetTop - (targetPane.clientHeight - targetEl.offsetHeight) / 2;
targetPane.scrollTo({ top: Math.max(0, centeredTop), behavior: "smooth" });
});
}, [matches, hiddenFiles, styles.pane]);
//
const syncCounterpartScroll = useCallback((docIdx, sourcePane) => {
if (scrollSyncRef.current || !sourcePane || !matches.length) return;
const sourcePaneRect = sourcePane.getBoundingClientRect();
const candidates = matches
.filter((match) => match.leftIdx === docIdx || match.rightIdx === docIdx)
.map((match) => {
const sourcePara = match.leftIdx === docIdx ? match.leftPara : match.rightPara;
const targetDocIdx = match.leftIdx === docIdx ? match.rightIdx : match.leftIdx;
const targetPara = match.leftIdx === docIdx ? match.rightPara : match.leftPara;
const sourceEl = document.getElementById("doc-container-" + docIdx + "-" + sourcePara);
const targetEl = document.getElementById("doc-container-" + targetDocIdx + "-" + targetPara);
if (!sourceEl || !targetEl) return null;
return {
sourceEl,
targetEl,
distance: Math.abs(sourceEl.getBoundingClientRect().top - sourcePaneRect.top),
};
})
.filter(Boolean)
.sort((a, b) => a.distance - b.distance);
const current = candidates[0];
const targetPane = current?.targetEl.closest("." + styles.pane);
if (!current || !targetPane) return;
const sourceTop = current.sourceEl.getBoundingClientRect().top - sourcePaneRect.top;
const targetPaneRect = targetPane.getBoundingClientRect();
const targetTop = targetPane.scrollTop + current.targetEl.getBoundingClientRect().top - targetPaneRect.top;
const nextTop = Math.max(0, targetTop - sourceTop);
if (Math.abs(targetPane.scrollTop - nextTop) < 2) return;
scrollSyncRef.current = true;
targetPane.scrollTop = nextTop;
requestAnimationFrame(() => {
scrollSyncRef.current = false;
});
}, [matches, styles.pane]);
const applyAiRewrite = useCallback(async () => {
if (!editingBlock || !editingBlock.taskId || !editingBlock.fileId || !editingBlock.blockId) {
message.warning("无法降重:缺少必要信息"); return;
@ -590,7 +667,12 @@ export default function AiPlagiarismCheck() {
const activeClass = isFocused ? " " + styles.active : "";
// CSS <mark>
if (/<[^>]+>/.test(html)) {
return (<div key={idx} id={blockId}><p className={styles.docP + " " + clr + activeClass} onClick={() => openEditor(docIdx, idx)} dangerouslySetInnerHTML={{ __html: html }} /></div>);
return (
<div key={idx} id={blockId} className={styles.matchParaWrap}>
<p className={styles.docP + " " + clr + activeClass} onClick={() => jumpToCounterpart(docIdx, idx)} dangerouslySetInnerHTML={{ __html: html }} />
<Button size="small" type="text" icon={<EditOutlined />} className={styles.matchEditBtn} title="编辑该段" onClick={() => openEditor(docIdx, idx)} />
</div>
);
}
//
const spans = [];
@ -614,7 +696,12 @@ export default function AiPlagiarismCheck() {
cursor = s.end;
});
if (cursor < plain.length) parts.push(plain.slice(cursor));
return (<div key={idx} id={blockId}><p className={styles.docP} onClick={() => openEditor(docIdx, idx)}>{parts}</p></div>);
return (
<div key={idx} id={blockId} className={styles.matchParaWrap}>
<p className={styles.docP} onClick={() => jumpToCounterpart(docIdx, idx)}>{parts}</p>
<Button size="small" type="text" icon={<EditOutlined />} className={styles.matchEditBtn} title="编辑该段" onClick={() => openEditor(docIdx, idx)} />
</div>
);
}
}
@ -669,7 +756,10 @@ export default function AiPlagiarismCheck() {
const blocks = fileBlocksCache[file?.id] || [];
return (
<div key={docIdx} className={styles.panelGroup}>
<div className={styles.pane} onScroll={redrawAll}>
<div className={styles.pane} onScroll={(event) => {
syncCounterpartScroll(docIdx, event.currentTarget);
redrawAll();
}}>
<div className={styles.paneHeader}>
<Typography.Text strong>{(nameMapRef.current[file?.originalName] || fixGarbledName(file?.originalName) || "文档" + (docIdx + 1))}</Typography.Text>
<Button size="small" danger icon={<CloseOutlined />} onClick={() => hidePanel(docIdx)}>隐藏</Button>

20
web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.module.less

@ -480,6 +480,26 @@
}
}
/* 匹配段落外层:承载点击跳转 + 悬浮编辑按钮 */
.matchParaWrap {
position: relative;
}
.matchEditBtn {
position: absolute;
top: 4px;
right: 6px;
opacity: 0.4;
background: #fff;
border-radius: 4px;
z-index: 2;
transition: opacity 0.15s ease;
}
.matchParaWrap:hover .matchEditBtn {
opacity: 1;
}
/* 中缝(面板间连线区域) */
.gutter {
width: 48px;

23
web/client/src/sections/superAgent/components/AgentSquare.jsx

@ -158,9 +158,10 @@ const AGENT_CATEGORY_FILTERS = [
{ key: "我的收藏", label: "我的收藏" },
];
// superAgent
// superAgent
const SUPER_AGENT_VISIBLE_CARD_KEYS = new Set([
"stable-finance-invoice",
"stable-plagiarism-check",
"beta-message",
]);
@ -280,6 +281,26 @@ const buildSections = (token, userIdParam) => [
icon: duibiIcon,
component: "financeInvoice",
},
{
key: "stable-plagiarism-check",
title: "AI查重",
description:
"上传文稿自动检测重复内容,按连续相同字数阈值定位重复段落,支持差异比对与在线修改。",
updatedAt: "2026-08-17",
badgeA: "正式",
badgeB: "分析类",
category: "专业应用",
cornerBadge: "new",
icon: duibiIcon,
component: "aiTenderDocument",
initialActiveMenu: "check",
url: buildAppPath("/aiTenderDocument", {
activeMenu: "check",
hideMenu: "1",
token,
userid: userIdParam,
}),
},
],
},
{

Loading…
Cancel
Save