123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582 |
- <script setup>
- import { ref, computed, onMounted, onBeforeUnmount } from "vue";
- import BG from "./components/BG.vue";
- import BottomArea from "./components/BottomArea.vue";
- import ChatList from "./components/ChatList.vue";
- import NavBarPage from "../components/NavBarPage.vue";
- import PQueue from "p-queue";
- import axios from "axios";
- import { fetchEventSource } from "@microsoft/fetch-event-source";
- import { useRoute, useRouter } from "vue-router";
- import delay from "delay";
- import { fetchTaskStatus } from "./utils/api";
- const route = useRoute();
- const router = useRouter();
- const conversationId = computed(() => route.query.taskId); // 会话ID
- const round = computed(() => route.query.round); // 轮次
- const sessionId = ref("");
- const taskStatus = ref(false)
- const loading = ref(true); //初始化进入的加载状态
- const currentRate = ref(0);
- const currentRound = computed(() => {
- const filteredMessages = chatList.value.filter(
- (message) =>
- message.text !== "我没有听清,麻烦在重复一下。" && message.text !== ""
- );
- return Math.floor(filteredMessages.length / 2);
- });
- // 当前进度百分比整数部分
- const rate = computed(() => {
- return Math.floor((currentRound.value / round.value) * 100);
- });
- const text = computed(() => `${currentRound.value} / ${round.value}`);
- // 对话记录
- const chatList = ref([]);
- let i = 0;
- // 我 我是 我是中国人 我是中国人的英雄 我是中国人的英雄。[
- const updateChatList = async (message = "", loading = true) => {
- const timer = setInterval(() => {
- i = i + 1;
- chatList.value = chatList.value.map((item, index) => {
- if (index == chatList.value.length - 1) {
- return {
- ...item,
- text: message.substring(0, i),
- };
- } else {
- return item;
- }
- });
- localStorage.setItem("chatHistory", JSON.stringify(chatList.value));
- if (i >= message.length) {
- clearInterval(timer);
- }
- }, 50);
- };
- // 创建一个队列实例,设置并发数为 1
- const queue = new PQueue({ concurrency: 3 });
- const queue1 = new PQueue({ concurrency: 1 });
- const queue2 = new PQueue({ concurrency: 1 });
- // 需要tts的文本队列
- const messageQueue = [];
- // lastStrIndex 用于记录上一个字符串的结束位置
- let lastStrIndex = 0;
- // 将字符串根据标点符号断句分割,并添加到messageQueue中
- const splitMessage = async (str) => {
- const punctuation = ["。","," ,"!", "?", ";", ":"];
- for (let i = lastStrIndex; i < str.length; i++) {
- if (punctuation.includes(str[i])) {
- const message = str.slice(lastStrIndex, i + 1);
- console.log(message, "==========");
- playTTS(message);
- lastStrIndex = i + 1; // 更新上一个字符串的结束位置
- }
- }
- };
- const handleStartRecord = async () => {
- queue.clear();
- queue1.clear();
- };
- const generateSessionId = () => {
- // 获取当前时间戳
- const timestamp = Date.now().toString();
- // 生成一个随机数
- const randomNum = Math.floor(Math.random() * 10000);
- // 将时间戳和随机数拼接成会话ID
- const sessionId = `${timestamp}${randomNum}`;
- return sessionId;
- };
- // 发送消息
- const handleStopRecord = (message) => {
- lastStrIndex = 0; // 重置
- chatList.value = [
- ...chatList.value,
- {
- id: Date.now(),
- type: "user",
- text: message,
- loading: false,
- },
- {
- id: Date.now(),
- type: "gpt",
- text: "",
- loading: true,
- },
- ];
- sessionId.value = generateSessionId();
- chatGpt(message);
- };
- let audioQueue = []; // 音频队列,保存按请求顺序的音频数据
- let playingQueue = []; // 用来记录当前正在播放的音频序号
- let isPlaying = false; // 当前是否正在播放
- let orderId = 0; // 请求顺序号
- // 用来控制正在播放音频的锁定机制
- let playingLock = false; // 标识是否正在播放
- // 1. 发起 TTS 请求
- // const playTTS = async (ttsMessage) => {
- // const currentOrderId = orderId++; // 获取当前请求的顺序号
- // try {
- // // 发起请求
- // const res = await axios.get(
- // `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
- // { responseType: "arraybuffer" }
- // );
- // // 将音频数据和顺序号一起存入 audioQueue
- // audioQueue.push({ orderId: currentOrderId, audioData: res.data });
- // // 如果当前没有音频在播放,开始播放
- // if (!isPlaying && !playingLock) {
- // await playNextAudio(); // 开始播放
- // }
- // } catch (error) {
- // console.error("调用 TTS API 时出错:", error);
- // }
- // };
- const playTTS = async (ttsMessage) => {
- queue.add(async () => {
- const currentOrderId = orderId++;
- try {
- const res = await axios.get(
- `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
- { responseType: "arraybuffer" }
- );
- if (!res.data || res.data.byteLength === 0) {
- console.error("音频数据无效,无法播放.");
- return;
- }
- // 按顺序存入音频队列
- audioQueue.push({ orderId: currentOrderId, audioData: res.data });
- // 仅在没有播放时启动播放
- if (!isPlaying) {
- await playNextAudio();
- }
- } catch (error) {
- console.error("调用 TTS API 时出错:", error);
- }
- });
- };
- // 2. 播放下一个音频
- const playNextAudio = async () => {
- // 如果播放队列为空,停止播放
- if (audioQueue.length === 0) {
- isPlaying = false; // 播放完所有音频
- // audioQueue = []; // 确保列表为空
- console.log("所有音频播放完毕.");
- return;
- }
- // 找到队列中最小序号的音频,确保顺序播放
- const nextAudio = getNextAudioToPlay();
- console.log(nextAudio,"没有找到下一个应该播放的音频,队列尚未按顺序准备好.");
- // 如果找不到下一个应该播放的音频,表示队列还没有按顺序准备好
- if (!nextAudio) {
- console.log("没有找到下一个应该播放的音频,队列尚未按顺序准备好.");
- console.log("当前音频队列:", audioQueue); // 打印当前音频队列
- console.log("当前已播放的序列:", playingQueue); // 打印已经播放的音频序列
- return;
- }
- // 打印正在播放的音频序号
- console.log(`正在播放音频,序号:${nextAudio.orderId}`);
- try {
- isPlaying = true; // 标记当前正在播放
- playingLock = true; // 锁定播放,避免新的音频插入
- // 播放当前音频
- await playAudio(nextAudio.audioData);
- // 播放完成后,移除已播放的音频
- audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId);
- playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
- // 输出音频播放完成的日志
- console.log(`音频序号 ${nextAudio.orderId} 播放完成,等待播放下一个包...`);
- // 解除锁定,等待下一个音频播放
- playingLock = false;
- // 使用定时器周期性检查是否可以播放下一个音频
- const checkNextAudioTimer = setInterval(async () => {
- if (!playingLock) {
- await playNextAudio();
- clearInterval(checkNextAudioTimer); // 清除定时器
- }
- }, 300); // 每秒检查一次
- } catch (error) {
- console.error(`播放音频序号 ${nextAudio.orderId} 时出错:`, error);
- // 如果某个音频播放失败,跳过这个音频,继续播放下一个
- audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId); // 移除报错的音频
- playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
- playingLock = false; // 解除锁定
- // 继续播放下一个音频
- await playNextAudio();
- }
- };
- // 获取下一个应该播放的音频(保证顺序)
- const getNextAudioToPlay = () => {
- // 检查音频队列中是否有下一个应该播放的音频
- console.log("检查下一个播放的音频...");
- for (let i = 0; i < audioQueue.length; i++) {
- const audio = audioQueue[i];
- // 找到最小的序号,即请求的顺序号
- if (audio.orderId === playingQueue.length) {
- console.log(`找到了下一个应该播放的音频,序号:${audio.orderId}`);
- return audio;
- }
- }
- console.log("没有找到下一个应该播放的音频,返回 null");
- return null; // 如果没有找到匹配的音频,返回 null
- };
- // 播放音频的方法
- const playAudio = (audioData) => {
- return new Promise((resolve, reject) => {
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const blob = new Blob([audioData], { type: "audio/wav" });
- const url = URL.createObjectURL(blob);
- const audioBufferSourceNode = audioContext.createBufferSource();
- fetch(url)
- .then((response) => {
- if (!response.ok) {
- console.error("音频文件请求失败: ", response.statusText);
- reject(new Error("音频文件请求失败"));
- return;
- }
- return response.arrayBuffer();
- })
- .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
- .then((audioBuffer) => {
- audioBufferSourceNode.buffer = audioBuffer;
- audioBufferSourceNode.connect(audioContext.destination);
- audioBufferSourceNode.start(0);
- // 当音频播放完毕时,调用 resolve
- audioBufferSourceNode.onended = () => {
- URL.revokeObjectURL(url);
- resolve(); // 播放完成后,继续播放下一个
- };
- audioBufferSourceNode.onerror = (error) => {
- console.error("音频播放错误:", error);
- URL.revokeObjectURL(url);
- reject(error); // 播放失败时,返回错误
- };
- })
- .catch((error) => {
- console.error("音频加载或解码时出错:", error);
- reject(error); // 如果解码或播放出错,reject
- });
- });
- };
- // const playTTS1 = async (ttsMessage) => {
- // try {
- // const res = await axios.post(
- // `/openapi-stg/ai/voice/tts/v2`,
- // { sessionId: "N7FB_G0WlrOLjc", text: ttsMessage },
- // {
- // responseType: "arraybuffer",
- // headers: {
- // "X-Ai-TTS-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
- // },
- // }
- // );
- // console.log(res.data);
- // queue1.add(async () => {
- // // 播放获取到的音频
- // await playAudio(res.data);
- // });
- // } catch (error) {
- // console.error("Error calling TTS API:", error);
- // }
- // };
- // const playAudio1 = (audioData, options = {}) => {
- // return new Promise((resolve, reject) => {
- // const blob = new Blob([audioData], { type: "audio/wav" });
- // const url = URL.createObjectURL(blob);
- // const audio = new Audio(url);
- // audio.setAttribute("id", "audio");
- // audio.setAttribute("autoplay", "autoplay");
- // if (options.volume) {
- // audio.volume = options.volume; // 设置音量
- // }
- // audio.onended = () => {
- // URL.revokeObjectURL(url);
- // resolve();
- // };
- // audio.onerror = (error) => {
- // URL.revokeObjectURL(url);
- // reject(error);
- // };
- // audio
- // .play()
- // .then(() => console.log("Audio playing"))
- // .catch(reject);
- // });
- // };
- const chatGpt = async (userMessage) => {
- const downloadUrl = "/openapi-prd/ai/intelligent-tutoring/task/dialogue";
- i = 0;
- queue2.add(() => delay(3000));
- try {
- const response = await axios.post(
- downloadUrl,
- {
- conversationId: conversationId.value,
- content: userMessage,
- },
- {
- headers: {
- "Content-Type": "application/json",
- },
- onDownloadProgress: ({ event }) => {
- const xhr = event.target;
- let { responseText } = xhr;
- console.log("responseText", responseText);
- if (responseText.includes("code") && responseText.includes("400")) {
- // console.log("responseTextcode", responseText);
- // 更新聊天列表
- const text = "我没有听清,麻烦在重复一下。";
- updateChatList(text, false);
- queue.add(async () => {
- await splitMessage(text);
- });
- } else {
- // 用于语音播报
- queue.add(async () => {
- await splitMessage(responseText);
- });
-
- queue2.add(async () => await updateChatList(responseText));
-
- //updateChatList(responseText);
- }
- },
- }
- );
- setTimeout(() => {
- audioQueue=[]
- playingQueue=[]
- handleTaskStatus()
- },3000)
- //queue2.add(async () => await updateChatList("", false));
- } catch (error) {
- //updateChatList("", false);
- console.error("出错:", error);
- }
- };
- const chatGpt2 = (userMessage) => {
- const params = {
- conversationId: "2c64eb8b69be432ca0bb9ae55bc78def",
- content: userMessage,
- };
- const ctrlAbout = new AbortController();
- fetchEventSource(
- "https://fls-ai.pingan.com.cn/openapi/ai/intelligent-tutoring/task/dialogue",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json", // 文本返回格式
- },
- body: JSON.stringify(params),
- signal: ctrlAbout.signal,
- openWhenHidden: true, // 在浏览器标签页隐藏时保持与服务器的EventSource连接
- onmessage(res) {
- // 操作流式数据
- console.log(JSON.parse(res.data), "==========");
- },
- onclose(res) {
- // 关闭流
- },
- onerror(error) {
- // 返回流报错
- },
- }
- );
- };
- const chatGpt1 = async (userMessage) => {
- const downloadUrl =
- "https://fls-ai.pingan.com.cn/openapi/ai/llm/forward/api/ai/nlp/dialogue";
- try {
- const response = await axios.post(
- downloadUrl,
- {
- conversationId: "1976cfe3a5174f9ba768677f789cad7e",
- content: `${userMessage},请输出纯文本的回答,不要使用markdown输出`,
- messageId: Date.now(),
- applicationId: "",
- },
- {
- headers: {
- "Team-Id": "123456",
- Authorization: "9b2f86c99b5847739045e6b85f355301",
- "Content-Type": "application/json",
- },
- onDownloadProgress: ({ event }) => {
- const xhr = event.target;
- const { responseText } = xhr;
- console.log(responseText);
- // 更新聊天列表
- updateChatList(responseText);
- // 用于语音播报
- queue.add(async () => {
- await splitMessage(responseText);
- });
- },
- }
- );
- updateChatList("", false);
- } catch (error) {
- updateChatList("", false);
- console.error("下载文件时出错:", error);
- }
- };
- const next = () => {
- router.push({
- name: "result",
- query: { conversationId: conversationId.value },
- });
- };
- const loadChatHistory = () => {
-
- const chatHistory = localStorage.getItem("chatHistory");
- const chatStatus = localStorage.getItem("status");
- if (chatHistory) {
- const history = JSON.parse(chatHistory);
- history.forEach((item) => {
- chatList.value.push(item);
- });
- }
- if(chatStatus){
- taskStatus.value = JSON.parse(chatStatus)
- }
- };
- // 查询任务结束状态
- const handleTaskStatus = async () => {
- const res = await fetchTaskStatus(conversationId.value)
- if(res.code == 200){
- taskStatus.value = res.body
- localStorage.setItem("status", res.body);
- }
- }
- // 在组件挂载时调用
- onMounted(() => {
- loadChatHistory();
- setTimeout(() => {
- loading.value = false;
- }, 2000);
- });
- </script>
- <template>
- <div class="main">
- <nav-bar-page title="考试" />
- <div class="rate-circle">
- <van-circle
- :stroke-width="80"
- size="70px"
- v-model:current-rate="currentRate"
- :rate="rate"
- :text="text"
- />
- </div>
- <BG />
- <div class="loading" v-show="loading">
- <van-loading color="#0094ff" size="26px" vertical>加载中...</van-loading>
- </div>
-
- <ChatList :chatList="chatList" />
- <BottomArea
- @startRecord="handleStartRecord"
- @stopRecord="handleStopRecord"
- v-show="rate < 100 || taskStatus"
- />
- <div v-show="rate >= 100 || taskStatus" class="next-btn">
- <van-button style="width: 100%" @click="next" type="primary"
- >已完成对话,下一步</van-button
- >
- </div>
- </div>
- </template>
- <style>
- * {
- -webkit-touch-callout: none; /*系统默认菜单被禁用*/
- -webkit-user-select: none; /*webkit浏览器*/
- -khtml-user-select: none; /*早期浏览器*/
- -moz-user-select: none; /*火狐*/
- -ms-user-select: none; /*IE10*/
- user-select: none;
- }
- </style>
- <style scoped>
- .main {
- width: 100vw;
- height: 100vh;
- overflow: hidden;
- }
- .rate-circle {
- position: fixed;
- right: 20px;
- top: 70px;
- z-index: 9999;
- }
- .next-btn {
- position: fixed;
- bottom: calc(20px + env(safe-area-inset-bottom));
- width: 100%;
- box-sizing: border-box;
- padding: 0 20px;
- }
- .loading {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 9999;
- }
- </style>
|