Refine report citation anchors

This commit is contained in:
liyizhouAI 2026-06-08 19:21:14 +08:00
parent a6a12d7775
commit c5cc5912c0
2 changed files with 110 additions and 14 deletions

View File

@ -697,14 +697,14 @@ def _expanded_report_sections() -> List[tuple[str, str]]:
def _chat_citations() -> List[Dict[str, Any]]:
return [
{"id": "S01", "label": "S01", "title": "执行结论", "section_index": 1},
{"id": "S02", "label": "S02", "title": "Agent问答暴露的问题", "section_index": 2},
{"id": "S03", "label": "S03", "title": "商业机会", "section_index": 3},
{"id": "A01", "label": "A01", "title": "高净值客户A", "section_index": 2, "agent_id": 1},
{"id": "A08", "label": "A08", "title": "配偶共同决策人", "section_index": 2, "agent_id": 8},
{"id": "A10", "label": "A10", "title": "企业财务负责人", "section_index": 2, "agent_id": 10},
{"id": "A05", "label": "A05", "title": "风控合规经理", "section_index": 2, "agent_id": 5},
{"id": "A29", "label": "A29", "title": "销售管理看板", "section_index": 2, "agent_id": 29},
{"id": "S01", "anchor_id": "S01", "label": "S01", "title": "执行结论", "section_index": 1},
{"id": "S02", "anchor_id": "S02", "label": "S02", "title": "Agent问答暴露的问题", "section_index": 2},
{"id": "S03", "anchor_id": "S03", "label": "S03", "title": "商业机会", "section_index": 3},
{"id": "A01", "anchor_id": "A01", "label": "A01", "title": "高净值客户A|核心客户", "section_index": 2, "agent_id": 1},
{"id": "A08", "anchor_id": "A08", "label": "A08", "title": "配偶共同决策人|家庭安全垫", "section_index": 2, "agent_id": 8},
{"id": "A10", "anchor_id": "A10", "label": "A10", "title": "企业财务负责人|资金分层", "section_index": 2, "agent_id": 10},
{"id": "A05", "anchor_id": "A05", "label": "A05", "title": "风控合规经理|适当性与话术", "section_index": 2, "agent_id": 5},
{"id": "A29", "anchor_id": "A29", "label": "A29", "title": "销售管理看板|管理层指标", "section_index": 2, "agent_id": 29},
]

View File

@ -279,10 +279,11 @@
:key="citation.id"
class="citation-chip"
type="button"
:data-citation-id="citation.anchor_id || citation.id"
:data-section-index="citation.section_index"
:data-agent-id="citation.agent_id ?? ''"
:title="citation.title"
@click="jumpToCitation(citation)"
@click.stop="jumpToCitation(citation)"
>
{{ citation.label }}
</button>
@ -506,6 +507,37 @@ const citationTargetSection = (citation) => {
return 1
}
const citationIdFromDataset = (target) => {
if (target.dataset.citationId) return target.dataset.citationId
if (target.dataset.agentId) return `A${String(Number(target.dataset.agentId)).padStart(2, '0')}`
if (target.dataset.sectionIndex) return `S${String(Number(target.dataset.sectionIndex)).padStart(2, '0')}`
return null
}
const clearCitationTargetHighlights = () => {
leftPanel.value
?.querySelectorAll('.citation-target-highlight')
.forEach(el => el.classList.remove('citation-target-highlight'))
}
const highlightCitationTarget = (target) => {
clearCitationTargetHighlights()
if (!target) return
target.classList.add('citation-target-highlight')
if (target.matches('[data-citation-anchor^="A"]')) {
let sibling = target.nextElementSibling
let highlighted = 0
while (sibling && highlighted < 3 && !sibling.matches('[data-citation-anchor], .md-h4, .md-h3, .md-h2')) {
sibling.classList.add('citation-target-highlight')
sibling = sibling.nextElementSibling
highlighted++
}
}
window.setTimeout(() => clearCitationTargetHighlights(), 3200)
}
const jumpToSection = (sectionIndex) => {
const index = Number(sectionIndex)
if (!Number.isFinite(index) || index < 1) return
@ -528,14 +560,46 @@ const jumpToSection = (sectionIndex) => {
}
const jumpToCitation = (citation) => {
jumpToSection(citationTargetSection(citation))
const sectionIndex = citationTargetSection(citation)
const citationId = citation.anchor_id || citation.id || (
citation.agent_id !== undefined
? `A${String(Number(citation.agent_id)).padStart(2, '0')}`
: `S${String(sectionIndex).padStart(2, '0')}`
)
const zeroIndex = sectionIndex - 1
const newSet = new Set(collapsedSections.value)
newSet.delete(zeroIndex)
collapsedSections.value = newSet
currentSectionIndex.value = sectionIndex
highlightedSectionIndex.value = citationId?.startsWith('S') ? sectionIndex : null
requestAnimationFrame(() => {
const exactTarget = citationId
? leftPanel.value?.querySelector(`[data-citation-anchor="${citationId}"]`)
: null
const fallbackTarget = leftPanel.value?.querySelector(`[data-section-index="${sectionIndex}"]`)
const target = exactTarget || fallbackTarget
target?.scrollIntoView({ behavior: 'smooth', block: exactTarget ? 'center' : 'start' })
highlightCitationTarget(exactTarget || (citationId?.startsWith('S') ? fallbackTarget : null))
})
if (citationId?.startsWith('S')) {
window.setTimeout(() => {
if (highlightedSectionIndex.value === sectionIndex) highlightedSectionIndex.value = null
}, 2200)
}
}
const handleCitationClick = (event) => {
const target = event.target?.closest?.('[data-section-index], [data-agent-id]')
const target = event.target?.closest?.('.citation-chip')
if (!target) return
const sectionIndex = target.dataset.sectionIndex || (target.dataset.agentId ? 2 : 1)
jumpToSection(sectionIndex)
jumpToCitation({
id: citationIdFromDataset(target),
anchor_id: citationIdFromDataset(target),
section_index: target.dataset.sectionIndex || (target.dataset.agentId ? 2 : 1),
agent_id: target.dataset.agentId || undefined,
})
}
const selectChatTarget = (target) => {
@ -620,11 +684,13 @@ const renderMarkdown = (content) => {
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
html = html.replace(/\[\[(S|A)(\d{1,2})\]\]/g, (match, type, number) => {
const value = String(Number(number)).padStart(2, '0')
const citationId = `${type}${value}`
const sectionIndex = type === 'S' ? Number(number) : 2
const agentId = type === 'A' ? Number(number) : ''
const title = type === 'S' ? `跳转到报告第 ${value}` : `跳转到 Agent ${value} 问答证据`
return `<button type="button" class="citation-chip citation-inline" data-section-index="${sectionIndex}" data-agent-id="${agentId}" title="${title}">${type}${value}</button>`
return `<button type="button" class="citation-chip citation-inline" data-citation-id="${citationId}" data-section-index="${sectionIndex}" data-agent-id="${agentId}" title="${title}">${citationId}</button>`
})
html = html.replace(/^#### (A\d{2})\s+(.+)$/gm, '<h5 class="md-h5 citation-anchor-heading" data-citation-anchor="$1">$1 $2</h5>')
html = html.replace(/^#### (.+)$/gm, '<h5 class="md-h5">$1</h5>')
html = html.replace(/^### (.+)$/gm, '<h4 class="md-h4">$1</h4>')
html = html.replace(/^## (.+)$/gm, '<h3 class="md-h3">$1</h3>')
@ -1279,6 +1345,31 @@ watch(() => props.simulationId, (newId) => {
font-family: 'Times New Roman', Times, serif;
}
.generated-content :deep([data-citation-anchor]) {
scroll-margin-top: 36px;
}
.generated-content :deep(.citation-target-highlight) {
background: rgba(16, 185, 129, 0.10);
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.24);
}
.generated-content :deep(.citation-anchor-heading.citation-target-highlight) {
display: block;
margin-left: -10px;
margin-right: -10px;
padding: 8px 10px;
border-radius: 8px 8px 0 0;
}
.generated-content :deep(.citation-target-highlight.md-quote),
.generated-content :deep(.citation-target-highlight.md-p) {
margin-left: -10px;
margin-right: -10px;
padding-left: 18px;
padding-right: 18px;
}
.generated-content :deep(.code-block) {
background: #F9FAFB;
padding: 12px;
@ -2712,4 +2803,9 @@ html[data-theme='dark'] .report-section-item.is-citation-highlight {
background: rgba(16, 185, 129, 0.14);
box-shadow: 0 0 0 1px rgba(52, 211, 153, 0.32), 0 12px 30px rgba(0, 0, 0, 0.24);
}
html[data-theme='dark'] .generated-content .citation-target-highlight {
background: rgba(52, 211, 153, 0.16);
box-shadow: 0 0 0 1px rgba(52, 211, 153, 0.34);
}
</style>