|
|
|
|
|
<template>
|
|
|
|
|
|
<aside class="training-ai-evaluation" aria-label="AI实训评价">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { getAiTrainingEvaluation, requestAiTrainingAssessment, requestAiTrainingHelp } from "@/api/aiTrainingEvaluation";
|
|
|
|
|
|
|
|
|
|
|
|
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 help = computed(() => normalizeOperation(evaluation.value.help || evaluation.value.helpEvaluation, evaluation.value, "help"));
|
|
|
|
|
|
const assessment = computed(() => normalizeOperation(evaluation.value.assessment || evaluation.value.assessmentEvaluation, evaluation.value, "assessment"));
|
|
|
|
|
|
const helpStatus = computed(() => help.value.status);
|
|
|
|
|
|
const assessmentStatus = computed(() => assessment.value.status);
|
|
|
|
|
|
const helpReport = computed(() => formatReport(help.value.report || help.value.content || help.value.suggestion));
|
|
|
|
|
|
const assessmentReport = computed(() => formatReport(assessment.value.report || assessment.value.content || assessment.value.feedback));
|
|
|
|
|
|
const assessmentReportData = computed(() => safeParseJson(evaluation.value.assessmentReportJson));
|
|
|
|
|
|
const score = computed(() => evaluation.value.assessmentScore ?? assessment.value.score ?? assessmentReportData.value?.score ?? evaluation.value.score ?? null);
|
|
|
|
|
|
const scoreCriteria = computed(() => normalizeCriteria(assessmentReportData.value?.criteria || assessment.value.scoreCriteria || evaluation.value.scoreCriteria));
|
|
|
|
|
|
const isHelpDisabled = computed(() => !props.hasAnyAnswer || helpLoading.value || isTerminal(helpStatus.value));
|
|
|
|
|
|
const isAssessmentDisabled = computed(() => !props.hasAnyAnswer || assessmentLoading.value || isTerminal(assessmentStatus.value));
|
|
|
|
|
|
const helpActionLabel = computed(() => actionLabel(helpStatus.value, "生成助学建议", helpLoading.value));
|
|
|
|
|
|
const assessmentActionLabel = computed(() => 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 normalizeOperation(operation, root, type) {
|
|
|
|
|
|
const value = operation && typeof operation === "object" ? operation : {};
|
|
|
|
|
|
const prefix = type === "help" ? "help" : "assessment";
|
|
|
|
|
|
const flatReportKey = type === "help" ? "helpReportJson" : "assessmentReportJson";
|
|
|
|
|
|
return {
|
|
|
|
|
|
...value,
|
|
|
|
|
|
status: String(value.status || root?.[`${prefix}Status`] || "PENDING").toUpperCase(),
|
|
|
|
|
|
report: value.report || root?.[flatReportKey] || root?.[`${prefix}Report`] || "",
|
|
|
|
|
|
scoreCriteria: value.scoreCriteria || root?.scoreCriteria || [],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeCriteria(value) {
|
|
|
|
|
|
if (!Array.isArray(value)) return [];
|
|
|
|
|
|
return value.map((item) => {
|
|
|
|
|
|
if (typeof item === "string") return { name: item, score: "", description: "" };
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: String(item?.name || item?.criterion || item?.title || "评分项"),
|
|
|
|
|
|
score: item?.score ?? item?.weight ?? "",
|
|
|
|
|
|
description: String(item?.description || item?.detail || ""),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatReport(value) {
|
|
|
|
|
|
if (value == null) return "";
|
|
|
|
|
|
const parsed = typeof value === "string" ? safeParseJson(value) : value;
|
|
|
|
|
|
if (parsed == null) return typeof value === "string" ? value : "";
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.stringify(parsed, null, 2);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function safeParseJson(value) {
|
|
|
|
|
|
if (typeof value !== "string" || !value.trim()) return null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(value);
|
|
|
|
|
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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>
|