feat: 评测模块新增详情页与创建向导
新增 EvalTaskDetail 与逐样本结果类型,Mock 与适配器补充评测详情接口,路由注册详情页并将维度创建并入向导;EvalCreateView 改为分步向导(任务配置、规则设置、维度表单),新增 EvalDetailView 展示综合得分与样本判定,列表补充详情入口,配套两份回归脚本。
This commit is contained in:
168
frontend/src/views/eval/create/DimensionFormFields.vue
Normal file
168
frontend/src/views/eval/create/DimensionFormFields.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { MdEditor } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
import { EVAL_METHODS, EVAL_METHOD_PROMPTS } from '@/constants/dimension'
|
||||
import { DIMENSION_TYPE_MAP } from '@/constants'
|
||||
import type { DimensionType, ModelItem } from '@/types'
|
||||
|
||||
export interface DimensionFormDraft {
|
||||
name: string
|
||||
type: DimensionType | ''
|
||||
description: string
|
||||
eval_model: string
|
||||
eval_method: string | string[]
|
||||
eval_prompt: string
|
||||
is_active: boolean
|
||||
is_default: boolean
|
||||
bleu_n: number
|
||||
output_precision: number
|
||||
score_min: number
|
||||
score_max: number
|
||||
pass_threshold: number
|
||||
}
|
||||
|
||||
defineProps<{ evalModels: ModelItem[] }>()
|
||||
|
||||
const form = defineModel<DimensionFormDraft>({ required: true })
|
||||
|
||||
const typeOptions = Object.entries(DIMENSION_TYPE_MAP).map(([value, label]) => ({ value, label }))
|
||||
const currentMethods = computed(() => (form.value.type ? EVAL_METHODS[form.value.type] || [] : []))
|
||||
|
||||
/**
|
||||
* 指标类型变化时初始化对应的评估方式和 Prompt。
|
||||
* 同步执行可保证编辑页批量回填时,接口返回值最终覆盖默认值。
|
||||
*/
|
||||
watch(
|
||||
() => form.value.type,
|
||||
(type) => {
|
||||
if (!type) return
|
||||
const firstMethod = EVAL_METHODS[type]?.[0]?.value || ''
|
||||
form.value.eval_method = type === 'text_similarity' ? [firstMethod] : firstMethod
|
||||
if (type !== 'text_similarity' && EVAL_METHOD_PROMPTS[firstMethod]) {
|
||||
form.value.eval_prompt = EVAL_METHOD_PROMPTS[firstMethod]
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
|
||||
/** 评估方式变化时应用对应的预置 Prompt。 */
|
||||
watch(
|
||||
() => form.value.eval_method,
|
||||
(method) => {
|
||||
if (form.value.type === 'text_similarity' || !method) return
|
||||
const selectedMethod = Array.isArray(method) ? method[0] : method
|
||||
if (selectedMethod && EVAL_METHOD_PROMPTS[selectedMethod]) {
|
||||
form.value.eval_prompt = EVAL_METHOD_PROMPTS[selectedMethod]
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form-item label="维度名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入维度名称" maxlength="50" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="指标类型" prop="type">
|
||||
<el-select v-model="form.type" placeholder="请选择指标类型" style="width: 100%">
|
||||
<el-option v-for="option in typeOptions" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="form.type === 'classification' || form.type === 'metric'">
|
||||
<el-form-item label="选择大模型" prop="eval_model">
|
||||
<el-select v-model="form.eval_model" placeholder="请选择评测模型" filterable style="width: 100%">
|
||||
<el-option v-for="model in evalModels" :key="model.id" :label="model.name" :value="model.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="评估方式" prop="eval_method">
|
||||
<el-radio-group v-model="(form.eval_method as string)">
|
||||
<el-radio v-for="method in currentMethods" :key="method.value" :value="method.value">
|
||||
{{ method.name }}
|
||||
<span class="method-desc">{{ method.desc }}</span>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="评估 Prompt" prop="eval_prompt">
|
||||
<MdEditor
|
||||
v-model="form.eval_prompt"
|
||||
language="zh-CN"
|
||||
:toolbars-exclude="['github', 'save']"
|
||||
style="height: 320px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-if="form.type === 'text_similarity'">
|
||||
<el-form-item label="评估方式" prop="eval_method">
|
||||
<el-checkbox-group v-model="(form.eval_method as string[])">
|
||||
<el-checkbox v-for="method in currentMethods" :key="method.value" :value="method.value">
|
||||
{{ method.name }}
|
||||
<span class="method-desc">{{ method.desc }}</span>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="BLEU n-gram">
|
||||
<el-select v-model="form.bleu_n" style="width: 160px">
|
||||
<el-option :value="1" label="1-gram" />
|
||||
<el-option :value="2" label="2-gram" />
|
||||
<el-option :value="3" label="3-gram" />
|
||||
<el-option :value="4" label="4-gram" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="输出精度">
|
||||
<el-select v-model="form.output_precision" style="width: 160px">
|
||||
<el-option :value="1" label="1 位小数" />
|
||||
<el-option :value="2" label="2 位小数" />
|
||||
<el-option :value="3" label="3 位小数" />
|
||||
<el-option :value="4" label="4 位小数" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-if="form.type === 'metric'">
|
||||
<el-form-item label="评分最小值">
|
||||
<el-input-number v-model="form.score_min" :min="0" :step="1" style="width: 160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="评分最大值">
|
||||
<el-input-number v-model="form.score_max" :min="0" :step="1" style="width: 160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="通过阈值">
|
||||
<div style="width: 100%">
|
||||
<el-slider
|
||||
v-model="form.pass_threshold"
|
||||
:min="form.score_min"
|
||||
:max="form.score_max"
|
||||
:step="0.5"
|
||||
show-input
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="启用该维度">
|
||||
<el-switch v-model="form.is_active" />
|
||||
</el-form-item>
|
||||
<el-form-item label="设为默认">
|
||||
<el-switch v-model="form.is_default" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.method-desc {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
159
frontend/src/views/eval/create/EvalRuleSetupStep.vue
Normal file
159
frontend/src/views/eval/create/EvalRuleSetupStep.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import DimensionFormFields, { type DimensionFormDraft } from './DimensionFormFields.vue'
|
||||
import type { Dimension, ModelItem } from '@/types'
|
||||
|
||||
export type EvalRuleMode = 'existing' | 'new' | 'baseline'
|
||||
|
||||
export interface EvalRuleSetupDraft {
|
||||
ruleMode: EvalRuleMode
|
||||
dimension_id: string | number
|
||||
newDimension: DimensionFormDraft
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
dimensions: Dimension[]
|
||||
evalModels: ModelItem[]
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const form = defineModel<EvalRuleSetupDraft>({ required: true })
|
||||
const existingFormRef = ref<FormInstance>()
|
||||
const newFormRef = ref<FormInstance>()
|
||||
|
||||
const existingRules: FormRules = {
|
||||
dimension_id: [{ required: true, message: '请选择评测维度', trigger: 'change' }],
|
||||
}
|
||||
|
||||
const dimensionRules: FormRules<DimensionFormDraft> = {
|
||||
name: [
|
||||
{ required: true, message: '请输入维度名称', trigger: 'blur' },
|
||||
{ max: 50, message: '维度名称不超过 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
type: [{ required: true, message: '请选择指标类型', trigger: 'change' }],
|
||||
eval_model: [{ required: true, message: '请选择大模型', trigger: 'change' }],
|
||||
eval_method: [{ required: true, message: '请选择评估方式', trigger: 'change' }],
|
||||
eval_prompt: [{ required: true, message: '请填写评估 Prompt', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const selectedDimension = computed(() =>
|
||||
props.dimensions.find((dimension) => String(dimension.id) === String(form.value.dimension_id)),
|
||||
)
|
||||
|
||||
async function validate() {
|
||||
if (form.value.ruleMode === 'baseline') return true
|
||||
const target = form.value.ruleMode === 'existing' ? existingFormRef.value : newFormRef.value
|
||||
if (!target) return false
|
||||
try {
|
||||
await target.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ validate })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rule-step">
|
||||
<div class="rule-mode-label">评测规则来源</div>
|
||||
<el-radio-group v-model="form.ruleMode" :disabled="disabled" class="rule-mode-group">
|
||||
<el-radio-button value="existing">选择已有维度</el-radio-button>
|
||||
<el-radio-button value="new">新建评测维度</el-radio-button>
|
||||
<el-radio-button value="baseline">系统基线规则</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<el-form
|
||||
v-if="form.ruleMode === 'existing'"
|
||||
ref="existingFormRef"
|
||||
:model="form"
|
||||
:rules="existingRules"
|
||||
label-width="120px"
|
||||
class="step-form"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<el-form-item label="评测维度" prop="dimension_id">
|
||||
<el-select
|
||||
v-model="form.dimension_id"
|
||||
placeholder="请选择评测维度"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:loading="loading"
|
||||
>
|
||||
<el-option
|
||||
v-for="dimension in dimensions"
|
||||
:key="dimension.id"
|
||||
:label="dimension.name"
|
||||
:value="dimension.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="selectedDimension" label="维度详情">
|
||||
<el-descriptions :column="1" border size="small" style="width: 100%">
|
||||
<el-descriptions-item label="指标类型">{{ selectedDimension.type || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="评估模型">{{ selectedDimension.eval_model || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="评估方法">
|
||||
{{
|
||||
Array.isArray(selectedDimension.eval_method)
|
||||
? selectedDimension.eval_method.join('、')
|
||||
: selectedDimension.eval_method || '-'
|
||||
}}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="selectedDimension.description" label="说明">
|
||||
{{ selectedDimension.description }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-form
|
||||
v-else-if="form.ruleMode === 'new'"
|
||||
ref="newFormRef"
|
||||
:model="form.newDimension"
|
||||
:rules="dimensionRules"
|
||||
label-width="130px"
|
||||
class="step-form step-form-wide"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<DimensionFormFields v-model="form.newDimension" :eval-models="evalModels" />
|
||||
</el-form>
|
||||
|
||||
<el-alert
|
||||
v-else
|
||||
title="系统将使用内置基线规则进行评测,无需额外创建或选择评测维度。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="baseline-alert"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rule-mode-label {
|
||||
margin-bottom: 10px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rule-mode-group {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.step-form {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.step-form-wide {
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.baseline-alert {
|
||||
max-width: 760px;
|
||||
}
|
||||
</style>
|
||||
162
frontend/src/views/eval/create/EvalTaskSetupStep.vue
Normal file
162
frontend/src/views/eval/create/EvalTaskSetupStep.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { DatasetItem, GpuInfo, TrainedModel } from '@/types'
|
||||
|
||||
export interface EvalTaskSetupDraft {
|
||||
eval_task_name: string
|
||||
model_id: string | number
|
||||
gpu_id: string | number
|
||||
data_source: 'dataset' | 'inference'
|
||||
dataset_id: string | number
|
||||
leaderboard: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
trainedModels: TrainedModel[]
|
||||
evalDatasets: DatasetItem[]
|
||||
gpus: GpuInfo[]
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const form = defineModel<EvalTaskSetupDraft>({ required: true })
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const rules: FormRules<EvalTaskSetupDraft> = {
|
||||
eval_task_name: [
|
||||
{ required: true, message: '请输入任务名称', trigger: 'blur' },
|
||||
{ max: 50, message: '任务名称不超过 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
model_id: [{ required: true, message: '请选择评测模型', trigger: 'change' }],
|
||||
gpu_id: [{ required: true, message: '请选择 GPU', trigger: 'change' }],
|
||||
dataset_id: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.value.data_source === 'dataset' && (value === '' || value === null || value === undefined)) {
|
||||
callback(new Error('请选择评测数据集'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
watch(
|
||||
() => form.value.data_source,
|
||||
(source) => {
|
||||
if (source !== 'dataset') {
|
||||
form.value.dataset_id = ''
|
||||
formRef.value?.clearValidate('dataset_id')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async function validate() {
|
||||
if (!formRef.value) return false
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ validate })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
class="step-form"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<el-form-item label="任务名称" prop="eval_task_name">
|
||||
<el-input v-model="form.eval_task_name" placeholder="请输入任务名称" maxlength="50" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="评测模型" prop="model_id">
|
||||
<el-select
|
||||
v-model="form.model_id"
|
||||
placeholder="请选择已训练且已合并的模型"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:loading="loading"
|
||||
>
|
||||
<el-option
|
||||
v-for="model in trainedModels"
|
||||
:key="model.id"
|
||||
:label="model.name"
|
||||
:value="model.id"
|
||||
:disabled="!model.merged"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="field-tip">未完成合并的模型不可用于评测。</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择 GPU" prop="gpu_id">
|
||||
<el-select v-model="form.gpu_id" placeholder="请选择 GPU" style="width: 100%" :loading="loading">
|
||||
<el-option
|
||||
v-for="(gpu, index) in gpus"
|
||||
:key="index"
|
||||
:label="`${gpu.name} (GPU ${index})`"
|
||||
:value="index"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="数据来源">
|
||||
<el-radio-group v-model="form.data_source">
|
||||
<el-radio value="dataset">评测数据集</el-radio>
|
||||
<el-radio value="inference">推理结果集</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.data_source === 'dataset'" label="评测数据集" prop="dataset_id">
|
||||
<el-select
|
||||
v-model="form.dataset_id"
|
||||
placeholder="请选择评测数据集"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:loading="loading"
|
||||
>
|
||||
<el-option v-for="dataset in evalDatasets" :key="dataset.id" :label="dataset.name" :value="dataset.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-alert
|
||||
v-else
|
||||
title="将使用该模型最近一次可用的推理结果集。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="source-alert"
|
||||
/>
|
||||
|
||||
<el-form-item label="加入排行榜">
|
||||
<el-switch v-model="form.leaderboard" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.step-form {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.field-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.source-alert {
|
||||
margin: -4px 0 20px 120px;
|
||||
max-width: 640px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user