levitatedSphere.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <template>
  2. <div class="levitatedSphere" v-show="show">
  3. <div :class="['ls_hello']" v-show="showIndex == 0" @click="stopOne()" @dblclick="stopTwo()">
  4. <el-image
  5. style="width: 110%; height: 110%"
  6. v-show="aiStatus == 1"
  7. :src="require('../../../assets/icon/course/aiTalk.svg')"
  8. fit="fill"
  9. ></el-image>
  10. <el-image
  11. style="width: 110%; height: 110%"
  12. v-show="aiStatus == 2"
  13. :src="require('../../../assets/icon/course/aiVanish.svg')"
  14. fit="fill"
  15. ></el-image>
  16. <el-image
  17. style="width: 110%; height: 110%;transform: scale(1.3,1.3);"
  18. v-show="aiStatus == 0"
  19. :src="require('../../../assets/icon/course/aiWait2.svg')"
  20. fit="fill"
  21. ></el-image>
  22. </div>
  23. <div
  24. class="ls_text"
  25. :style="{
  26. width: userText || aiText || showTextIndex == 2 ? '300px' : '0px'
  27. }"
  28. >
  29. <div class="ls_t_ai" v-if="[0, 2].includes(showTextIndex)">
  30. <span
  31. v-if="[0].includes(showTextIndex)"
  32. v-html="htmlContent(aiText)"
  33. ></span>
  34. <span v-if="[2].includes(showTextIndex)">正在组织语言...</span>
  35. </div>
  36. <div class="ls_t_user" v-if="[1].includes(showTextIndex)">
  37. {{ userText }}
  38. </div>
  39. </div>
  40. <!-- 录音转文字 -->
  41. <iframe
  42. allow="camera *; microphone *;display-capture;midi;encrypted-media;"
  43. src="https://beta.cloud.cocorobo.cn/browser/public/index.html"
  44. ref="iiframe"
  45. v-show="false"
  46. ></iframe>
  47. <!-- 文字转语音-->
  48. <iframe
  49. allow="camera *; microphone *;display-capture;midi;encrypted-media;"
  50. src="https://beta.cloud.cocorobo.cn/browser/public/index1.html"
  51. ref="iiframe2"
  52. v-show="false"
  53. ></iframe>
  54. <mini-audio
  55. v-show="false"
  56. ref="audioRef"
  57. :loop="false"
  58. :audio-source="wozaiAudioUrl"
  59. ></mini-audio>
  60. </div>
  61. </template>
  62. <script>
  63. import { v4 as uuidv4 } from "uuid";
  64. import MarkdownIt from "markdown-it";
  65. export default {
  66. data() {
  67. return {
  68. show: false,
  69. showIndex: 0,
  70. aiStatus: 0, //0 :在说话 1 : 接收 2:待命
  71. aiText: "您好,我是小可,有什么可以帮助您的?",
  72. userText: "",
  73. showTextIndex: 0, //0:ai,1:用户, 2:组织语言 3: 无
  74. timer: null,
  75. isOpen: false,
  76. userId: this.$route.query.userid,
  77. chatLoading: false,
  78. talkLoading: false,
  79. source: null,
  80. talkTextList: [],
  81. wozaiAudioUrl:"https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/default%2F%E4%B8%AD%E6%96%87%E7%89%88%E6%88%91%E5%9C%A81728961654002.wav",
  82. wozaiAudioEnglishUrl:"https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/default%2F%E8%8B%B1%E6%96%87%E7%89%88what%27sup1728961667376.wav",
  83. };
  84. },
  85. computed: {
  86. htmlContent() {
  87. const md = new MarkdownIt();
  88. return _md => {
  89. return md.render(_md);
  90. };
  91. }
  92. },
  93. methods: {
  94. stopTwo(){
  95. this.show = false;
  96. this.showTextIndex = 3;
  97. this.aiStatus = 2;
  98. this.aiText = "";
  99. this.userText = "";
  100. this.stopRecord();
  101. },
  102. stopOne(){
  103. if(this.source){
  104. this.source.close();
  105. this.source = null;
  106. }
  107. if (this.talkLoading) {
  108. this.stopTalk();
  109. }
  110. this.showIndex = 0;
  111. this.showTextIndex = 0;
  112. this.aiText = "您好,我是小可,有什么可以帮助您的?";
  113. this.aiStatus = 0;
  114. this.chatLoading = false;
  115. },
  116. recordStart(_text) {
  117. var OpenCC = require("opencc-js");
  118. let converter = OpenCC.Converter({
  119. from: "hk",
  120. to: "cn"
  121. });
  122. // try {
  123. // this.$parent.changeRecordType(1);
  124. // return this.$message.success("已开启语音助手,请说“可可同学”来唤醒")
  125. if (this.isOpen)
  126. return this.$message.info("已开启语音助手,无需重复开启");
  127. let iiframe = this.$refs["iiframe"];
  128. iiframe.contentWindow.window.document.getElementById(
  129. "languageOptions"
  130. ).selectedIndex = 2; //普通话
  131. iiframe.contentWindow.testdoContinuousPronunciationAssessment();
  132. //this.talkTextList.push("我在");
  133. //this.talkText();
  134. // console.log("打开")
  135. this.$refs.audioRef.play();
  136. // return;
  137. // this.$message.success("已开启语音助手,请说“可可同学”来唤醒");
  138. this.$parent.changeRecordType(1);
  139. this.isOpen = true;
  140. this.aiText = "您好,我是小可,有什么可以帮助您的?";
  141. this.aiStatus = 0;
  142. this.showIndex = 0;
  143. this.showTextIndex = 0;
  144. this.chatLoading = false;
  145. this.talkLoading == false;
  146. this.show = true;
  147. iiframe.contentWindow.onRecognizedResult = e => {
  148. let _msg = converter(e.privText);
  149. // let _msg2 = e.privText;
  150. // let _msg = _text;
  151. console.log("👇");
  152. console.log(_msg);
  153. // _msg = converter(_msg)
  154. if (!_msg) return console.log("输出为空");
  155. // if (_msg.indexOf(converter("可可同学")) != -1 && !this.show) {
  156. // this.aiText = "您好,我是小可,有什么可以帮助您的?";
  157. // this.aiStatus = 0;
  158. // this.showIndex = 0;
  159. // this.show = true;
  160. // console.log("已唤醒");
  161. // return;
  162. // } else
  163. if (this.show == true) {
  164. if (
  165. _msg.indexOf(converter("可可同学")) != -1 &&
  166. _msg.indexOf(converter("停止")) != -1
  167. ) {
  168. this.stopTalk();
  169. } else if (
  170. this.showTextIndex == 2 ||
  171. this.chatLoading ||
  172. this.talkLoading
  173. ) {
  174. return console.log("组织语言中");
  175. // }else if(_msg.indexOf('可可同学')!=-1 && _msg.indexOf("停止")!=-1){
  176. // this.stopTalk();
  177. } else {
  178. this.showTextIndex = 1;
  179. this.aiText = "";
  180. this.userText += _msg;
  181. this.aiStatus = 1;
  182. if (this.timer) {
  183. clearTimeout(this.timer);
  184. this.timer = null;
  185. }
  186. this.timer = setTimeout(() => {
  187. if (this.userText.indexOf(converter("关闭语音助手")) != -1) {
  188. // return setTimeout(()=>{
  189. this.show = false;
  190. this.showTextIndex = 3;
  191. this.aiStatus = 2;
  192. this.aiText = "";
  193. this.userText = "";
  194. this.stopRecord();
  195. return;
  196. // },1000)
  197. }
  198. this.showTextIndex = 2;
  199. this.aiText = "";
  200. let regExp = new RegExp(
  201. converter("计时") + "(.+)" + converter("分钟")
  202. );
  203. if (regExp.test(this.userText)) {
  204. // setTimeout(() => {
  205. let _number = this.userText.match(regExp)[1];
  206. let _time = 0;
  207. if (!/^\d+$/.test(_number)) {
  208. _time = this.chineseToNumber(_number) * 60;
  209. } else {
  210. _time = parseInt(_numberList[1]) * 60;
  211. }
  212. this.$emit("startTime", _time);
  213. this.aiStatus = 0;
  214. this.showTextIndex = 0;
  215. this.aiText =
  216. "好的,我已为您计时" +
  217. this.userText.match(regExp)[1] +
  218. "分钟。";
  219. this.userText = "";
  220. this.timer = setTimeout(() => {
  221. this.showTextIndex = 3;
  222. this.aiStatus = 2;
  223. this.aiText = "";
  224. this.userText = "";
  225. }, 3000);
  226. // }, 2000);
  227. } else {
  228. let msgList = [];
  229. msgList.push({
  230. type:"text",
  231. text: `请在150字以内回答以下问题:\n${this.userText}`
  232. })
  233. this.chatLoading = true;
  234. const _uuid = uuidv4();
  235. let params = {
  236. assistant_id: "f8e1ebb2-2e0d-11ef-8bf4-12e77c4cb76b",
  237. userId: this.userId,
  238. message: msgList,
  239. session_name: _uuid + "-qgt",
  240. uid: _uuid,
  241. file_ids: [],
  242. model: "gpt-4o-2024-08-06",
  243. };
  244. this.ajax
  245. // .post("https://claude3.cocorobo.cn/chat", params)
  246. // .post("https://gpt4.cocorobo.cn/chat", params)
  247. .post(
  248. "https://gpt4.cocorobo.cn/ai_agent_park_chat_new",
  249. params
  250. )
  251. .then(res => {
  252. if (
  253. converter(res.data.FunctionResponse.result) ==
  254. converter("发送成功")
  255. ) {
  256. this.userText = "";
  257. this.showIndex = 0;
  258. } else {
  259. // this.$message.warning(res.data.FunctionResponse.result);
  260. console.log(res.data.FunctionResponse.result);
  261. this.chatLoading = false;
  262. this.aiStatus = 0;
  263. this.showTextIndex = 0;
  264. this.aiText = "对不起,我无法理解您的问题,请重新提问";
  265. this.timer = setTimeout(() => {
  266. this.showTextIndex = 3;
  267. this.aiStatus = 2;
  268. this.aiText = "";
  269. this.userText = "";
  270. }, 3000);
  271. }
  272. })
  273. .catch(e => {
  274. console.log(e);
  275. this.chatLoading = false;
  276. this.aiStatus = 0;
  277. this.showTextIndex = 0;
  278. this.aiText = "对不起,我无法理解您的问题,请重新提问";
  279. this.timer = setTimeout(() => {
  280. this.showTextIndex = 3;
  281. this.aiStatus = 2;
  282. this.aiText = "";
  283. this.userText = "";
  284. }, 3000);
  285. });
  286. // 通过流获取ai对话数据
  287. this.getAtAuContent(_uuid);
  288. }
  289. }, 5000);
  290. }
  291. } else {
  292. console.log("不响应");
  293. }
  294. };
  295. // } catch (error) {
  296. // console.log("ai录音报错👇");
  297. // console.log(error);
  298. // setTimeout(() => {
  299. // this.recordStart();
  300. // }, 1000);
  301. // }
  302. },
  303. stopRecord() {
  304. // this.$parent.changeRecordType(0);
  305. // this.$message.success("已关闭语音助手")
  306. // return
  307. let iiframe = this.$refs["iiframe"];
  308. iiframe.contentWindow.window.document
  309. .getElementById("scenarioStopButton")
  310. .click();
  311. if (this.talkLoading) {
  312. this.stopTalk();
  313. }
  314. // this.stopTalk();
  315. // 录音借宿
  316. iiframe.contentWindow.onSessionStopped = (s, e) => {
  317. this.isOpen = false;
  318. this.show = false;
  319. this.showTextIndex = 3;
  320. this.showIndex = 2;
  321. this.$parent.changeRecordType(0);
  322. this.$message.success("已关闭语音助手");
  323. if (this.talkLoading) {
  324. this.$refs.iiframe2.contentWindow.closesynthesizer();
  325. }
  326. this.userText = "";
  327. this.aiText = "";
  328. };
  329. },
  330. chineseToNumber(chinese) {
  331. var OpenCC = require("opencc-js");
  332. let converter = OpenCC.Converter({
  333. from: "hk",
  334. to: "cn"
  335. });
  336. chinese = converter(chinese);
  337. const chineseNumbers = {
  338. 零: 0,
  339. 一: 1,
  340. 二: 2,
  341. 三: 3,
  342. 四: 4,
  343. 五: 5,
  344. 六: 6,
  345. 七: 7,
  346. 八: 8,
  347. 九: 9,
  348. 十: 10,
  349. 百: 100,
  350. 千: 1000,
  351. 万: 10000,
  352. 亿: 100000000
  353. };
  354. let result = 0;
  355. let tempNum = 0; // 用于累积处理
  356. let sectionNum = 0; // 每个段的值
  357. for (let i = 0; i < chinese.length; i++) {
  358. const char = chinese[i];
  359. const num = chineseNumbers[char];
  360. if (num === undefined) {
  361. throw new Error(`Unexpected character: ${char}`);
  362. }
  363. if (
  364. num === 10 ||
  365. num === 100 ||
  366. num === 1000 ||
  367. num === 10000 ||
  368. num === 100000000
  369. ) {
  370. if (tempNum === 0) tempNum = 1; // 如果前面没有数,默认是1
  371. tempNum *= num;
  372. if (num === 10000 || num === 100000000) {
  373. sectionNum += tempNum;
  374. result += sectionNum;
  375. tempNum = 0;
  376. sectionNum = 0;
  377. }
  378. } else {
  379. tempNum += num;
  380. }
  381. }
  382. result += sectionNum + tempNum;
  383. return result;
  384. },
  385. removeMarkdown(text) {
  386. return text
  387. .replace(/[#*_~`>+\-]/g, "") // 移除 #、*、_、~、`、>、+、- 符号
  388. .replace(/!\[.*?\]\(.*?\)/g, "") // 移除图片
  389. .replace(/\[.*?\]\(.*?\)/g, "") // 移除链接
  390. .replace(/```[\s\S]*?```/g, "") // 移除代码块(不使用 s 标志)
  391. .replace(/`[^`]*`/g, "") // 移除行内代码
  392. .replace(/\d+\./g, "") // 移除有序列表
  393. .replace(/^\s*[-*+]\s+/gm, "") // 移除无序列表
  394. .replace(/\s+/g, " ") // 将多个空白字符替换为一个空格
  395. .trim(); // 去除字符串两端的空白字符
  396. },
  397. getAtAuContent(_uid) {
  398. this.source = new EventSource(
  399. `https://gpt4.cocorobo.cn/question/${_uid}`
  400. );
  401. //http://gpt4.cocorobo.cn:8011/question/ https://gpt4.cocorobo.cn/question/
  402. let _allText = "";
  403. let _mdText = "";
  404. let _talkText = "";
  405. // let _talkIndex = 0;
  406. // const md = new MarkdownIt();
  407. this.source.onmessage = _e => {
  408. this.showIndex = 0;
  409. this.aiStatus = 0
  410. let _eData = JSON.parse(_e.data);
  411. if (_eData.content.replace("'", "").replace("'", "") == "[DONE]") {
  412. let _result = [];
  413. if ("result" in _eData) {
  414. _result = _eData.result;
  415. for (let i = 0; i < _result.length; i++) {
  416. _mdText = _mdText.replace(_result[i].text, _result[i].fileName);
  417. }
  418. }
  419. _mdText = _mdText.replace("_", "");
  420. this.aiText = _mdText;
  421. if (_talkText != "") {
  422. let _resultText = this.removeMarkdown(_talkText);
  423. this.talkTextList.push(_resultText);
  424. _talkText = "";
  425. if (!this.talkLoading) this.talkText();
  426. }
  427. this.chatLoading = false;
  428. this.source.close();
  429. } else {
  430. // _talkIndex+=1;
  431. let _text = _eData.content.replace("'", "").replace("'", "");
  432. if (_allText == "") {
  433. _allText = _text.replace(/^\n+/, ""); //去掉回复消息中偶尔开头就存在的连续换行符
  434. _talkText += _text.replace(/^\n+/, "");
  435. } else {
  436. _allText += _text;
  437. _talkText += _text;
  438. }
  439. `~`;
  440. _mdText = _allText + "_";
  441. _mdText = _mdText.replace(/\\n/g, "\n");
  442. _mdText = _mdText.replace(/\\/g, "");
  443. if (_allText.split("```").length % 2 == 0) _mdText += "\n```\n";
  444. this.aiText = _mdText;
  445. this.showTextIndex = 0;
  446. if (/[,。:;?!)]/.test(_talkText)) {
  447. let _resultText = this.removeMarkdown(_talkText);
  448. this.talkTextList.push(_resultText);
  449. _talkText = "";
  450. if (!this.talkLoading) this.talkText();
  451. }
  452. }
  453. };
  454. },
  455. talkText() {
  456. let _text = this.talkTextList.shift();
  457. let _talkTextIiframe2 = this.$refs.iiframe2;
  458. if (_text) {
  459. this.talkLoading = true;
  460. if (this.timer) {
  461. clearTimeout(this.timer);
  462. this.timer = null;
  463. }
  464. console.log(`👉转语音:${_text}`);
  465. _talkTextIiframe2.contentWindow.texttospeech(
  466. _text,
  467. this.talkText,
  468. this.endTalk
  469. );
  470. } else {
  471. _talkTextIiframe2.contentWindow.closesynthesizer();
  472. }
  473. },
  474. endTalk() {
  475. console.log("👉转语音结束👈");
  476. this.talkLoading = false;
  477. },
  478. stopTalk() {
  479. this.talkTextList = [];
  480. let _talkTextIiframe2 = this.$refs.iiframe2;
  481. _talkTextIiframe2.contentWindow.closesynthesizer();
  482. _talkTextIiframe2.contentWindow.pausesynthesizer();
  483. this.talkLoading = false;
  484. }
  485. },
  486. mounted() {
  487. // // this.recordStart()
  488. // setTimeout(()=>{
  489. // this.recordStart("可可同学。")
  490. // setTimeout(()=>{
  491. // this.recordStart("世界上最大的火山是什么。")
  492. // setTimeout(()=>{
  493. // this.recordStart("位于哪里。他温度怎么样。")
  494. // },2000)
  495. // },2000)
  496. // },5000)
  497. }
  498. };
  499. </script>
  500. <style scoped>
  501. .levitatedSphere {
  502. position: fixed;
  503. width: auto;
  504. height: auto;
  505. top: 20px;
  506. right: 10px;
  507. z-index: 9999;
  508. display: flex;
  509. }
  510. .ls_hello {
  511. width: 80px;
  512. height: 80px;
  513. position: absolute;
  514. top: 0;
  515. right: 0;
  516. cursor: pointer;
  517. animation: smallToBig 1s both;
  518. }
  519. /* 定义渐入动画 */
  520. @keyframes smallToBig {
  521. from {
  522. opacity: 0;
  523. transform: scale(0.1);
  524. }
  525. to {
  526. opacity: 1;
  527. transform: scale(1);
  528. }
  529. }
  530. .ls_text {
  531. position: absolute;
  532. right: 100px;
  533. top: 20px;
  534. max-width: 300px;
  535. display: flex;
  536. justify-content: flex-end;
  537. }
  538. .ls_t_ai {
  539. padding: 12px;
  540. width: auto;
  541. height: auto;
  542. border-radius: 12px 0px 12px 12px;
  543. background: #00000099;
  544. color: #fff;
  545. }
  546. .ls_t_user {
  547. padding: 12px;
  548. width: auto;
  549. height: auto;
  550. border-radius: 12px 12px 12px 12px;
  551. background: #00000099;
  552. color: #fff;
  553. }
  554. </style>