feat(frontend): add review score calibration

This commit is contained in:
2026-07-09 23:58:09 +08:00
parent 6f0a4c3d63
commit 6b8ccd5a35
2 changed files with 24 additions and 5 deletions
+23 -4
View File
@@ -66,9 +66,7 @@
<p>{{ turn.text }}</p>
<div v-if="hasAudio(turn)" class="dialogue-audio-row">
<audio v-if="audioSource(turn, index)" controls :src="audioSource(turn, index)" />
<el-button v-else size="small" :loading="audioLoading[audioKey(index)]" @click.stop="loadAudio(turn, index)">
加载录音
</el-button>
<el-button v-else size="small" :loading="audioLoading[audioKey(index)]" @click.stop="loadAudio(turn, index)">加载录音</el-button>
<span v-if="audioErrors[audioKey(index)]" class="audio-error">{{ audioErrors[audioKey(index)] }}</span>
</div>
</div>
@@ -92,6 +90,14 @@
<el-form-item label="贡献学分">
<el-input-number v-model="reviewForm.incentivePoint" :min="0" :max="100" />
</el-form-item>
<el-form-item label="人工校准总分">
<el-input-number v-model="reviewForm.correctedTotal" :min="0" :max="100" />
</el-form-item>
</div>
<el-form-item label="校准原因">
<el-input v-model="reviewForm.calibrationReason" placeholder="例如:AI 对安抚情绪权重偏低" />
</el-form-item>
<div class="form-grid">
<el-form-item label="下次训练">
<el-select v-model="reviewForm.nextScenarioId" clearable placeholder="不派发">
<el-option v-for="scenario in scenarios" :key="scenario.id" :label="scenario.name" :value="scenario.id" />
@@ -116,7 +122,7 @@
import { Refresh } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
import { calibratePracticeSession, listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
import {
getPracticeAudioBlobUrl,
getOrgSnapshot,
@@ -144,6 +150,8 @@ const reviewForm = reactive({
reviewer: '主管',
advice: '',
incentivePoint: 10,
correctedTotal: 0,
calibrationReason: '',
nextScenarioId: '',
nextReason: ''
});
@@ -155,6 +163,8 @@ type ReviewDialogueTurn = ReviewDetail['dialogue'][number];
const audioKey = (index: number) => `${detail.value?.id || 'review'}-${index}`;
const hasAudio = (turn: ReviewDialogueTurn) => Boolean(turn.audioUrl || turn.audioOssId);
const audioSource = (turn: ReviewDialogueTurn, index: number) => audioSources.value[audioKey(index)] || (!turn.audioOssId ? turn.audioUrl || '' : '');
const needsCalibration = () =>
Boolean(detail.value?.sessionId) && (reviewForm.correctedTotal !== detail.value.score || Boolean(reviewForm.calibrationReason.trim()));
const releaseAudioSources = () => {
Object.values(audioSources.value).forEach((url) => URL.revokeObjectURL(url));
@@ -224,6 +234,8 @@ const selectReview = async (row: ReviewRecord) => {
detail.value = result.data;
reviewForm.advice = result.data.reviewAdvice || '';
reviewForm.incentivePoint = result.data.incentivePoint || 10;
reviewForm.correctedTotal = result.data.score;
reviewForm.calibrationReason = '';
reviewForm.nextScenarioId = '';
reviewForm.nextReason = '';
};
@@ -243,6 +255,13 @@ const submitReview = async () => {
if (!detail.value) return;
saving.value = true;
try {
if (needsCalibration()) {
await calibratePracticeSession(detail.value.sessionId, {
reviewer: reviewForm.reviewer,
correctedTotal: reviewForm.correctedTotal,
reason: reviewForm.calibrationReason
});
}
await markPracticeReviewed(detail.value.id, {
reviewer: reviewForm.reviewer,
advice: reviewForm.advice,