diff --git a/.superpowers/sdd/task-4-report.md b/.superpowers/sdd/task-4-report.md new file mode 100644 index 0000000..0022f65 --- /dev/null +++ b/.superpowers/sdd/task-4-report.md @@ -0,0 +1,15 @@ +# Task 4: AI training evaluation frontend + +## Delivered + +- Added the AI training evaluation API client for status, help, and assessment operations. +- Added `TrainingAiEvaluation.vue` with distinct AI助学 and AI助评 controls, backend status display, confirmation before assessment, retryable errors, safe interpolated report rendering, and score criteria. +- Integrated the component into `GenericTrainingPage.vue` before the legacy AI sidebar and derived `hasAnyAnswer` from form fields, work rows, and process rows. +- Added a static contract test for this integration. + +## Verification + +- `node tests/ai-training-evaluation.static.test.cjs` +- `npm run build:prod` + +The production build emits pre-existing warnings for deprecated `::v-deep` syntax, a runtime-resolved navbar image, and oversized chunks; it exits successfully. diff --git a/src/api/aiTrainingEvaluation.js b/src/api/aiTrainingEvaluation.js new file mode 100644 index 0000000..413cae0 --- /dev/null +++ b/src/api/aiTrainingEvaluation.js @@ -0,0 +1,24 @@ +import request from "@/utils/request"; + +export function getAiTrainingEvaluation(taskKey) { + return request({ + url: `/api/student/training-tasks/${taskKey}/ai-evaluation`, + method: "get", + }); +} + +export function requestAiTrainingHelp(taskKey) { + return request({ + url: `/api/student/training-tasks/${taskKey}/ai-evaluation/help`, + method: "post", + headers: { repeatSubmit: false }, + }); +} + +export function requestAiTrainingAssessment(taskKey) { + return request({ + url: `/api/student/training-tasks/${taskKey}/ai-evaluation/assessment`, + method: "post", + headers: { repeatSubmit: false }, + }); +} diff --git a/src/views/components/TrainingAiEvaluation.vue b/src/views/components/TrainingAiEvaluation.vue new file mode 100644 index 0000000..ee21315 --- /dev/null +++ b/src/views/components/TrainingAiEvaluation.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/src/views/training/GenericTrainingPage.vue b/src/views/training/GenericTrainingPage.vue index e1cb499..bb148fa 100644 --- a/src/views/training/GenericTrainingPage.vue +++ b/src/views/training/GenericTrainingPage.vue @@ -146,10 +146,14 @@ - +
+ + + +
@@ -158,6 +162,7 @@ import { Download, Upload } from "@element-plus/icons-vue"; import { getTrainingTaskByKey } from "@/api/trainingTask"; import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"; import { getStudentTrainingPage } from "./studentTrainingPages"; +import TrainingAiEvaluation from "@/views/components/TrainingAiEvaluation.vue"; import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; @@ -188,6 +193,11 @@ const buildInitialProcessRows = () => const form = ref(buildInitialForm()); const workRows = ref(buildInitialRows()); const processRows = ref(buildInitialProcessRows()); +const hasAnyAnswer = computed(() => + Object.values(form.value).some(hasMeaningfulAnswer) || + workRows.value.some((row) => hasMeaningfulAnswer(row.answer)) || + processRows.value.some((row) => hasMeaningfulAnswer(row.workContent)) +); const uploadInputRef = ref(null); const saving = ref(false); const uploading = ref(false); @@ -272,6 +282,11 @@ function parseArray(value) { } } +function hasMeaningfulAnswer(value) { + if (typeof value === "string") return value.trim().length > 0; + return value !== null && value !== undefined && value !== ""; +} + function buildOutcome() { return { title: page.value.title, @@ -466,6 +481,13 @@ function exportOutcome() { backdrop-filter: blur(8px); } +.training-ai-tools { + display: flex; + flex-direction: column; + gap: 22px; + min-width: 0; +} + .training-core { padding: 28px; border-radius: 36px; diff --git a/tests/ai-training-evaluation.static.test.cjs b/tests/ai-training-evaluation.static.test.cjs new file mode 100644 index 0000000..50712bb --- /dev/null +++ b/tests/ai-training-evaluation.static.test.cjs @@ -0,0 +1,23 @@ +const assert = require("assert"); +const fs = require("fs"); +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 page = fs.readFileSync(path.join(root, "src/views/training/GenericTrainingPage.vue"), "utf8"); + +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(/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"); +assert(/PROCESSING/.test(component) && /SUCCEEDED/.test(component), "AI evaluation should surface backend statuses"); +assert(/!props\.hasAnyAnswer/.test(component), "AI actions should disable when no answer is available"); +assert(/proxy\?\.\$modal\?\.confirm/.test(component), "assessment should request confirmation through the modal proxy"); +assert(!/v-html/.test(component), "AI reports must be rendered as interpolated text, never v-html"); +assert(/scoreCriteria/.test(component), "assessment should display score criteria"); +assert(/retry/.test(component) || /重试/.test(component), "failed AI operations should expose a retry action");