ChatTts.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <script setup>
  2. import { ref, computed, onMounted, onBeforeUnmount } from "vue";
  3. import BG from "./components/BG.vue";
  4. import BottomArea from "./components/BottomArea.vue";
  5. import ChatList from "./components/ChatList.vue";
  6. import NavBarPage from "../components/NavBarPage.vue";
  7. import PQueue from "p-queue";
  8. import axios from "axios";
  9. import { fetchEventSource } from "@microsoft/fetch-event-source";
  10. import { useRoute, useRouter } from "vue-router";
  11. import delay from "delay";
  12. const route = useRoute();
  13. const router = useRouter();
  14. const conversationId = computed(() => route.query.taskId); // 会话ID
  15. const round = computed(() => route.query.round); // 轮次
  16. const sessionId = ref("");
  17. const currentRate = ref(0);
  18. const currentRound = computed(() => {
  19. const filteredMessages = chatList.value.filter(
  20. (message) =>
  21. message.text !== "我没有听清,麻烦在重复一下。" && message.text !== ""
  22. );
  23. return Math.floor(filteredMessages.length / 2);
  24. });
  25. // 当前进度百分比整数部分
  26. const rate = computed(() => {
  27. return Math.floor((currentRound.value / round.value) * 100);
  28. });
  29. const text = computed(() => `${currentRound.value} / ${round.value}`);
  30. // 对话记录
  31. const chatList = ref([]);
  32. let i = 0;
  33. // 我 我是 我是中国人 我是中国人的英雄 我是中国人的英雄。[
  34. const updateChatList = async (message = "", loading = true) => {
  35. const timer = setInterval(() => {
  36. i = i + 1;
  37. chatList.value = chatList.value.map((item, index) => {
  38. if (index == chatList.value.length - 1) {
  39. return {
  40. ...item,
  41. text: message.substring(0, i),
  42. };
  43. } else {
  44. return item;
  45. }
  46. });
  47. localStorage.setItem("chatHistory", JSON.stringify(chatList.value));
  48. if (i >= message.length) {
  49. clearInterval(timer);
  50. }
  51. }, 50);
  52. // chatList.value = chatList.value.map((item) =>
  53. // item.loading
  54. // ? message
  55. // ? { ...item, text: message, loading }
  56. // : { ...item, loading }
  57. // : item
  58. // );
  59. // localStorage.setItem("chatHistory", JSON.stringify(chatList.value));
  60. };
  61. // 创建一个队列实例,设置并发数为 1
  62. const queue = new PQueue({ concurrency: 1 });
  63. const queue1 = new PQueue({ concurrency: 1 });
  64. const queue2 = new PQueue({ concurrency: 1 });
  65. // 需要tts的文本队列
  66. const messageQueue = [];
  67. // lastStrIndex 用于记录上一个字符串的结束位置
  68. let lastStrIndex = 0;
  69. // 将字符串根据标点符号断句分割,并添加到messageQueue中
  70. const splitMessage = async (str) => {
  71. const punctuation = ["。", "!", "?", ";", ":"];
  72. for (let i = lastStrIndex; i < str.length; i++) {
  73. if (punctuation.includes(str[i])) {
  74. const message = str.slice(lastStrIndex, i + 1);
  75. console.log(message, "==========");
  76. await playTTS(message);
  77. lastStrIndex = i + 1; // 更新上一个字符串的结束位置
  78. }
  79. }
  80. };
  81. const handleStartRecord = async () => {
  82. queue.clear();
  83. queue1.clear();
  84. };
  85. const generateSessionId = () => {
  86. // 获取当前时间戳
  87. const timestamp = Date.now().toString();
  88. // 生成一个随机数
  89. const randomNum = Math.floor(Math.random() * 10000);
  90. // 将时间戳和随机数拼接成会话ID
  91. const sessionId = `${timestamp}${randomNum}`;
  92. return sessionId;
  93. };
  94. // 发送消息
  95. const handleStopRecord = (message) => {
  96. lastStrIndex = 0; // 重置
  97. chatList.value = [
  98. ...chatList.value,
  99. {
  100. id: Date.now(),
  101. type: "user",
  102. text: message,
  103. loading: false,
  104. },
  105. {
  106. id: Date.now(),
  107. type: "gpt",
  108. text: "",
  109. loading: true,
  110. },
  111. ];
  112. sessionId.value = generateSessionId();
  113. chatGpt(message);
  114. };
  115. const playTTS1 = async (ttsMessage) => {
  116. try {
  117. const res = await axios.post(
  118. `/openapi-stg/ai/voice/tts/v2`,
  119. { sessionId: "N7FB_G0WlrOLjc", text: ttsMessage },
  120. {
  121. responseType: "arraybuffer",
  122. headers: {
  123. "X-Ai-TTS-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
  124. },
  125. }
  126. );
  127. console.log(res.data);
  128. queue1.add(async () => {
  129. // 播放获取到的音频
  130. await playAudio(res.data);
  131. });
  132. } catch (error) {
  133. console.error("Error calling TTS API:", error);
  134. }
  135. };
  136. const playTTS = async (ttsMessage) => {
  137. try {
  138. const res = await axios.get(
  139. `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
  140. {
  141. responseType: "arraybuffer",
  142. }
  143. );
  144. console.log(res.data);
  145. queue1.add(async () => {
  146. // 播放获取到的音频
  147. await playAudio(res.data);
  148. });
  149. } catch (error) {
  150. console.error("Error calling TTS API:", error);
  151. }
  152. };
  153. const playAudio1 = (audioData, options = {}) => {
  154. return new Promise((resolve, reject) => {
  155. const blob = new Blob([audioData], { type: "audio/wav" });
  156. const url = URL.createObjectURL(blob);
  157. const audio = new Audio(url);
  158. audio.setAttribute("id", "audio");
  159. audio.setAttribute("autoplay", "autoplay");
  160. if (options.volume) {
  161. audio.volume = options.volume; // 设置音量
  162. }
  163. audio.onended = () => {
  164. URL.revokeObjectURL(url);
  165. resolve();
  166. };
  167. audio.onerror = (error) => {
  168. URL.revokeObjectURL(url);
  169. reject(error);
  170. };
  171. audio
  172. .play()
  173. .then(() => console.log("Audio playing"))
  174. .catch(reject);
  175. });
  176. };
  177. const playAudio = (audioData, options = {}) => {
  178. return new Promise((resolve, reject) => {
  179. const audioContext = new (window.AudioContext ||
  180. window.webkitAudioContext)();
  181. const blob = new Blob([audioData], { type: "audio/wav" });
  182. const url = URL.createObjectURL(blob);
  183. const audioBufferSourceNode = audioContext.createBufferSource();
  184. fetch(url)
  185. .then((response) => response.arrayBuffer())
  186. .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
  187. .then((audioBuffer) => {
  188. audioBufferSourceNode.buffer = audioBuffer;
  189. audioBufferSourceNode.connect(audioContext.destination);
  190. if (options.volume) {
  191. audioBufferSourceNode.gain.value = options.volume; // 设置音量
  192. }
  193. audioBufferSourceNode.start(0);
  194. audioBufferSourceNode.onended = () => {
  195. URL.revokeObjectURL(url);
  196. resolve();
  197. };
  198. audioBufferSourceNode.onerror = (error) => {
  199. URL.revokeObjectURL(url);
  200. reject(error);
  201. };
  202. })
  203. .catch(reject);
  204. });
  205. };
  206. const chatGpt = async (userMessage) => {
  207. const downloadUrl = "/openapi-prd/ai/intelligent-tutoring/task/dialogue";
  208. i = 0;
  209. queue2.add(() => delay(3000));
  210. try {
  211. const response = await axios.post(
  212. downloadUrl,
  213. {
  214. conversationId: conversationId.value,
  215. content: userMessage,
  216. },
  217. {
  218. headers: {
  219. "Content-Type": "application/json",
  220. },
  221. onDownloadProgress: ({ event }) => {
  222. const xhr = event.target;
  223. let { responseText } = xhr;
  224. console.log("responseText", responseText);
  225. if (responseText.includes("code") && responseText.includes("400")) {
  226. // console.log("responseTextcode", responseText);
  227. // 更新聊天列表
  228. const text = "我没有听清,麻烦在重复一下。";
  229. updateChatList(text, false);
  230. queue.add(async () => {
  231. await splitMessage(text);
  232. });
  233. } else {
  234. // 用于语音播报
  235. queue.add(async () => {
  236. await splitMessage(responseText);
  237. });
  238. // const timer = setInterval(() => {
  239. // i = i + 1;
  240. // //updateChatList(responseText.substring(0, i));
  241. // queue2.add(
  242. // async () => await updateChatList(responseText.substring(0, i))
  243. // );
  244. // if (i >= responseText.length) {
  245. // clearInterval(timer);
  246. // }
  247. // console.log(
  248. // i,
  249. // responseText.substring(i),
  250. // "=======responseText.substring(i)"
  251. // );
  252. // if (
  253. // responseText.substring(i).indexOf("[DONE]") === 0 ||
  254. // responseText.substring(i).indexOf("[DONE]") === 1
  255. // ) {
  256. // console.log("========done===========");
  257. // updateChatList("", false);
  258. // // 保存聊天记录到本地
  259. // const chatHistory = JSON.stringify(chatList.value);
  260. // localStorage.setItem("chatHistory", chatHistory);
  261. // }
  262. //}, 200);
  263. queue2.add(async () => await updateChatList(responseText));
  264. //updateChatList(responseText);
  265. }
  266. },
  267. }
  268. );
  269. //queue2.add(async () => await updateChatList("", false));
  270. } catch (error) {
  271. //updateChatList("", false);
  272. console.error("出错:", error);
  273. }
  274. };
  275. const chatGpt2 = (userMessage) => {
  276. const params = {
  277. conversationId: "2c64eb8b69be432ca0bb9ae55bc78def",
  278. content: userMessage,
  279. };
  280. const ctrlAbout = new AbortController();
  281. fetchEventSource(
  282. "https://fls-ai.pingan.com.cn/openapi/ai/intelligent-tutoring/task/dialogue",
  283. {
  284. method: "POST",
  285. headers: {
  286. "Content-Type": "application/json", // 文本返回格式
  287. },
  288. body: JSON.stringify(params),
  289. signal: ctrlAbout.signal,
  290. openWhenHidden: true, // 在浏览器标签页隐藏时保持与服务器的EventSource连接
  291. onmessage(res) {
  292. // 操作流式数据
  293. console.log(JSON.parse(res.data), "==========");
  294. },
  295. onclose(res) {
  296. // 关闭流
  297. },
  298. onerror(error) {
  299. // 返回流报错
  300. },
  301. }
  302. );
  303. };
  304. const chatGpt1 = async (userMessage) => {
  305. const downloadUrl =
  306. "https://fls-ai.pingan.com.cn/openapi/ai/llm/forward/api/ai/nlp/dialogue";
  307. try {
  308. const response = await axios.post(
  309. downloadUrl,
  310. {
  311. conversationId: "1976cfe3a5174f9ba768677f789cad7e",
  312. content: `${userMessage},请输出纯文本的回答,不要使用markdown输出`,
  313. messageId: Date.now(),
  314. applicationId: "",
  315. },
  316. {
  317. headers: {
  318. "Team-Id": "123456",
  319. Authorization: "9b2f86c99b5847739045e6b85f355301",
  320. "Content-Type": "application/json",
  321. },
  322. onDownloadProgress: ({ event }) => {
  323. const xhr = event.target;
  324. const { responseText } = xhr;
  325. console.log(responseText);
  326. // 更新聊天列表
  327. updateChatList(responseText);
  328. // 用于语音播报
  329. queue.add(async () => {
  330. await splitMessage(responseText);
  331. });
  332. },
  333. }
  334. );
  335. updateChatList("", false);
  336. } catch (error) {
  337. updateChatList("", false);
  338. console.error("下载文件时出错:", error);
  339. }
  340. };
  341. const next = () => {
  342. router.push({
  343. name: "result",
  344. query: { conversationId: conversationId.value },
  345. });
  346. };
  347. const loadChatHistory = () => {
  348. const chatHistory = localStorage.getItem("chatHistory");
  349. if (chatHistory) {
  350. const history = JSON.parse(chatHistory);
  351. history.forEach((item) => {
  352. chatList.value.push(item);
  353. });
  354. }
  355. };
  356. // 在组件挂载时调用
  357. onMounted(() => {
  358. loadChatHistory();
  359. });
  360. // onBeforeUnmount(() => {
  361. // localStorage.removeItem("chatHistory");
  362. // });
  363. </script>
  364. <template>
  365. <div class="main">
  366. <nav-bar-page title="考试" />
  367. <div class="rate-circle">
  368. <van-circle
  369. :stroke-width="80"
  370. size="70px"
  371. v-model:current-rate="currentRate"
  372. :rate="rate"
  373. :text="text"
  374. />
  375. </div>
  376. <BG />
  377. <ChatList :chatList="chatList" />
  378. <BottomArea
  379. @startRecord="handleStartRecord"
  380. @stopRecord="handleStopRecord"
  381. v-show="rate < 100"
  382. />
  383. <div v-show="rate >= 100" class="next-btn">
  384. <van-button style="width: 100%" @click="next" type="primary"
  385. >已完成对话,下一步</van-button
  386. >
  387. </div>
  388. </div>
  389. </template>
  390. <style>
  391. * {
  392. -webkit-touch-callout: none; /*系统默认菜单被禁用*/
  393. -webkit-user-select: none; /*webkit浏览器*/
  394. -khtml-user-select: none; /*早期浏览器*/
  395. -moz-user-select: none; /*火狐*/
  396. -ms-user-select: none; /*IE10*/
  397. user-select: none;
  398. }
  399. </style>
  400. <style scoped>
  401. .main {
  402. width: 100vw;
  403. height: 100vh;
  404. overflow: hidden;
  405. }
  406. .rate-circle {
  407. position: fixed;
  408. right: 20px;
  409. top: 70px;
  410. z-index: 9999;
  411. }
  412. .next-btn {
  413. position: fixed;
  414. bottom: calc(20px + env(safe-area-inset-bottom));
  415. width: 100%;
  416. box-sizing: border-box;
  417. padding: 0 20px;
  418. }
  419. </style>