You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/components/TrainingAiEvaluation.vue

216 lines
9.0 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<aside class="training-ai-evaluation" aria-label="AI实训评价">
<section v-if="isDemo" class="evaluation-panel"><h3>演示模式</h3><p class="evaluation-panel__hint">为避免产生外部调用和保存评价数据,AI 助学与助评在演示模式下不可用。</p></section>
<template v-else>
<section class="evaluation-panel">
<div class="evaluation-panel__header">
<div>
<p class="evaluation-panel__eyebrow">AI TRAINING SUPPORT</p>
<h3>AI助学</h3>
</div>
<span class="evaluation-status" :class="`evaluation-status--${helpStatus.toLowerCase()}`">{{ statusLabel(helpStatus) }}</span>
</div>
<p class="evaluation-panel__hint">根据当前已填写的实训内容生成针对性的完善建议。</p>
<el-button :loading="helpLoading" :disabled="isHelpDisabled" type="primary" @click="requestHelp">
{{ helpActionLabel }}
</el-button>
<p v-if="helpError" class="evaluation-error">
{{ helpError }}
<button type="button" class="retry-link" @click="retry('help')">重试</button>
</p>
<div v-if="helpReport" class="evaluation-report">
<h4>助学建议</h4>
<pre>{{ helpReport }}</pre>
</div>
</section>
<section class="evaluation-panel">
<div class="evaluation-panel__header">
<div>
<p class="evaluation-panel__eyebrow">AI TRAINING ASSESSMENT</p>
<h3>AI助评</h3>
</div>
<span class="evaluation-status" :class="`evaluation-status--${assessmentStatus.toLowerCase()}`">{{ statusLabel(assessmentStatus) }}</span>
</div>
<p class="evaluation-panel__hint">AI根据已保存的答案评分。修改并保存答案后可重新助评;答案未变时保留上次结果。</p>
<el-button :loading="assessmentLoading" :disabled="isAssessmentDisabled" type="primary" @click="confirmAssessment">
{{ assessmentActionLabel }}
</el-button>
<p v-if="assessmentError" class="evaluation-error">
{{ assessmentError }}
<button type="button" class="retry-link" @click="retry('assessment')">重试</button>
</p>
<div v-if="assessmentReport" class="evaluation-report">
<h4>评价报告</h4>
<p v-if="score !== null" class="evaluation-score">综合得分:<strong>{{ score }}</strong></p>
<pre>{{ assessmentReport }}</pre>
</div>
<div v-if="scoreCriteria.length" class="score-criteria">
<h4>评分标准</h4>
<ul>
<li v-for="(criterion, index) in scoreCriteria" :key="`${criterion.name}-${index}`">
<strong>{{ criterion.name }}</strong>
<span v-if="criterion.score !== ''">{{ criterion.score }}</span>
<span v-if="criterion.description">:{{ criterion.description }}</span>
</li>
</ul>
</div>
</section>
</template>
</aside>
</template>
<script setup>
import { getAiTrainingEvaluation, requestAiTrainingAssessment, requestAiTrainingHelp } from "@/api/aiTrainingEvaluation";
import { normalizeEvaluationReports, normalizeOperation } from "./trainingAiEvaluationReport";
import { isStudentDemo } from "@/utils/studentDemo";
const props = defineProps({
taskKey: { type: String, default: "" },
hasAnyAnswer: { type: Boolean, default: false },
});
const { proxy } = getCurrentInstance();
const evaluation = ref({});
const helpLoading = ref(false);
const assessmentLoading = ref(false);
const helpError = ref("");
const assessmentError = ref("");
const isDemo = computed(() => isStudentDemo());
const normalizedReports = computed(() => normalizeEvaluationReports(evaluation.value));
const help = computed(() => normalizedReports.value.help);
const assessment = computed(() => normalizedReports.value.assessment);
const helpStatus = computed(() => help.value.status);
const assessmentStatus = computed(() => assessment.value.status);
const helpReport = computed(() => normalizedReports.value.helpReport);
const assessmentReport = computed(() => normalizedReports.value.assessmentReport);
const score = computed(() => normalizedReports.value.score);
const scoreCriteria = computed(() => normalizedReports.value.scoreCriteria);
const isHelpDisabled = computed(() => !props.hasAnyAnswer || helpLoading.value || isTerminal(helpStatus.value));
const isAssessmentDisabled = computed(() => !props.hasAnyAnswer || assessmentLoading.value || assessmentStatus.value === "PROCESSING");
const helpActionLabel = computed(() => actionLabel(helpStatus.value, "生成助学建议", helpLoading.value));
const assessmentActionLabel = computed(() => assessmentStatus.value === "SUCCEEDED" && !assessmentLoading.value
? "重新助评"
: actionLabel(assessmentStatus.value, "请求AI助评", assessmentLoading.value));
onMounted(() => loadEvaluation());
watch(
() => props.taskKey,
() => loadEvaluation()
);
async function loadEvaluation() {
evaluation.value = {};
helpError.value = "";
assessmentError.value = "";
if (!props.taskKey) return;
try {
const res = await getAiTrainingEvaluation(props.taskKey);
evaluation.value = res?.data || {};
} catch (error) {
const message = "AI评价状态加载失败,请重试。";
helpError.value = message;
assessmentError.value = message;
}
}
async function requestHelp() {
if (isHelpDisabled.value || !props.taskKey) return;
helpLoading.value = true;
helpError.value = "";
setLocalStatus("help", "PROCESSING");
try {
const res = await requestAiTrainingHelp(props.taskKey);
applyOperationResult("help", res?.data);
await loadEvaluation();
} catch (error) {
setLocalStatus("help", "FAILED");
helpError.value = "AI助学请求失败,可重试。";
} finally {
helpLoading.value = false;
}
}
async function confirmAssessment() {
if (isAssessmentDisabled.value || !props.taskKey) return;
try {
await proxy?.$modal?.confirm("AI将根据当前已保存的答案进行评估,答案未变时保留上次结果,是否继续?", "提示");
} catch (error) {
return;
}
await requestAssessment();
}
async function requestAssessment() {
assessmentLoading.value = true;
assessmentError.value = "";
setLocalStatus("assessment", "PROCESSING");
try {
const res = await requestAiTrainingAssessment(props.taskKey);
applyOperationResult("assessment", res?.data);
await loadEvaluation();
} catch (error) {
setLocalStatus("assessment", "FAILED");
assessmentError.value = "AI助评请求失败,可重试。";
} finally {
assessmentLoading.value = false;
}
}
function retry(type) {
if (type === "help") return requestHelp();
return confirmAssessment();
}
function applyOperationResult(type, result) {
if (!result || typeof result !== "object") return;
evaluation.value = {
...evaluation.value,
[type]: result[type] || result,
};
}
function setLocalStatus(type, status) {
evaluation.value = {
...evaluation.value,
[type]: { ...normalizeOperation(evaluation.value[type], evaluation.value, type), status },
};
}
function isTerminal(status) {
return ["PROCESSING", "SUCCEEDED"].includes(status);
}
function statusLabel(status) {
return ({ PENDING: "待生成", PROCESSING: "生成中", SUCCEEDED: "已完成", FAILED: "生成失败" }[status] || status);
}
function actionLabel(status, fallback, loading) {
if (loading || status === "PROCESSING") return "生成中…";
if (status === "SUCCEEDED") return "已生成";
return fallback;
}
</script>
<style lang="scss" scoped>
.training-ai-evaluation { display: grid; gap: 16px; align-self: start; }
.evaluation-panel { padding: 18px; border: 1px solid rgba(72, 207, 255, .34); border-radius: 20px; color: #d9efff; background: rgba(6, 19, 31, .94); }
.evaluation-panel__header { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; }
.evaluation-panel__header h3, .evaluation-report h4, .score-criteria h4 { margin: 0; color: #fff; }
.evaluation-panel__eyebrow { margin: 0 0 5px; color: #77eaff; font-size: 11px; font-weight: 800; }
.evaluation-panel__hint { margin: 14px 0; line-height: 1.7; }
.evaluation-status { padding: 4px 8px; border-radius: 999px; color: #a9ecff; background: rgba(29, 162, 255, .16); font-size: 12px; white-space: nowrap; }
.evaluation-status--failed { color: #ffb5b5; background: rgba(255, 96, 96, .14); }
.evaluation-status--succeeded { color: #9df4c2; background: rgba(84, 213, 139, .14); }
.evaluation-error { margin: 10px 0 0; color: #ffb5b5; }
.retry-link { margin-left: 8px; border: 0; color: #77eaff; background: transparent; cursor: pointer; text-decoration: underline; }
.evaluation-report, .score-criteria { margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(129, 211, 255, .18); }
.evaluation-report pre { margin: 10px 0 0; white-space: pre-wrap; word-break: break-word; font: inherit; line-height: 1.7; }
.evaluation-score { margin: 10px 0 0; }.evaluation-score strong { color: #7ee9ff; font-size: 22px; }
.score-criteria ul { margin: 10px 0 0; padding-left: 20px; line-height: 1.7; }.score-criteria span { margin-left: 4px; }
</style>