levitatedSphere.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. <template>
  2. <div class="levitatedSphere" v-show="show">
  3. <div class="ls_hello" v-if="showIndex == 0">
  4. <el-image
  5. style="width: 100%; height: 100%"
  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: 100%; height: 100%"
  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: 100%; height: 100%"
  18. v-show="aiStatus == 0"
  19. :src="require('../../../assets/icon/course/aiWait.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, //0 :在说话 1 : 接收 2:待命
  64. aiStatus: 0,
  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. recordStart(_text) {
  87. // try {
  88. // this.$parent.changeRecordType(1);
  89. // return this.$message.success("已开启语音助手,请说“可可同学”来唤醒")
  90. if (this.isOpen)
  91. return this.$message.info("已开启语音助手,无需重复开启");
  92. let iiframe = this.$refs["iiframe"];
  93. iiframe.contentWindow.window.document.getElementById(
  94. "languageOptions"
  95. ).selectedIndex = 2; //普通话
  96. iiframe.contentWindow.testdoContinuousPronunciationAssessment();
  97. this.$message.success("已开启语音助手,请说“可可同学”来唤醒");
  98. this.$parent.changeRecordType(1);
  99. this.isOpen = true;
  100. iiframe.contentWindow.onRecognizedResult = e => {
  101. let _msg = e.privText;
  102. // let _msg = _text;
  103. console.log("👇");
  104. console.log(_msg);
  105. if (!_msg) return console.log("输出为空");
  106. if (_msg.indexOf("可可同学") != -1 && !this.show) {
  107. this.aiText = "您好,我是小可,有什么可以帮助您的?";
  108. this.aiStatus = 0;
  109. this.showIndex = 0;
  110. this.show = true;
  111. console.log("已唤醒");
  112. return;
  113. } else if (this.show == true) {
  114. if (this.showTextIndex == 2 || this.chatLoading || this.talkLoading) {
  115. return console.log("组织语言中");
  116. } else {
  117. this.showTextIndex = 1;
  118. this.aiText = "";
  119. this.userText += _msg;
  120. this.aiStatus = 1;
  121. if (this.timer) {
  122. clearTimeout(this.timer);
  123. this.timer = null;
  124. }
  125. this.timer = setTimeout(() => {
  126. if (this.userText.indexOf("关闭语音助手") != -1) {
  127. // return setTimeout(()=>{
  128. (this.show = false), (this.showTextIndex = 3);
  129. this.aiStatus = 2;
  130. this.aiText = "";
  131. this.userText = "";
  132. this.stopRecord();
  133. return;
  134. // },1000)
  135. }
  136. this.showTextIndex = 2;
  137. this.aiText = "";
  138. if (/计时(.+)分钟/.test(this.userText)) {
  139. // setTimeout(() => {
  140. let _number = this.userText.match(/计时(.+)分钟/)[1];
  141. let _time = 0;
  142. if (!/^\d+$/.test(_number)) {
  143. _time = this.chineseToNumber(_number) * 60;
  144. } else {
  145. _time = parseInt(_numberList[1]) * 60;
  146. }
  147. this.$emit("startTime", _time);
  148. this.aiStatus = 0;
  149. this.showTextIndex = 0;
  150. this.aiText =
  151. "好的,我已为您计时" +
  152. this.userText.match(/计时(.+)分钟/)[1] +
  153. "分钟。";
  154. this.userText = "";
  155. this.timer = setTimeout(() => {
  156. this.showTextIndex = 3;
  157. this.aiStatus = 2;
  158. this.aiText = "";
  159. this.userText = "";
  160. }, 3000);
  161. // }, 2000);
  162. } else {
  163. this.chatLoading = true;
  164. const _uuid = uuidv4();
  165. let params = {
  166. assistant_id: "f8e1ebb2-2e0d-11ef-8bf4-12e77c4cb76b",
  167. userId: this.userId,
  168. message: this.userText,
  169. session_name: _uuid + "-qgt",
  170. uid: _uuid,
  171. file_ids: []
  172. };
  173. this.ajax
  174. // .post("https://claude3.cocorobo.cn/chat", params)
  175. // .post("https://gpt4.cocorobo.cn/chat", params)
  176. .post(
  177. "https://gpt4.cocorobo.cn/ai_agent_park_chat_new",
  178. params
  179. )
  180. .then(res => {
  181. if (res.data.FunctionResponse.result == "发送成功") {
  182. this.userText = "";
  183. } else {
  184. // this.$message.warning(res.data.FunctionResponse.result);
  185. console.log(res.data.FunctionResponse.result);
  186. this.chatLoading = false;
  187. this.aiStatus = 0;
  188. this.showTextIndex = 0;
  189. this.aiText = "对不起,我无法理解您的问题,请重新提问";
  190. this.timer = setTimeout(() => {
  191. this.showTextIndex = 3;
  192. this.aiStatus = 2;
  193. this.aiText = "";
  194. this.userText = "";
  195. }, 3000);
  196. }
  197. })
  198. .catch(e => {
  199. console.log(e);
  200. this.chatLoading = false;
  201. this.aiStatus = 0;
  202. this.showTextIndex = 0;
  203. this.aiText = "对不起,我无法理解您的问题,请重新提问";
  204. this.timer = setTimeout(() => {
  205. this.showTextIndex = 3;
  206. this.aiStatus = 2;
  207. this.aiText = "";
  208. this.userText = "";
  209. }, 3000);
  210. });
  211. // 通过流获取ai对话数据
  212. this.getAtAuContent(_uuid);
  213. }
  214. }, 5000);
  215. }
  216. } else {
  217. console.log("不响应");
  218. }
  219. };
  220. // } catch (error) {
  221. // console.log("ai录音报错👇");
  222. // console.log(error);
  223. // setTimeout(() => {
  224. // this.recordStart();
  225. // }, 1000);
  226. // }
  227. },
  228. stopRecord() {
  229. // this.$parent.changeRecordType(0);
  230. // this.$message.success("已关闭语音助手")
  231. // return
  232. let iiframe = this.$refs["iiframe"];
  233. iiframe.contentWindow.window.document
  234. .getElementById("scenarioStopButton")
  235. .click();
  236. // 录音借宿
  237. iiframe.contentWindow.onSessionStopped = (s, e) => {
  238. this.isOpen = false;
  239. this.show = false;
  240. this.showTextIndex = 3;
  241. this.$parent.changeRecordType(0);
  242. this.$message.success("已关闭语音助手");
  243. if (this.talkLoading) {
  244. this.$refs.iiframe2.contentWindow.closesynthesizer();
  245. }
  246. this.userText = "";
  247. this.aiText = "";
  248. };
  249. },
  250. chineseToNumber(chinese) {
  251. const chineseNumbers = {
  252. 零: 0,
  253. 一: 1,
  254. 二: 2,
  255. 三: 3,
  256. 四: 4,
  257. 五: 5,
  258. 六: 6,
  259. 七: 7,
  260. 八: 8,
  261. 九: 9,
  262. 十: 10,
  263. 百: 100,
  264. 千: 1000,
  265. 万: 10000,
  266. 亿: 100000000
  267. };
  268. let result = 0;
  269. let tempNum = 0; // 用于累积处理
  270. let sectionNum = 0; // 每个段的值
  271. for (let i = 0; i < chinese.length; i++) {
  272. const char = chinese[i];
  273. const num = chineseNumbers[char];
  274. if (num === undefined) {
  275. throw new Error(`Unexpected character: ${char}`);
  276. }
  277. if (
  278. num === 10 ||
  279. num === 100 ||
  280. num === 1000 ||
  281. num === 10000 ||
  282. num === 100000000
  283. ) {
  284. if (tempNum === 0) tempNum = 1; // 如果前面没有数,默认是1
  285. tempNum *= num;
  286. if (num === 10000 || num === 100000000) {
  287. sectionNum += tempNum;
  288. result += sectionNum;
  289. tempNum = 0;
  290. sectionNum = 0;
  291. }
  292. } else {
  293. tempNum += num;
  294. }
  295. }
  296. result += sectionNum + tempNum;
  297. return result;
  298. },
  299. removeMarkdown(text) {
  300. return text
  301. .replace(/[#*_~`>+\-]/g, "") // 移除 #、*、_、~、`、>、+、- 符号
  302. .replace(/!\[.*?\]\(.*?\)/g, "") // 移除图片
  303. .replace(/\[.*?\]\(.*?\)/g, "") // 移除链接
  304. .replace(/```[\s\S]*?```/g, "") // 移除代码块(不使用 s 标志)
  305. .replace(/`[^`]*`/g, "") // 移除行内代码
  306. .replace(/\d+\./g, "") // 移除有序列表
  307. .replace(/^\s*[-*+]\s+/gm, "") // 移除无序列表
  308. .replace(/\s+/g, " ") // 将多个空白字符替换为一个空格
  309. .trim(); // 去除字符串两端的空白字符
  310. },
  311. getAtAuContent(_uid) {
  312. this.source = new EventSource(
  313. `https://gpt4.cocorobo.cn/question/${_uid}`
  314. );
  315. //http://gpt4.cocorobo.cn:8011/question/ https://gpt4.cocorobo.cn/question/
  316. let _allText = "";
  317. let _mdText = "";
  318. let _talkText = "";
  319. // let _talkIndex = 0;
  320. // const md = new MarkdownIt();
  321. this.source.onmessage = _e => {
  322. let _eData = JSON.parse(_e.data);
  323. if (_eData.content.replace("'", "").replace("'", "") == "[DONE]") {
  324. let _result = [];
  325. if ("result" in _eData) {
  326. _result = _eData.result;
  327. for (let i = 0; i < _result.length; i++) {
  328. _mdText = _mdText.replace(_result[i].text, _result[i].fileName);
  329. }
  330. }
  331. _mdText = _mdText.replace("_", "");
  332. this.aiText = _mdText;
  333. if (_talkText != "") {
  334. let _resultText = this.removeMarkdown(_talkText);
  335. this.talkTextList.push(_resultText);
  336. _talkText = "";
  337. if (!this.talkLoading) this.talkText();
  338. }
  339. this.chatLoading = false;
  340. this.source.close();
  341. } else {
  342. // _talkIndex+=1;
  343. let _text = _eData.content.replace("'", "").replace("'", "");
  344. if (_allText == "") {
  345. _allText = _text.replace(/^\n+/, ""); //去掉回复消息中偶尔开头就存在的连续换行符
  346. _talkText += _text.replace(/^\n+/, "");
  347. } else {
  348. _allText += _text;
  349. _talkText += _text;
  350. }
  351. `~`;
  352. _mdText = _allText + "_";
  353. _mdText = _mdText.replace(/\\n/g, "\n");
  354. _mdText = _mdText.replace(/\\/g, "");
  355. if (_allText.split("```").length % 2 == 0) _mdText += "\n```\n";
  356. this.aiText = _mdText;
  357. this.showTextIndex = 0;
  358. if (/[,。:;?!)]/.test(_talkText)) {
  359. let _resultText = this.removeMarkdown(_talkText);
  360. this.talkTextList.push(_resultText);
  361. _talkText = "";
  362. if (!this.talkLoading) this.talkText();
  363. }
  364. // if(_talkIndex==10){
  365. // _talkIndex = 0;
  366. // this.talkTextList.push(_talkText)
  367. // _talkText = "";
  368. // if(!this.talkLoading)this.talkText();
  369. // }
  370. }
  371. };
  372. },
  373. talkText() {
  374. let _text = this.talkTextList.shift();
  375. let _talkTextIiframe2 = this.$refs.iiframe2;
  376. if (_text) {
  377. this.talkLoading = true;
  378. if (this.timer) {
  379. clearTimeout(this.timer);
  380. this.timer = null;
  381. }
  382. // console.log(`👉转语音:${_text}`);
  383. // setTimeout(()=>{
  384. // this.talkText();
  385. // },2000)
  386. _talkTextIiframe2.contentWindow.texttospeech(_text, this.talkText);
  387. } else {
  388. console.log("👉转语音结束👈");
  389. _talkTextIiframe2.contentWindow.closesynthesizer();
  390. this.talkLoading = false;
  391. // this.timer = setTimeout(() => {
  392. // this.showTextIndex = 3;
  393. // this.aiStatus = 2;
  394. // this.aiText = "";
  395. // this.userText = "";
  396. // this.time = null;
  397. // }, 5000);
  398. }
  399. }
  400. },
  401. mounted() {
  402. // // this.recordStart()
  403. // setTimeout(()=>{
  404. // this.recordStart("可可同学。")
  405. // setTimeout(()=>{
  406. // this.recordStart("世界上最大的火山是什么。")
  407. // setTimeout(()=>{
  408. // this.recordStart("位于哪里。他温度怎么样。")
  409. // },2000)
  410. // },2000)
  411. // },2000)
  412. }
  413. };
  414. </script>
  415. <style scoped>
  416. .levitatedSphere {
  417. position: fixed;
  418. width: auto;
  419. height: auto;
  420. top: 20px;
  421. right: 10px;
  422. z-index: 9999;
  423. display: flex;
  424. }
  425. .ls_hello {
  426. width: 80px;
  427. height: 80px;
  428. position: absolute;
  429. top: 0;
  430. right: 0;
  431. }
  432. .ls_text {
  433. position: absolute;
  434. right: 100px;
  435. top: 20px;
  436. max-width: 300px;
  437. display: flex;
  438. justify-content: flex-end;
  439. }
  440. .ls_t_ai {
  441. padding: 12px;
  442. width: auto;
  443. height: auto;
  444. border-radius: 12px 0px 12px 12px;
  445. background: #00000099;
  446. color: #fff;
  447. }
  448. .ls_t_user {
  449. padding: 12px;
  450. width: auto;
  451. height: auto;
  452. border-radius: 12px 12px 12px 12px;
  453. background: #00000099;
  454. color: #fff;
  455. }
  456. </style>