From 19e1310ca8e1a51c7196ddea7e99d43c71729101 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Thu, 6 Aug 2026 20:04:36 +0800 Subject: [PATCH] fix: guard AI assessment until saved --- src/views/components/TrainingAiSidebar.vue | 12 ++++++++++-- src/views/training/GenericTrainingPage.vue | 7 +++++++ tests/ai-training-evaluation.static.test.cjs | 8 ++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/views/components/TrainingAiSidebar.vue b/src/views/components/TrainingAiSidebar.vue index 73e1c02..430c279 100644 --- a/src/views/components/TrainingAiSidebar.vue +++ b/src/views/components/TrainingAiSidebar.vue @@ -38,7 +38,8 @@ - AI智能评分 +

请先至少填写并保存一项实训内容,再请求 AI 助评。

+ AI智能评分 @@ -84,6 +85,7 @@ const props = defineProps({ default: () => [], }, taskKey: { type: String, default: "" }, + hasSavedAnswer: { type: Boolean, default: true }, }); const emit = defineEmits(["request-assist", "request-evaluation"]); @@ -135,7 +137,10 @@ async function requestAssist() { async function requestEvaluation() { emit("request-evaluation", reviewQuestion.value); - if (!currentTaskKey.value) return; + if (!currentTaskKey.value || !props.hasSavedAnswer) { + internalEvalMessage.value = "请先至少填写并保存一项实训内容,再请求 AI 助评。"; + return; + } assessmentLoading.value = true; try { await requestAiTrainingAssessment(currentTaskKey.value); @@ -143,6 +148,8 @@ async function requestEvaluation() { internalScoreVisible.value = reports.value.score !== null; internalScore.value = reports.value.score ?? "--"; internalEvalMessage.value = reports.value.assessmentReport || props.evalMessage; + } catch (error) { + internalEvalMessage.value = "AI 助评暂时不可用,请稍后重试。"; } finally { assessmentLoading.value = false; } } @@ -228,6 +235,7 @@ async function requestEvaluation() { .reference-block { width: 100%; border: 0; background: transparent; padding: 14px 0 0; text-align: left; cursor: pointer; } .reference-block:hover strong { color: #77eaff; } .assessment-button { width: 100%; margin-top: 14px; min-height: 44px; font-weight: 800; } +.assessment-hint { margin: 12px 0 -2px; color: #9fcbe2; font-size: 13px; line-height: 1.6; } .output-glow { position: absolute; diff --git a/src/views/training/GenericTrainingPage.vue b/src/views/training/GenericTrainingPage.vue index 701d259..ca66d2e 100644 --- a/src/views/training/GenericTrainingPage.vue +++ b/src/views/training/GenericTrainingPage.vue @@ -157,6 +157,7 @@
@@ -201,6 +202,7 @@ const buildInitialProcessRows = () => const form = ref(buildInitialForm()); const workRows = ref(buildInitialRows()); const processRows = ref(buildInitialProcessRows()); +const hasSavedAnswer = ref(false); const hasAnyAnswer = computed(() => Object.values(form.value).some(hasMeaningfulAnswer) || workRows.value.some((row) => hasMeaningfulAnswer(row.answer)) || @@ -225,6 +227,7 @@ watch( form.value = buildInitialForm(); workRows.value = buildInitialRows(); processRows.value = buildInitialProcessRows(); + hasSavedAnswer.value = false; await loadSavedAnswer(); }, { immediate: true } @@ -352,6 +355,9 @@ async function loadSavedAnswer() { try { const res = await getStudentTrainingAnswer(pageKey.value); const answer = res?.data; + hasSavedAnswer.value = Boolean( + answer && [answer.step1Answer, answer.step2Answer, answer.step3Answer, answer.step4Answer, answer.dynamicStepAnswers].some(hasMeaningfulAnswer) + ); if (answer?.currentStep && Number(answer.currentStep) > 0) { currentStep.value = Number(answer.currentStep); } @@ -377,6 +383,7 @@ async function saveAnswer(saveAction = "SAVE") { saving.value = true; try { await saveStudentTrainingAnswer(pageKey.value, buildAnswerPayload(saveAction)); + hasSavedAnswer.value = hasAnyAnswer.value; proxy?.$modal?.msgSuccess(saveAction === "SUBMIT" ? "提交成功" : "已保存"); } catch (error) { proxy?.$modal?.msgError?.(saveAction === "SUBMIT" ? "提交失败" : "保存失败"); diff --git a/tests/ai-training-evaluation.static.test.cjs b/tests/ai-training-evaluation.static.test.cjs index 131d196..799392c 100644 --- a/tests/ai-training-evaluation.static.test.cjs +++ b/tests/ai-training-evaluation.static.test.cjs @@ -5,14 +5,14 @@ const path = require("path"); const root = path.resolve(__dirname, ".."); const api = fs.readFileSync(path.join(root, "src/api/aiTrainingEvaluation.js"), "utf8"); const component = fs.readFileSync(path.join(root, "src/views/components/TrainingAiEvaluation.vue"), "utf8"); +const sidebar = fs.readFileSync(path.join(root, "src/views/components/TrainingAiSidebar.vue"), "utf8"); const page = fs.readFileSync(path.join(root, "src/views/training/GenericTrainingPage.vue"), "utf8"); const { normalizeEvaluationReports } = require(path.join(root, "src/views/components/trainingAiEvaluationReport.cjs")); assert(/`\/api\/student\/training-tasks\/\$\{taskKey\}\/ai-evaluation`/.test(api), "evaluation API should use the student task endpoint"); assert(/\/help/.test(api) && /\/assessment/.test(api), "evaluation API should expose help and assessment operations"); -assert(/TrainingAiEvaluation/.test(page) && /:has-any-answer="hasAnyAnswer"/.test(page), "generic training page should pass meaningful answer state to AI evaluation"); +assert(/TrainingAiSidebar/.test(page) && /:has-saved-answer="hasSavedAnswer"/.test(page), "generic training page should pass persisted answer state to the AI sidebar"); assert(/form\.value/.test(page) && /workRows\.value/.test(page) && /processRows\.value/.test(page), "hasAnyAnswer should inspect all supported answer shapes"); -assert(/\s*props\.taskKey/.test(component), "AI evaluation should load on mount and task changes"); assert(/AI助学/.test(component) && /AI助评/.test(component), "AI evaluation should offer separate help and assessment controls"); @@ -56,3 +56,7 @@ assert.strictEqual(malformedPayload.assessmentReport, "{also not valid JSON", "m assert.strictEqual(malformedPayload.score, null, "malformed assessment JSON should not produce a score"); assert.deepStrictEqual(malformedPayload.scoreCriteria, [], "malformed assessment JSON should not produce criteria"); assert(/retry/.test(component) || /重试/.test(component), "failed AI operations should expose a retry action"); +assert(/hasSavedAnswer/.test(page) && /:has-saved-answer="hasSavedAnswer"/.test(page), "generic training page should distinguish persisted answers before enabling AI assessment"); +assert(/hasSavedAnswer/.test(sidebar), "AI sidebar should accept persisted-answer state"); +assert(/!props\.hasSavedAnswer/.test(sidebar), "AI assessment should be disabled until a saved answer exists"); +assert(/catch \(error\)/.test(sidebar), "AI sidebar should handle assessment failures without an unhandled rejection");