levitatedSphere.vue 17 KB

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