|
|
@@ -28,6 +28,7 @@
|
|
|
:dialogue="currentDialogue"
|
|
|
:script-roles="scriptRoles"
|
|
|
:index="currentDialogueIndex"
|
|
|
+ :is-playback-started="isPlaybackStarted"
|
|
|
:previous-quest="previousQuestDialogue"
|
|
|
:poem-show="showPoem"
|
|
|
:poem-content="currentPoemContent"
|
|
|
@@ -41,9 +42,9 @@
|
|
|
<!-- 控制按钮 -->
|
|
|
<InputButtons
|
|
|
:can-prev="!isAtFirstDialogue"
|
|
|
- :can-next="!isAtLastDialogue"
|
|
|
+ :can-next="Boolean(currentDialogue)"
|
|
|
@prev="playPrevious"
|
|
|
- @next="playNext"
|
|
|
+ @next="handleNextClick"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -57,6 +58,7 @@
|
|
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
|
|
import { useRouter } from 'vue-router';
|
|
|
import { marked } from 'marked';
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
import { CreateDialogue, sendChatMessageStream } from "@/api/questions.js";
|
|
|
import { useAudioPlayer } from "@/api/tts/useAudioPlayer.js";
|
|
|
|
|
|
@@ -78,7 +80,6 @@ const router = useRouter();
|
|
|
* @prop {Object} scriptData - 剧本数据(包含章节和对话)
|
|
|
* @prop {Array} scriptRoles - 角色列表
|
|
|
* @prop {String} backText - 返回按钮文本
|
|
|
- * @prop {Boolean} isLastCourse - 是否为最后一节课程
|
|
|
*/
|
|
|
const props = defineProps({
|
|
|
scriptData: {
|
|
|
@@ -92,10 +93,6 @@ const props = defineProps({
|
|
|
backText: {
|
|
|
type: String,
|
|
|
default: '返回课程'
|
|
|
- },
|
|
|
- isLastCourse: {
|
|
|
- type: Boolean,
|
|
|
- default: false
|
|
|
}
|
|
|
});
|
|
|
|
|
|
@@ -174,13 +171,16 @@ const isAtFirstDialogue = computed(() => {
|
|
|
return currentSectionIndex.value === 0 && currentDialogueIndex.value === 0;
|
|
|
});
|
|
|
|
|
|
-// 是否在最后一个对话
|
|
|
-const isAtLastDialogue = computed(() => {
|
|
|
- if (!props.scriptData.sections?.length) return false;
|
|
|
- const lastSectionIdx = props.scriptData.sections.length - 1;
|
|
|
- const lastSection = props.scriptData.sections[lastSectionIdx];
|
|
|
+// 是否在当前课程的最后一个对话
|
|
|
+const isAtLastDialogueOfCourse = computed(() => {
|
|
|
+ const sections = props.scriptData.sections;
|
|
|
+ if (!sections?.length) return false;
|
|
|
+
|
|
|
+ const lastSectionIndex = sections.length - 1;
|
|
|
+ const lastSection = sections[lastSectionIndex];
|
|
|
if (!lastSection?.dialogues?.length) return false;
|
|
|
- return currentSectionIndex.value === lastSectionIdx &&
|
|
|
+
|
|
|
+ return currentSectionIndex.value === lastSectionIndex &&
|
|
|
currentDialogueIndex.value === lastSection.dialogues.length - 1;
|
|
|
});
|
|
|
|
|
|
@@ -255,9 +255,7 @@ const playDialogueAudio = (isAutoPlay = false) => {
|
|
|
dialogueAudio.value = audio;
|
|
|
|
|
|
audio.onended = () => {
|
|
|
- if (isAtLastDialogue.value) {
|
|
|
- if (currentDialogue.value?.type === 'user') return;
|
|
|
- emit('dialogueEnded', props.isLastCourse);
|
|
|
+ if (isAtLastDialogueOfCourse.value) {
|
|
|
isPlaying.value = false;
|
|
|
return;
|
|
|
}
|
|
|
@@ -274,9 +272,7 @@ const playDialogueAudio = (isAutoPlay = false) => {
|
|
|
|
|
|
audio.play().catch(e => {
|
|
|
console.error('对话语音播放失败:', e);
|
|
|
- if (isAtLastDialogue.value) {
|
|
|
- if (currentDialogue.value?.type === 'user') return;
|
|
|
- emit('dialogueEnded', props.isLastCourse);
|
|
|
+ if (isAtLastDialogueOfCourse.value) {
|
|
|
isPlaying.value = false;
|
|
|
return;
|
|
|
}
|
|
|
@@ -290,9 +286,7 @@ const playDialogueAudio = (isAutoPlay = false) => {
|
|
|
}
|
|
|
});
|
|
|
} else if (currentDialogue.value?.type !== 'video') {
|
|
|
- if (isAtLastDialogue.value) {
|
|
|
- if (currentDialogue.value?.type === 'user') return;
|
|
|
- emit('dialogueEnded', props.isLastCourse);
|
|
|
+ if (isAtLastDialogueOfCourse.value) {
|
|
|
isPlaying.value = false;
|
|
|
return;
|
|
|
}
|
|
|
@@ -338,10 +332,6 @@ const handleVideoEnded = () => {
|
|
|
if (isPlaying.value) {
|
|
|
setTimeout(() => {
|
|
|
if (!playNext(true)) {
|
|
|
- if (isAtLastDialogue.value) {
|
|
|
- console.log('视频序列:已到达最后一个对话');
|
|
|
- emit('dialogueEnded', props.isLastCourse);
|
|
|
- }
|
|
|
isPlaying.value = false;
|
|
|
stopAllAudio();
|
|
|
}
|
|
|
@@ -368,10 +358,6 @@ const playSequence = () => {
|
|
|
} else {
|
|
|
setTimeout(() => {
|
|
|
if (!playNext(true)) {
|
|
|
- if (isAtLastDialogue.value) {
|
|
|
- console.log('诗词序列:已到达最后一个对话');
|
|
|
- emit('dialogueEnded', props.isLastCourse);
|
|
|
- }
|
|
|
isPlaying.value = false;
|
|
|
stopAllAudio();
|
|
|
}
|
|
|
@@ -445,13 +431,27 @@ const playPrevious = () => {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+/**
|
|
|
+ * 处理用户点击“下一个”按钮
|
|
|
+ */
|
|
|
+const handleNextClick = () => {
|
|
|
+ if (!currentDialogue.value) return;
|
|
|
+
|
|
|
+ if (isAtLastDialogueOfCourse.value) {
|
|
|
+ emit('dialogueEnded');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ playNext(false);
|
|
|
+};
|
|
|
+
|
|
|
/**
|
|
|
* 播放下一条对话
|
|
|
* @param {Boolean} isAutoPlay - 是否自动播放
|
|
|
* @returns {Boolean} 是否成功切换
|
|
|
*/
|
|
|
const playNext = (isAutoPlay = false) => {
|
|
|
- if (isAtLastDialogue.value) return false;
|
|
|
+ if (isAtLastDialogueOfCourse.value) return false;
|
|
|
|
|
|
if (dialogueAudio.value) {
|
|
|
dialogueAudio.value.pause();
|
|
|
@@ -507,7 +507,6 @@ const playNext = (isAutoPlay = false) => {
|
|
|
if (currentDialogue.value?.voiceoverUrl) {
|
|
|
playDialogueAudio(isPlaying.value);
|
|
|
} else {
|
|
|
- // 诗词没有语音,延迟后继续切换下一句(避免连续递归)
|
|
|
setTimeout(() => {
|
|
|
playNext(isAutoPlay);
|
|
|
}, 100);
|
|
|
@@ -574,7 +573,7 @@ const handleKeydown = (event) => {
|
|
|
event.preventDefault();
|
|
|
break;
|
|
|
case 'ArrowRight':
|
|
|
- playNext();
|
|
|
+ handleNextClick();
|
|
|
event.preventDefault();
|
|
|
break;
|
|
|
default:
|
|
|
@@ -585,16 +584,42 @@ const handleKeydown = (event) => {
|
|
|
/**
|
|
|
* 创建AI对话会话
|
|
|
*/
|
|
|
-const createAiChart = async () => {
|
|
|
- let role = props.scriptRoles.find(r => r.name === currentDialogue.value.roleName);
|
|
|
- await CreateDialogue({ roleId: role.id })
|
|
|
- .then(res => {
|
|
|
- console.log("创建会话:", res.data);
|
|
|
- activeConversationId.value = res.data;
|
|
|
- })
|
|
|
- .catch(error => {
|
|
|
- console.error('请求出错:', error);
|
|
|
+const createAiChart = async (preferredRoleName = '') => {
|
|
|
+ activeConversationId.value = null;
|
|
|
+
|
|
|
+ // user 对话通常没有角色,优先使用对应问题的角色,再向前查找最近的有效角色。
|
|
|
+ const roleNames = [preferredRoleName, currentDialogue.value?.roleName];
|
|
|
+ for (let i = currentDialogueIndex.value - 1; i >= 0; i--) {
|
|
|
+ roleNames.push(currentSection.value?.dialogues?.[i]?.roleName);
|
|
|
+ }
|
|
|
+
|
|
|
+ const role = roleNames
|
|
|
+ .filter(Boolean)
|
|
|
+ .map(roleName => props.scriptRoles.find(item => item.name === roleName))
|
|
|
+ .find(Boolean);
|
|
|
+
|
|
|
+ if (!role || role.id == null) {
|
|
|
+ console.error('创建AI会话失败:找不到对话角色', {
|
|
|
+ preferredRoleName,
|
|
|
+ scriptRoles: props.scriptRoles
|
|
|
});
|
|
|
+ ElMessage.error('未找到对应的AI角色,请检查课程角色配置');
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await CreateDialogue({ roleId: role.id });
|
|
|
+ console.log('创建会话:', res.data);
|
|
|
+ activeConversationId.value = res.data;
|
|
|
+ return {
|
|
|
+ conversationId: res.data,
|
|
|
+ roleName: role.name
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('创建AI会话失败:', error);
|
|
|
+ ElMessage.error('创建AI会话失败,请稍后重试');
|
|
|
+ return null;
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
@@ -602,15 +627,25 @@ const createAiChart = async () => {
|
|
|
* @param {Object} data - 用户输入数据
|
|
|
*/
|
|
|
const handleUserInputSubmit = async (data) => {
|
|
|
- console.log('用户输入:', data.content);
|
|
|
- await createAiChart();
|
|
|
+ const content = data?.content?.trim();
|
|
|
+ if (!content) return;
|
|
|
|
|
|
- let userInputTemp = data.content;
|
|
|
- let currentDialogueTemp = currentSection.value.dialogues[currentDialogueIndex.value - 1];
|
|
|
- userInputTemp += "(此内容是帮我解答的问题,问题是:" + currentDialogueTemp.content + ",回复要求:根据问题回复我回答的内容是否正确,并给予鼓励或夸赞;注意请使用精简回答,尽量控制字体数量在50个字内)";
|
|
|
+ const questionDialogue = previousQuestDialogue.value;
|
|
|
+ if (!questionDialogue) {
|
|
|
+ console.error('用户输入提交失败:找不到上一条提问对话');
|
|
|
+ ElMessage.error('找不到对应的问题,请返回上一条后重试');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('用户输入:', content);
|
|
|
+ const session = await createAiChart(questionDialogue.roleName);
|
|
|
+ if (!session) return;
|
|
|
+
|
|
|
+ const userInputTemp = `${content}(此内容是帮我解答的问题,问题是:${questionDialogue.content},回复要求:根据问题回复我回答的内容是否正确,并给予鼓励或夸赞;注意请使用精简回答,尽量控制字体数量在50个字内)`;
|
|
|
|
|
|
await doSendMessageStream({
|
|
|
- conversationId: activeConversationId.value,
|
|
|
+ conversationId: session.conversationId,
|
|
|
+ roleName: session.roleName,
|
|
|
content: userInputTemp,
|
|
|
contentAnswer: null,
|
|
|
});
|
|
|
@@ -625,13 +660,19 @@ const handleWriteReadingSubmit = async (data) => {
|
|
|
const imitation = data.content?.trim();
|
|
|
if (!originalPoem || !imitation) return;
|
|
|
|
|
|
- await createAiChart();
|
|
|
- if (!activeConversationId.value) return;
|
|
|
+ const session = await createAiChart(currentDialogue.value?.roleName);
|
|
|
+ if (!session) return;
|
|
|
|
|
|
- const content = `你是一位古诗词仿写指导老师。请根据“原诗词”和“学生仿写作品”,给出友善、具体、鼓励式的评价。\n\n原诗词:\n${originalPoem}\n\n学生仿写:\n${imitation}\n\n请从主题意境、结构句式、语言表达和创新性四方面评价。先肯定亮点,再指出1~2个可操作的改进建议;不苛求严格格律;控制在150字以内。`;
|
|
|
+ const content = `你是一位古诗词仿写指导老师。请根据“原诗词”和“学生仿写作品”,给出友善、具体、鼓励式的评价。\n\n原诗词:\n${originalPoem}\n\n学生仿写:\n${imitation}\n\n
|
|
|
+ 请从文学价值、感情价值、社会价值三个方面评价。\n\n
|
|
|
+ 1.文学价值:先评价学生的仿写,然后按照格律诗的要求修改这首诗。
|
|
|
+ 2.感情价值:先评价学生的仿写,然后让这首诗更具有怎么样的情感?
|
|
|
+ 3.社会价值:先评价学生的仿写,然后让这首诗更具有正能量,具有推广价值。
|
|
|
+ 控制在150字以内。`;
|
|
|
|
|
|
await doSendMessageStream({
|
|
|
- conversationId: activeConversationId.value,
|
|
|
+ conversationId: session.conversationId,
|
|
|
+ roleName: session.roleName,
|
|
|
content,
|
|
|
contentAnswer: null,
|
|
|
});
|
|
|
@@ -643,14 +684,17 @@ const handleWriteReadingSubmit = async (data) => {
|
|
|
*/
|
|
|
const handleSingleChoiceSubmit = async (data) => {
|
|
|
console.log('用户选择:', data.label);
|
|
|
- await createAiChart();
|
|
|
|
|
|
const dialogue = previousQuestDialogue.value;
|
|
|
if (!dialogue) {
|
|
|
console.error('找不到上一条quest对话!');
|
|
|
+ ElMessage.error('找不到对应的问题,请返回上一条后重试');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ const session = await createAiChart(dialogue.roleName);
|
|
|
+ if (!session) return;
|
|
|
+
|
|
|
const optionLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
|
|
const optionsStr = dialogue.options.map((opt, idx) => `${optionLabels[idx]}. ${opt.content}`).join(';');
|
|
|
const content = `问题:${dialogue.content}\n选项:${optionsStr}\n我的答案:${data.label}\n正确答案:${dialogue.answer}\n\n请判断我的答案是否正确(如果没有正确答案则提示此题没有标准答案),并给予鼓励或夸赞,回复请精简,控制在50字内。`;
|
|
|
@@ -658,7 +702,8 @@ const handleSingleChoiceSubmit = async (data) => {
|
|
|
console.log('发送单选问题:', content);
|
|
|
|
|
|
await doSendMessageStream({
|
|
|
- conversationId: activeConversationId.value,
|
|
|
+ conversationId: session.conversationId,
|
|
|
+ roleName: session.roleName,
|
|
|
content: content,
|
|
|
contentAnswer: null,
|
|
|
});
|
|
|
@@ -667,9 +712,10 @@ const handleSingleChoiceSubmit = async (data) => {
|
|
|
/**
|
|
|
* 显示问题回答对话(AI思考中)
|
|
|
*/
|
|
|
-const showQuestAnswerDialogue = () => {
|
|
|
+const showQuestAnswerDialogue = (roleName) => {
|
|
|
currentDialogueCache.value = JSON.parse(JSON.stringify(currentDialogue.value));
|
|
|
currentDialogue.value.type = "digital";
|
|
|
+ currentDialogue.value.roleName = roleName || currentDialogue.value.roleName;
|
|
|
currentDialogue.value.content = "让我思考一下...";
|
|
|
};
|
|
|
|
|
|
@@ -736,7 +782,7 @@ const doSendMessageStream = async (userMessage) => {
|
|
|
conversationInProgress.value = true;
|
|
|
receiveMessageFullText.value = '';
|
|
|
|
|
|
- showQuestAnswerDialogue();
|
|
|
+ showQuestAnswerDialogue(userMessage.roleName);
|
|
|
|
|
|
try {
|
|
|
let isFirstChunk = true;
|
|
|
@@ -776,9 +822,7 @@ const doSendMessageStream = async (userMessage) => {
|
|
|
() => {
|
|
|
console.log(`结束对话!`);
|
|
|
stopStream();
|
|
|
- if (isAtLastDialogue.value && currentDialogue.value?.type === 'digital') {
|
|
|
- console.log('AI回答完成,触发 dialogueEnded 事件');
|
|
|
- emit('dialogueEnded', props.isLastCourse);
|
|
|
+ if (isAtLastDialogueOfCourse.value && currentDialogue.value?.type === 'digital') {
|
|
|
isPlaying.value = false;
|
|
|
}
|
|
|
}
|
|
|
@@ -809,9 +853,7 @@ const handleAudioPlaybackComplete = () => {
|
|
|
setOnPlaybackComplete(null);
|
|
|
stopAllAudio();
|
|
|
|
|
|
- if (isAtLastDialogue.value) {
|
|
|
- console.log('已到达最后一个对话,触发 dialogueEnded 事件');
|
|
|
- emit('dialogueEnded', props.isLastCourse);
|
|
|
+ if (isAtLastDialogueOfCourse.value) {
|
|
|
isPlaying.value = false;
|
|
|
return;
|
|
|
}
|
|
|
@@ -851,6 +893,7 @@ watch(() => props.scriptData, (newVal, oldVal) => {
|
|
|
currentDialogueIndex.value = 0;
|
|
|
isPlaying.value = false;
|
|
|
showMask.value = true;
|
|
|
+ isPlaybackStarted.value = false;
|
|
|
currentDialogueCache.value = null;
|
|
|
showPoem.value = false;
|
|
|
currentPoemContent.value = '';
|