|
|
<template>
|
|
|
<aside class="training-ai-sidebar">
|
|
|
<section class="ai-panel ai-panel--study">
|
|
|
<h3>
|
|
|
<el-icon><Reading /></el-icon>
|
|
|
AI助学
|
|
|
</h3>
|
|
|
|
|
|
<p class="ai-instruction">点击参考提示词可自动带入问题,AI 将结合当前实训内容给出建议。</p>
|
|
|
<div class="ai-output">
|
|
|
<div class="output-glow"></div>
|
|
|
<p>{{ displayAssistMessage }}</p>
|
|
|
<button v-if="referenceText" type="button" class="reference-block" @click="useReferencePrompt">
|
|
|
<strong>{{ referenceTitle }}</strong>
|
|
|
<span v-html="referenceText"></span>
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-input-shell">
|
|
|
<textarea v-model="assistQuestion" placeholder="可以根据任务要求问我相关问题"></textarea>
|
|
|
<button type="button" class="send-btn" aria-label="发送助学问题" @click="requestAssist">
|
|
|
<el-icon><Top /></el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="ai-panel ai-panel--review">
|
|
|
<h3>
|
|
|
<el-icon><Star /></el-icon>
|
|
|
AI助评
|
|
|
</h3>
|
|
|
|
|
|
<div class="ai-output ai-output--review">
|
|
|
<div class="output-glow"></div>
|
|
|
<p>{{ displayEvalMessage }}</p>
|
|
|
<div v-if="displayScoreVisible" class="score-badge">
|
|
|
综合评分:<strong>{{ displayFinalScore }}</strong> / 100
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<p v-if="currentTaskKey && !hasSavedAnswer" class="assessment-hint">请先至少填写并保存一项实训内容,再请求 AI 助评。</p>
|
|
|
<el-button class="assessment-button" :loading="assessmentLoading" :disabled="!currentTaskKey || !hasSavedAnswer || assessmentLoading" @click="requestEvaluation">AI智能评分</el-button>
|
|
|
</section>
|
|
|
</aside>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
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({
|
|
|
studyTip: {
|
|
|
type: String,
|
|
|
default: "当前任务建议:先明确任务要求,再结合真实业务场景逐步补全实训内容。",
|
|
|
},
|
|
|
referenceTitle: {
|
|
|
type: String,
|
|
|
default: "填写提醒",
|
|
|
},
|
|
|
referenceText: {
|
|
|
type: String,
|
|
|
default: "",
|
|
|
},
|
|
|
assistText: {
|
|
|
type: String,
|
|
|
default: "建议已生成:请围绕任务目标补充关键数据、分析过程和结论依据。",
|
|
|
},
|
|
|
evalMessage: {
|
|
|
type: String,
|
|
|
default: "完成实训内容后,可以请求AI助评获取智能评分和改进建议。",
|
|
|
},
|
|
|
finalScore: {
|
|
|
type: [String, Number],
|
|
|
default: "--",
|
|
|
},
|
|
|
scoreVisible: {
|
|
|
type: Boolean,
|
|
|
default: false,
|
|
|
},
|
|
|
progressItems: {
|
|
|
type: Array,
|
|
|
default: () => [],
|
|
|
},
|
|
|
taskKey: { type: String, default: "" },
|
|
|
hasSavedAnswer: { type: Boolean, default: true },
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits(["request-assist", "request-evaluation"]);
|
|
|
|
|
|
const assistQuestion = ref("");
|
|
|
const reviewQuestion = ref("");
|
|
|
const assistMessage = ref("");
|
|
|
const internalEvalMessage = ref("");
|
|
|
const internalScore = ref("--");
|
|
|
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 displayScoreVisible = computed(() => props.scoreVisible || internalScoreVisible.value || reports.value.score !== null);
|
|
|
const displayFinalScore = computed(() => (props.scoreVisible ? props.finalScore : reports.value.score ?? internalScore.value));
|
|
|
const displayEvalMessage = computed(() => {
|
|
|
if (props.scoreVisible) return props.evalMessage;
|
|
|
return reports.value.assessmentReport || internalEvalMessage.value || props.evalMessage;
|
|
|
});
|
|
|
|
|
|
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);
|
|
|
if (currentTaskKey.value) {
|
|
|
try {
|
|
|
await requestAiTrainingHelp(currentTaskKey.value);
|
|
|
await loadEvaluation();
|
|
|
assistMessage.value = reports.value.helpReport || props.assistText;
|
|
|
return;
|
|
|
} catch (error) {}
|
|
|
}
|
|
|
assistMessage.value = assistQuestion.value ? `围绕“${assistQuestion.value}”,建议先对照任务目标检查信息是否完整,再补充数据依据和分析结论。` : props.assistText;
|
|
|
}
|
|
|
|
|
|
async function requestEvaluation() {
|
|
|
emit("request-evaluation", reviewQuestion.value);
|
|
|
if (!currentTaskKey.value || !props.hasSavedAnswer) {
|
|
|
internalEvalMessage.value = "请先至少填写并保存一项实训内容,再请求 AI 助评。";
|
|
|
return;
|
|
|
}
|
|
|
assessmentLoading.value = true;
|
|
|
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;
|
|
|
} catch (error) {
|
|
|
internalEvalMessage.value = "AI 助评暂时不可用,请稍后重试。";
|
|
|
} finally { assessmentLoading.value = false; }
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.training-ai-sidebar {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 22px;
|
|
|
align-self: start;
|
|
|
min-height: calc(100vh - 106px);
|
|
|
padding: 10px;
|
|
|
border: 1px solid rgba(0, 174, 255, 0.22);
|
|
|
border-radius: 26px;
|
|
|
background:
|
|
|
linear-gradient(180deg, rgba(13, 30, 44, 0.92), rgba(5, 15, 26, 0.96)),
|
|
|
radial-gradient(circle at 50% 0%, rgba(0, 220, 255, 0.16), transparent 42%);
|
|
|
box-shadow:
|
|
|
inset 0 1px 0 rgba(169, 229, 255, 0.12),
|
|
|
0 24px 56px rgba(0, 0, 0, 0.22);
|
|
|
}
|
|
|
|
|
|
.ai-panel {
|
|
|
position: relative;
|
|
|
overflow: hidden;
|
|
|
border: 1px solid rgba(72, 207, 255, 0.34);
|
|
|
border-radius: 22px;
|
|
|
padding: 18px;
|
|
|
background:
|
|
|
linear-gradient(180deg, rgba(12, 32, 49, 0.88), rgba(6, 19, 31, 0.94)),
|
|
|
repeating-linear-gradient(90deg, rgba(95, 221, 255, 0.04) 0 1px, transparent 1px 26px);
|
|
|
box-shadow:
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
|
|
0 18px 36px rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
&::before {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
left: 18px;
|
|
|
top: 0;
|
|
|
width: 38px;
|
|
|
height: 2px;
|
|
|
background: linear-gradient(90deg, #5ef2ff, transparent);
|
|
|
}
|
|
|
|
|
|
h3 {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
margin: 0 0 14px;
|
|
|
color: #ffffff;
|
|
|
font-size: 20px;
|
|
|
font-weight: 900;
|
|
|
letter-spacing: 0;
|
|
|
|
|
|
.el-icon {
|
|
|
color: #77eaff;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.ai-output {
|
|
|
position: relative;
|
|
|
min-height: 220px;
|
|
|
padding: 18px;
|
|
|
border: 1px solid rgba(129, 211, 255, 0.4);
|
|
|
border-radius: 14px;
|
|
|
color: #d9efff;
|
|
|
background:
|
|
|
linear-gradient(180deg, rgba(8, 27, 42, 0.88), rgba(4, 17, 29, 0.92)),
|
|
|
radial-gradient(circle at 18% 12%, rgba(38, 205, 255, 0.12), transparent 32%);
|
|
|
box-shadow: inset 0 0 28px rgba(55, 183, 255, 0.08);
|
|
|
font-size: 14px;
|
|
|
line-height: 1.75;
|
|
|
|
|
|
p {
|
|
|
position: relative;
|
|
|
z-index: 1;
|
|
|
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; }
|
|
|
.assessment-hint { margin: 12px 0 -2px; color: #9fcbe2; font-size: 13px; line-height: 1.6; }
|
|
|
|
|
|
.output-glow {
|
|
|
position: absolute;
|
|
|
inset: 0;
|
|
|
pointer-events: none;
|
|
|
background:
|
|
|
linear-gradient(90deg, rgba(96, 237, 255, 0.1), transparent 22%),
|
|
|
linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 26%);
|
|
|
opacity: 0.75;
|
|
|
}
|
|
|
|
|
|
.reference-block {
|
|
|
position: relative;
|
|
|
z-index: 1;
|
|
|
margin-top: 14px;
|
|
|
padding-top: 14px;
|
|
|
border-top: 1px solid rgba(129, 211, 255, 0.18);
|
|
|
color: #c9e8fb;
|
|
|
|
|
|
strong {
|
|
|
display: block;
|
|
|
margin-bottom: 8px;
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.ai-input-shell {
|
|
|
position: relative;
|
|
|
margin-top: 14px;
|
|
|
|
|
|
textarea {
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
min-height: 142px;
|
|
|
resize: vertical;
|
|
|
padding: 14px 54px 14px 14px;
|
|
|
border: 1px solid rgba(129, 211, 255, 0.36);
|
|
|
border-radius: 12px;
|
|
|
outline: none;
|
|
|
color: #eef8ff;
|
|
|
background: rgba(3, 16, 28, 0.86);
|
|
|
font-size: 14px;
|
|
|
line-height: 1.7;
|
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
|
|
|
|
&::placeholder {
|
|
|
color: rgba(198, 225, 244, 0.62);
|
|
|
}
|
|
|
|
|
|
&:focus {
|
|
|
border-color: rgba(100, 229, 255, 0.78);
|
|
|
box-shadow: 0 0 0 3px rgba(0, 174, 255, 0.14);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.send-btn {
|
|
|
position: absolute;
|
|
|
right: 12px;
|
|
|
bottom: 12px;
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
width: 36px;
|
|
|
height: 36px;
|
|
|
border: 1px solid rgba(116, 232, 255, 0.5);
|
|
|
border-radius: 50%;
|
|
|
color: #ffffff;
|
|
|
background: linear-gradient(135deg, #1da2ff, #6b5cff);
|
|
|
box-shadow: 0 10px 24px rgba(0, 136, 255, 0.34);
|
|
|
cursor: pointer;
|
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
|
&:hover {
|
|
|
transform: translateY(-2px);
|
|
|
box-shadow: 0 14px 30px rgba(0, 174, 255, 0.44);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.score-badge {
|
|
|
position: relative;
|
|
|
z-index: 1;
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: 6px;
|
|
|
margin-top: 14px;
|
|
|
padding: 8px 12px;
|
|
|
border: 1px solid rgba(126, 233, 255, 0.38);
|
|
|
border-radius: 999px;
|
|
|
color: #dff8ff;
|
|
|
background: rgba(11, 42, 56, 0.78);
|
|
|
|
|
|
strong {
|
|
|
color: #7ee9ff;
|
|
|
font-size: 20px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1280px) {
|
|
|
.training-ai-sidebar {
|
|
|
min-height: auto;
|
|
|
}
|
|
|
|
|
|
.ai-output {
|
|
|
min-height: 160px;
|
|
|
}
|
|
|
}
|
|
|
</style>
|