253 lines
6.0 KiB
Vue
253 lines
6.0 KiB
Vue
|
|
<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>
|
|||
|
|
|