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

253 lines
10 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 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>