levitatedSphere.vue 17 KB

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