Files
tps-dashboard/frontend/src/components/Analytics/DiagnosisSummary.jsx
T

214 lines
6.5 KiB
React
Raw Normal View History

import { useState, useEffect, useMemo } from 'react'
import { Spin } from 'antd'
import { useNavigate } from 'react-router-dom'
import { generateDiagnosisReport } from '../../services/analyticsService'
/**
* AI 诊断报告摘要块 — 收视分析页内嵌
* - 根据 avgShare 判定三档 tier
* - 调用 DeepSeek 生成报告
* - 从 markdown 提取核心发现和行动建议
*/
/** 从 markdown 中提取指定章节内容 */
function extractSection(markdown, startMarker, endMarkers) {
if (!markdown) return ''
const startIdx = markdown.indexOf(startMarker)
if (startIdx === -1) return ''
const afterStart = startIdx + startMarker.length
// 找最近的结束标记
let endIdx = markdown.length
for (const marker of endMarkers) {
const idx = markdown.indexOf(marker, afterStart)
if (idx !== -1 && idx < endIdx) {
endIdx = idx
}
}
return markdown.substring(afterStart, endIdx).trim()
}
/** 清理 markdown 为纯文本(去掉标题标记、多余符号等) */
function cleanMarkdown(text) {
if (!text) return []
return text
.split('\n')
.map((line) => line.replace(/^#+\s*/, '').replace(/^\*\*(.+?)\*\*/, '$1').trim())
.filter((line) => line.length > 0)
}
const TIER_CONFIG = {
danger: {
leftTitle: '问题聚焦',
rightTitle: '病因与预警',
color: '#7aa874',
rightBg: 'rgba(122,168,116,0.06)',
},
on_target: {
leftTitle: '核心发现',
rightTitle: '病因与提振建议',
color: '#5b8db8',
rightBg: 'rgba(91,141,184,0.06)',
},
excellent: {
leftTitle: '高光复盘',
rightTitle: '经验总结',
color: '#c0584f',
rightBg: 'rgba(192,88,79,0.06)',
},
}
function DiagnosisSummary({ episodes, yearlyTarget, selectedYear }) {
const navigate = useNavigate()
const [report, setReport] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
// 计算平均份额和 tier
const { avgShare, tier, epStart, epEnd } = useMemo(() => {
if (!episodes || episodes.length === 0 || !yearlyTarget) {
return { avgShare: 0, tier: null, epStart: 0, epEnd: 0 }
}
const withShare = episodes.filter((ep) => ep.audience_share != null)
if (withShare.length === 0) {
return { avgShare: 0, tier: null, epStart: 0, epEnd: 0 }
}
const sum = withShare.reduce((acc, ep) => acc + Number(ep.audience_share), 0)
const avg = sum / withShare.length
const base = Number(yearlyTarget.base_target)
const stretch = Number(yearlyTarget.stretch_target)
let t = 'danger'
if (avg > stretch) t = 'excellent'
else if (avg >= base) t = 'on_target'
const nums = withShare.map((ep) => ep.episode_number)
return {
avgShare: avg,
tier: t,
epStart: Math.min(...nums),
epEnd: Math.max(...nums),
}
}, [episodes, yearlyTarget])
const config = tier ? TIER_CONFIG[tier] : null
// 调用 API
useEffect(() => {
if (!tier || !episodes || episodes.length === 0) return
let cancelled = false
setLoading(true)
setError(null)
setReport(null)
generateDiagnosisReport({
year: selectedYear,
ep_start: epStart,
ep_end: epEnd,
})
.then((data) => {
if (cancelled) return
if (data.error) {
setError(data.error)
} else {
setReport(data)
}
})
.catch((err) => {
if (cancelled) return
setError(err?.response?.data?.detail || '报告生成失败')
})
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true
}
}, [selectedYear, epStart, epEnd, tier, episodes])
// 空态
if (!episodes || episodes.length === 0 || !tier) {
return (
<div className="analytics-chart-card" style={{ textAlign: 'center', padding: '40px 24px' }}>
<h2 className="analytics-chart-title" style={{ textAlign: 'left' }}>AI 诊断报告</h2>
<p style={{ color: '#aaa', fontSize: 14 }}>请先选择包含收视数据的期次范围</p>
</div>
)
}
// 提取核心发现和行动建议
const markdown = report?.report_markdown || ''
// 核心发现:从 "#### 二、核心发现" 到 "#### 三、"
let coreFindings = extractSection(markdown, '#### 二、核心发现', ['#### 三、'])
// 行动建议:从 "#### 四、行动建议" 到末尾
let actions = extractSection(markdown, '#### 四、行动建议', [])
const coreLines = cleanMarkdown(coreFindings)
const actionLines = cleanMarkdown(actions)
return (
<div className="analytics-chart-card">
{/* 标题行 */}
<div className="diagnosis-summary-header">
<h2 className="analytics-chart-title" style={{ marginBottom: 0 }}>AI 诊断报告</h2>
{report && !loading && (
<span
className="diagnosis-summary-link"
onClick={() =>
navigate(`/analytics/report?year=${selectedYear}&start=${epStart}&end=${epEnd}`)
}
>
查看完整报告
</span>
)}
</div>
{/* 内容 */}
{loading ? (
<div className="diagnosis-loading">
<Spin size="small" />
<span style={{ marginLeft: 8 }}>正在生成 AI 诊断报告</span>
</div>
) : error ? (
<div className="diagnosis-loading" style={{ color: '#c0584f' }}>
{error}
</div>
) : report ? (
<div className="diagnosis-summary-content">
{/* 左块 */}
<div
className="diagnosis-summary-left"
style={{ borderColor: config.color }}
>
<div className="diagnosis-block-title">{config.leftTitle}</div>
{coreLines.length > 0 ? (
coreLines.map((line, i) => (
<p key={i} style={{ margin: '0 0 8px 0' }}>{line}</p>
))
) : (
<p>报告已生成请查看完整报告</p>
)}
</div>
{/* 右块 */}
<div
className="diagnosis-summary-right"
style={{ background: config.rightBg }}
>
<div className="diagnosis-block-title">{config.rightTitle}</div>
{actionLines.length > 0 ? (
actionLines.map((line, i) => (
<p key={i} style={{ margin: '0 0 8px 0' }}>{line}</p>
))
) : (
<p>报告已生成请查看完整报告</p>
)}
</div>
</div>
) : null}
</div>
)
}
export default DiagnosisSummary