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

235 lines
9.5 KiB
Vue

<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(() => asText(help.value.report || help.value.content || help.value.suggestion));
const assessmentReport = computed(() => asText(assessment.value.report || assessment.value.content || assessment.value.feedback));
const score = computed(() => assessment.value.score ?? evaluation.value.score ?? null);
const scoreCriteria = computed(() => normalizeCriteria(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";
return {
...value,
status: String(value.status || root?.[`${prefix}Status`] || "PENDING").toUpperCase(),
report: value.report || 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 asText(value) {
if (value == null) return "";
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
}
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>