fix(frontend): report overall NaN + header card styling
All checks were successful
CI / test (push) Successful in 4m1s

- NaN root cause: report.scores is nested { overall, dimensions: {...} };
  the old code treated every value as a number, so Number({object}) → NaN.
  Now: dimScores from scores.dimensions, overall prefers scores.overall.
- Header summary cards restyled: icon + colored big number + responsive
  (xs=12 md=6), colored score (>0.8 green / 0.6-0.8 amber / else red).
This commit is contained in:
sinohqb 2026-08-17 02:01:45 +08:00
parent 0c1e4578c9
commit 6309b6abca

View File

@ -1,8 +1,11 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { import {
Button, Card, Col, Collapse, Empty, Row, Space, Spin, Statistic, Tag, message, Button, Card, Col, Collapse, Empty, Row, Space, Spin, Tag, message,
} from 'antd' } from 'antd'
import { ArrowLeftOutlined, DownloadOutlined } from '@ant-design/icons' import {
ArrowLeftOutlined, BulbOutlined, DownloadOutlined, MessageOutlined,
StarOutlined, WarningOutlined,
} from '@ant-design/icons'
import { Bar } from '@ant-design/charts' import { Bar } from '@ant-design/charts'
import ChatBubble from '../ChatBubble' import ChatBubble from '../ChatBubble'
import { import {
@ -125,16 +128,28 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
const findings = [...(report?.findings ?? [])] const findings = [...(report?.findings ?? [])]
.sort((a, b) => severityOf(a.severity).order - severityOf(b.severity).order) .sort((a, b) => severityOf(a.severity).order - severityOf(b.severity).order)
const scores = Object.entries(report?.scores ?? {}) // report.scores 是嵌套结构:{ overall: number, dimensions: { 维度: 分 } }
const overall = scores.length > 0 // 只把 dimensions 的值当作数字维度overall 单独取,避免 Number(对象) 产生 NaN。
? (scores.reduce((sum, [, v]) => sum + Number(v), 0) / scores.length).toFixed(1) const rawScores = report?.scores ?? {}
: null const dimScores = Object.entries(rawScores.dimensions ?? {}).filter(
([, v]) => typeof v === 'number',
)
const rawOverall = rawScores.overall
const overall = typeof rawOverall === 'number'
? rawOverall.toFixed(1)
: dimScores.length > 0
? (dimScores.reduce((sum, [, v]) => sum + Number(v), 0) / dimScores.length).toFixed(1)
: null
const overallNum = overall != null ? Number(overall) : null
const overallColor = overallNum == null ? colors.textMuted
: overallNum >= 0.8 ? '#52c41a'
: overallNum >= 0.6 ? colors.warning : '#ff4d4f'
const sevCounts = findings.reduce((acc, f) => { const sevCounts = findings.reduce((acc, f) => {
acc[f.severity] = (acc[f.severity] ?? 0) + 1 acc[f.severity] = (acc[f.severity] ?? 0) + 1
return acc return acc
}, {} as Record<string, number>) }, {} as Record<string, number>)
const scoreData = scores.map(([dimension, score]) => ({ dimension, score: Number(score) })) const scoreData = dimScores.map(([dimension, score]) => ({ dimension, score: Number(score) }))
const scoreConfig = { const scoreConfig = {
data: scoreData, data: scoreData,
xField: 'dimension', xField: 'dimension',
@ -144,6 +159,19 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
color: colors.primary, color: colors.primary,
} }
const summaryCard: React.CSSProperties = {
display: 'flex', alignItems: 'center', gap: 10,
border: `1px solid ${colors.border}`,
borderRadius: 10, padding: '14px 16px', background: colors.bgContainer,
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
height: '100%',
}
const summaryIcon = (color: string): React.CSSProperties => ({
width: 42, height: 42, borderRadius: 10, flexShrink: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: `${color}1a`, color, fontSize: 20,
})
return ( return (
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}> <div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
@ -167,23 +195,53 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
{report && ( {report && (
<> <>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}> <Row gutter={[12, 12]} style={{ marginBottom: 16 }}>
<Col span={6}> <Col xs={12} md={6}>
<Card size="small"><Statistic title="综合评分" value={overall ?? '—'} /></Card> <div style={summaryCard}>
</Col> <div style={summaryIcon(overallColor)}><StarOutlined /></div>
<Col span={6}> <div>
<Card size="small"> <div style={{ fontSize: 12, color: colors.textMuted }}></div>
<Statistic title="问题发现" value={findings.length} /> <div style={{ fontSize: 26, fontWeight: 700, color: overallColor, lineHeight: 1.2 }}>
<div style={{ fontSize: 12, color: colors.textMuted }}> {overall ?? '—'}
{sevCounts.high ?? 0} · {sevCounts.medium ?? 0} · {sevCounts.low ?? 0} </div>
</div> </div>
</Card> </div>
</Col> </Col>
<Col span={6}> <Col xs={12} md={6}>
<Card size="small"><Statistic title="亮点" value={report.highlights.length} /></Card> <div style={summaryCard}>
<div style={summaryIcon('#ff4d4f')}><WarningOutlined /></div>
<div>
<div style={{ fontSize: 12, color: colors.textMuted }}></div>
<div style={{ fontSize: 26, fontWeight: 700, color: colors.text, lineHeight: 1.2 }}>
{findings.length}
</div>
<div style={{ fontSize: 12, color: colors.textMuted }}>
{sevCounts.high ?? 0} · {sevCounts.medium ?? 0} · {sevCounts.low ?? 0}
</div>
</div>
</div>
</Col> </Col>
<Col span={6}> <Col xs={12} md={6}>
<Card size="small"><Statistic title="会话数" value={ev.session_count} /></Card> <div style={summaryCard}>
<div style={summaryIcon(colors.warning)}><BulbOutlined /></div>
<div>
<div style={{ fontSize: 12, color: colors.textMuted }}></div>
<div style={{ fontSize: 26, fontWeight: 700, color: colors.text, lineHeight: 1.2 }}>
{report.highlights.length}
</div>
</div>
</div>
</Col>
<Col xs={12} md={6}>
<div style={summaryCard}>
<div style={summaryIcon(colors.primary)}><MessageOutlined /></div>
<div>
<div style={{ fontSize: 12, color: colors.textMuted }}></div>
<div style={{ fontSize: 26, fontWeight: 700, color: colors.text, lineHeight: 1.2 }}>
{ev.session_count}
</div>
</div>
</div>
</Col> </Col>
</Row> </Row>