feat: unify student AI learning and assessment panel

dev-QQq
chenyuan 4 weeks ago
parent 01af770d8e
commit b6be9d0eac

@ -6,13 +6,14 @@
AI助学 AI助学
</h3> </h3>
<p class="ai-instruction">点击参考提示词可自动带入问题AI 将结合当前实训内容给出建议</p>
<div class="ai-output"> <div class="ai-output">
<div class="output-glow"></div> <div class="output-glow"></div>
<p>{{ displayAssistMessage }}</p> <p>{{ displayAssistMessage }}</p>
<div v-if="referenceText" class="reference-block"> <button v-if="referenceText" type="button" class="reference-block" @click="useReferencePrompt">
<strong>{{ referenceTitle }}</strong> <strong>{{ referenceTitle }}</strong>
<span v-html="referenceText"></span> <span v-html="referenceText"></span>
</div> </button>
</div> </div>
<div class="ai-input-shell"> <div class="ai-input-shell">
@ -29,7 +30,7 @@
AI助评 AI助评
</h3> </h3>
<div class="ai-output"> <div class="ai-output ai-output--review">
<div class="output-glow"></div> <div class="output-glow"></div>
<p>{{ displayEvalMessage }}</p> <p>{{ displayEvalMessage }}</p>
<div v-if="displayScoreVisible" class="score-badge"> <div v-if="displayScoreVisible" class="score-badge">
@ -37,18 +38,17 @@
</div> </div>
</div> </div>
<div class="ai-input-shell"> <el-button class="assessment-button" :loading="assessmentLoading" :disabled="!currentTaskKey" @click="requestEvaluation">AI</el-button>
<textarea v-model="reviewQuestion" placeholder="请求AI助评智能打分"></textarea>
<button type="button" class="send-btn" aria-label="AI" @click="requestEvaluation">
<el-icon><Top /></el-icon>
</button>
</div>
</section> </section>
</aside> </aside>
</template> </template>
<script setup> <script setup>
import { Reading, Star, Top } from "@element-plus/icons-vue"; import { Reading, Star, Top } from "@element-plus/icons-vue";
import { getAiTrainingEvaluation, requestAiTrainingAssessment, requestAiTrainingHelp } from "@/api/aiTrainingEvaluation";
import { normalizeEvaluationReports } from "./trainingAiEvaluationReport";
import { resolveTrainingTaskKey } from "@/views/training/taskKeyMap";
import { buildReferencePrompt } from "./studentTrainingAiPanel";
const props = defineProps({ const props = defineProps({
studyTip: { studyTip: {
@ -83,6 +83,7 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
taskKey: { type: String, default: "" },
}); });
const emit = defineEmits(["request-assist", "request-evaluation"]); const emit = defineEmits(["request-assist", "request-evaluation"]);
@ -93,27 +94,56 @@ const assistMessage = ref("");
const internalEvalMessage = ref(""); const internalEvalMessage = ref("");
const internalScore = ref("--"); const internalScore = ref("--");
const internalScoreVisible = ref(false); const internalScoreVisible = ref(false);
const assessmentLoading = ref(false);
const evaluation = ref({});
const route = useRoute();
const currentTaskKey = computed(() => resolveTrainingTaskKey(route, props.taskKey));
const reports = computed(() => normalizeEvaluationReports(evaluation.value));
const displayAssistMessage = computed(() => assistMessage.value || props.studyTip); const displayAssistMessage = computed(() => assistMessage.value || props.studyTip);
const displayScoreVisible = computed(() => props.scoreVisible || internalScoreVisible.value); const displayScoreVisible = computed(() => props.scoreVisible || internalScoreVisible.value || reports.value.score !== null);
const displayFinalScore = computed(() => (props.scoreVisible ? props.finalScore : internalScore.value)); const displayFinalScore = computed(() => (props.scoreVisible ? props.finalScore : reports.value.score ?? internalScore.value));
const displayEvalMessage = computed(() => { const displayEvalMessage = computed(() => {
if (props.scoreVisible) return props.evalMessage; if (props.scoreVisible) return props.evalMessage;
return internalEvalMessage.value || props.evalMessage; return reports.value.assessmentReport || internalEvalMessage.value || props.evalMessage;
}); });
function requestAssist() { onMounted(loadEvaluation);
watch(currentTaskKey, loadEvaluation);
async function loadEvaluation() {
if (!currentTaskKey.value) return;
try { evaluation.value = (await getAiTrainingEvaluation(currentTaskKey.value))?.data || {}; } catch (error) { evaluation.value = {}; }
}
function useReferencePrompt() {
assistQuestion.value = buildReferencePrompt(props.referenceText);
}
async function requestAssist() {
emit("request-assist", assistQuestion.value); emit("request-assist", assistQuestion.value);
assistMessage.value = assistQuestion.value if (currentTaskKey.value) {
? `围绕“${assistQuestion.value}”,建议先对照任务目标检查信息是否完整,再补充数据依据和分析结论。` try {
: props.assistText; await requestAiTrainingHelp(currentTaskKey.value);
await loadEvaluation();
assistMessage.value = reports.value.helpReport || props.assistText;
return;
} catch (error) {}
}
assistMessage.value = assistQuestion.value ? `围绕“${assistQuestion.value}”,建议先对照任务目标检查信息是否完整,再补充数据依据和分析结论。` : props.assistText;
} }
function requestEvaluation() { async function requestEvaluation() {
emit("request-evaluation", reviewQuestion.value); emit("request-evaluation", reviewQuestion.value);
internalScoreVisible.value = true; if (!currentTaskKey.value) return;
internalScore.value = 88; assessmentLoading.value = true;
internalEvalMessage.value = "已完成初步评估:内容结构基本完整,建议继续补充真实数据来源、分析过程和可执行结论。"; try {
await requestAiTrainingAssessment(currentTaskKey.value);
await loadEvaluation();
internalScoreVisible.value = reports.value.score !== null;
internalScore.value = reports.value.score ?? "--";
internalEvalMessage.value = reports.value.assessmentReport || props.evalMessage;
} finally { assessmentLoading.value = false; }
} }
</script> </script>
@ -194,6 +224,10 @@ function requestEvaluation() {
margin: 0; margin: 0;
} }
} }
.ai-instruction { margin: -4px 0 12px; color: #9fcbe2; font-size: 13px; line-height: 1.6; }
.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; }
.output-glow { .output-glow {
position: absolute; position: absolute;

@ -0,0 +1,4 @@
export function buildReferencePrompt(referenceText) {
const text = String(referenceText || "").trim();
return text ? `请根据以下参考提示词协助我完成当前任务:\n${text}` : "";
}

@ -155,9 +155,8 @@
</main> </main>
<div class="training-ai-tools"> <div class="training-ai-tools">
<TrainingAiEvaluation :task-key="pageKey" :has-any-answer="hasAnyAnswer" />
<TrainingAiSidebar <TrainingAiSidebar
:task-key="pageKey"
:study-tip="`当前任务建议:围绕“${page.title}”先明确产品对象,再按任务流程逐步补全分析内容。`" :study-tip="`当前任务建议:围绕“${page.title}”先明确产品对象,再按任务流程逐步补全分析内容。`"
:progress-items="progressItems" :progress-items="progressItems"
/> />
@ -170,7 +169,6 @@ import { Download, Upload } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask"; import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"; import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { getStudentTrainingPage } from "./studentTrainingPages"; import { getStudentTrainingPage } from "./studentTrainingPages";
import TrainingAiEvaluation from "@/views/components/TrainingAiEvaluation.vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { isStudentDemo } from "@/utils/studentDemo"; import { isStudentDemo } from "@/utils/studentDemo";

@ -0,0 +1,10 @@
import assert from "node:assert/strict";
import { buildReferencePrompt } from "../src/views/components/studentTrainingAiPanel.js";
assert.equal(
buildReferencePrompt("先从案例中拆出核心层、有形层、延伸层。"),
"请根据以下参考提示词协助我完成当前任务:\n先从案例中拆出核心层、有形层、延伸层。"
);
assert.equal(buildReferencePrompt(""), "");
console.log("student training AI panel contract passed");
Loading…
Cancel
Save