Files
YG_FT/frontend/src/views/data-process/create/QualityRadarPopover.vue

253 lines
6.0 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed } from 'vue'
import VChart from 'vue-echarts'
import '@/plugins/echarts'
import type { EChartsOption } from 'echarts'
import type { ResultQualityDetails } from './types'
const props = defineProps<{
quality: ResultQualityDetails
score?: number
}>()
// 轴标签按词意预置断行,避免长中文标签把雷达网格挤偏或被机械切词。
const JUDGE_DIMENSION_LABELS: Record<string, string> = {
faithfulness: '忠实度',
correctness: '正确性',
clarity: '问题\n清晰度',
completeness: '回答\n完整性',
alignment: '指令\n对齐',
reasoning_validity: '推理\n有效性',
chosen_quality: 'chosen\n质量',
rejected_quality: 'rejected\n质量',
preference_reasonableness: '偏好\n区分',
}
const SEMANTIC_DIMENSION_LABELS: Record<string, string> = {
question_answer: '问答\n相关',
answer_source: '来源\n覆盖',
}
interface RadarDimension {
name: string
value: number
}
const radarDimensions = computed<RadarDimension[]>(() => {
const dimensions: RadarDimension[] = []
for (const [key, value] of Object.entries(props.quality?.judge?.scores ?? {})) {
dimensions.push({
name: JUDGE_DIMENSION_LABELS[key] ?? key,
value: Math.round(value * 20),
})
}
for (const [key, value] of Object.entries(props.quality?.semantic ?? {})) {
if (key === 'overall' || typeof value !== 'number') continue
dimensions.push({
name: SEMANTIC_DIMENSION_LABELS[key] ?? key,
value: Math.round(value),
})
}
return dimensions
})
// 可用维度太少时雷达图失去意义,降级为分层分数展示。
const showRadar = computed(() => radarDimensions.value.length >= 3)
const radarOption = computed<EChartsOption>(() => ({
radar: {
indicator: radarDimensions.value.map((dimension) => ({
name: dimension.name,
max: 100,
})),
radius: '56%',
center: ['50%', '50%'],
splitNumber: 4,
axisName: {
color: '#667085',
fontSize: 10,
lineHeight: 13,
},
splitArea: { areaStyle: { color: ['#fbfbfd', '#f2f4f8'] } },
splitLine: { lineStyle: { color: '#e4e7ec' } },
axisLine: { lineStyle: { color: '#e4e7ec' } },
},
series: [{
type: 'radar',
symbol: 'circle',
symbolSize: 3,
data: [{
value: radarDimensions.value.map((dimension) => dimension.value),
name: '质量维度',
areaStyle: { color: 'rgba(91, 80, 242, 0.18)' },
lineStyle: { color: '#5b50f2', width: 1.5 },
itemStyle: { color: '#5b50f2' },
}],
}],
}))
const layerScores = computed(() => {
const layers = props.quality?.layers ?? {}
return [
{ label: '规则层', value: layers.rule },
{ label: '语义层', value: layers.semantic },
{ label: '评审层', value: layers.judge },
].filter((layer): layer is { label: string; value: number } => (
typeof layer.value === 'number'
))
})
const displayScore = computed(() => {
if (typeof props.score === 'number' && !isNaN(props.score)) {
return props.score.toFixed(1)
}
return null
})
const scoreTone = computed(() => {
const numScore = props.score ?? 0
return numScore >= 80 ? 'is-success' : numScore >= 60 ? 'is-warning' : 'is-danger'
})
</script>
<template>
<div class="quality-popover">
<div v-if="displayScore !== null" class="popover-header">
<strong>质量评测</strong>
<span class="popover-score" :class="scoreTone">{{ displayScore }}</span>
</div>
<VChart
v-if="showRadar"
class="quality-radar"
:option="radarOption"
autoresize
/>
<div v-else class="radar-fallback">
维度数据不足已评测维度少于 3 个时以分层分数为准
</div>
<div class="layer-scores">
<div v-for="layer in layerScores" :key="layer.label" class="layer-item">
<span>{{ layer.label }}</span>
<el-progress
:percentage="Math.round(layer.value)"
:stroke-width="6"
:show-text="false"
:color="layer.value >= 80 ? '#12b76a' : layer.value >= 60 ? '#f0b429' : '#d92d20'"
/>
<em>{{ layer.value.toFixed(0) }}</em>
</div>
</div>
<p v-if="quality?.judge?.reason" class="judge-reason">{{ quality.judge.reason }}</p>
<div v-if="quality?.judge?.issues?.length" class="judge-issues">
<span v-for="issue in quality.judge.issues" :key="issue" class="issue-tag">{{ issue }}</span>
</div>
<div v-if="quality?.judge?.model" class="judge-model">评审模型{{ quality.judge.model }}</div>
</div>
</template>
<style scoped lang="scss">
.quality-popover {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
box-sizing: border-box;
}
.popover-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 6px;
border-bottom: 1px solid #f2f4f7;
strong {
color: #344054;
font-size: 13px;
}
}
.popover-score {
color: #344054;
font-size: 18px;
font-weight: 700;
font-variant-numeric: tabular-nums;
&.is-success { color: #12b76a; }
&.is-warning { color: #d99b0b; }
&.is-danger { color: #d92d20; }
}
.quality-radar {
width: 100%;
height: 220px;
box-sizing: border-box;
}
.radar-fallback {
padding: 18px 10px;
color: #98a2b3;
font-size: 12px;
text-align: center;
}
.layer-scores {
display: flex;
flex-direction: column;
gap: 6px;
}
.layer-item {
display: grid;
grid-template-columns: 44px 1fr 28px;
align-items: center;
gap: 8px;
color: #667085;
font-size: 11px;
em {
color: #344054;
font-style: normal;
font-weight: 600;
text-align: right;
font-variant-numeric: tabular-nums;
}
}
.judge-reason {
margin: 0;
color: #475467;
font-size: 12px;
line-height: 1.6;
}
.judge-issues {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.issue-tag {
padding: 2px 8px;
color: #b54708;
background: #fef0c7;
border-radius: 3px;
font-size: 11px;
}
.judge-model {
color: #98a2b3;
font-size: 11px;
}
</style>
<style lang="scss">
.quality-radar-popper {
padding: 14px 16px !important;
}
</style>