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/TrainingAiSidebar.vue

347 lines
10 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>