mixin.js 82 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227
  1. import {
  2. v4 as uuidv4
  3. } from 'uuid';
  4. var OpenCC = require("opencc-js");
  5. let converter = OpenCC.Converter({
  6. from: "hk",
  7. to: "cn"
  8. });
  9. import _ from "lodash";
  10. import Papa from "papaparse";
  11. import markdownIt from "markdown-it";
  12. import QRCode from "qrcodejs2";
  13. import * as echarts from "echarts";
  14. import "echarts-wordcloud";
  15. // word
  16. import htmlDocx from "html-docx-js/dist/html-docx";
  17. export const toolMixin = {
  18. data(){
  19. return{
  20. tag: {
  21. 0: "一",
  22. 1: "二",
  23. 2: "三",
  24. 3: "四",
  25. 4: "五",
  26. 5: "六",
  27. 6: "七",
  28. 7: "八",
  29. 8: "九",
  30. 9: "十",
  31. 10: "十一",
  32. 11: "十二",
  33. 12: "十三",
  34. 13: "十四",
  35. 14: "十五",
  36. 15: "十六",
  37. 16: "十七",
  38. 17: "十八",
  39. 18: "十九",
  40. 19: "二十"
  41. }
  42. }
  43. },
  44. methods: {
  45. testMixin() {
  46. },
  47. getTextContentMixin(file) {
  48. return new Promise((resolve) => {
  49. const txtRegex = /\.(txt|csv)$/i;
  50. if (txtRegex.test(file.url)) {
  51. this.getFile(file.url).then(fileData => {
  52. const arr = Papa.parse(fileData.data, {
  53. header: false
  54. }).data.slice(1);
  55. // console.log(arr)
  56. const _editorBarDataContent = `<table
  57. border="0"
  58. width="100%"
  59. cellpadding="0"
  60. cellspacing="0"
  61. style="text-align: center">
  62. <tbody>
  63. <tr>
  64. <th>序号</th>
  65. <th>开始时间</th>
  66. <th>结束时间</th>
  67. <th>发言内容</th>
  68. <th>时长</th>
  69. <th>说话人身份</th>
  70. <th>行为编码</th>
  71. </tr>
  72. ${arr.map(row => `<tr>
  73. <td>${_.get(row, 0, "")}</td>
  74. <td>${_.get(row, 1, "")}</td>
  75. <td>${_.get(row, 2, "")}</td>
  76. <td>${_.get(row, 3, "")}</td>
  77. <td>${_.get(row, 4, "")}</td>
  78. <td>${_.get(row, 5, "")}</td>
  79. <td>${_.get(row, 6, "")}</td>
  80. </tr>
  81. `).join("\n")}</tbody></table>`;
  82. var blob = new Blob([_editorBarDataContent], { type: "text/plain;charset=utf-8" });
  83. blob.lastModifiedDate = new Date();
  84. blob.name = `${file.name.replace('.txt', '')}_classroomObservation.txt`;
  85. this.uploadFileMixin(blob).then(upload => {
  86. resolve({ editorBarData: { type: "0", url: upload.Location, content: _editorBarDataContent } })
  87. // this.ajax
  88. // .put("https://gpt4.cocorobo.cn/upload_file_knowledge", {
  89. // url: upload.Location
  90. // })
  91. // .then(res => {
  92. // let resData = res.data.FunctionResponse;
  93. // if (resData.result && resData.result.id) {
  94. // resolve({ fileId: resData.result.id, editorBarData: { type: "0", url: upload.Location }, })
  95. // }
  96. // })
  97. })
  98. })
  99. }
  100. })
  101. },
  102. uploadFileMixin(file) {
  103. return new Promise((resolve, reject) => {
  104. var credentials = {
  105. accessKeyId: "AKIATLPEDU37QV5CHLMH",
  106. secretAccessKey: "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"
  107. }; //秘钥形式的登录上传
  108. window.AWS.config.update(credentials);
  109. window.AWS.config.region = "cn-northwest-1"; //设置区域
  110. var bucket = new window.AWS.S3({ params: { Bucket: "ccrb" } }); //选择桶
  111. var _this = this;
  112. if (file) {
  113. var params = {
  114. Key:
  115. file.name.split(".")[0] +
  116. new Date().getTime() +
  117. "." +
  118. file.name.split(".")[file.name.split(".").length - 1],
  119. ContentType: file.type,
  120. Body: file,
  121. "Access-Control-Allow-Credentials": "*",
  122. ACL: "public-read"
  123. }; //key可以设置为桶的相抵路径,Body为文件, ACL最好要设置
  124. var options = {
  125. partSize: 2048 * 1024 * 1024,
  126. queueSize: 2,
  127. leavePartsOnError: true
  128. };
  129. bucket
  130. .upload(params, options)
  131. .on("httpUploadProgress", function (evt) {
  132. //这里可以写进度条
  133. // _this.progressData.value = parseInt((evt.loaded * 100) / evt.total);
  134. // console.log("Uploaded : " + parseInt((evt.loaded * 80) / evt.total) + '%');
  135. })
  136. .send(function (err, data) {
  137. if (err) {
  138. _this.$message.error("上传失败");
  139. } else {
  140. resolve(data);
  141. }
  142. });
  143. }
  144. })
  145. },
  146. getFileIdMixin(url) {
  147. return new Promise((resolve) => {
  148. this.ajax
  149. .put("https://gpt4.cocorobo.cn/upload_file_knowledge", {
  150. url: url
  151. })
  152. .then(res => {
  153. let resData = res.data.FunctionResponse;
  154. if (resData.result && resData.result.id) {
  155. resolve({ fileId: resData.result.id })
  156. }
  157. })
  158. })
  159. },
  160. getVideoToVoiceAndUploadMixin(fileData) {
  161. return new Promise(async (resolve) => {
  162. let _file = null;
  163. console.log("fileData👉", fileData)
  164. if (fileData.fileObj) {
  165. _file = fileData.fileObj
  166. } else if (fileData.url) {
  167. let videoRes = await this.getFileBody(fileData.url);
  168. if (videoRes.data === 1) return resolve({ data: 1 })
  169. // 把uint8Array转换为视频文件
  170. _file = new File([videoRes.data], 'video.mp4', { type: 'video/mp4' });
  171. }
  172. if (!_file) return resolve({ data: 2,err:"未找到文件" })
  173. console.log("需要处理的文件👉", _file)
  174. try {
  175. const reader = new FileReader();
  176. reader.onload = async (e) => {
  177. try {
  178. // 创建音频上下文
  179. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  180. //解码音频数据
  181. const buffer = await audioContext.decodeAudioData(e.target.result);
  182. //创建离线音频上下文
  183. const offlineAudioContext = new OfflineAudioContext({ numberOfChannels: buffer.numberOfChannels, length: buffer.length, sampleRate: buffer.sampleRate });
  184. //创建音源节点
  185. const source = offlineAudioContext.createBufferSource();
  186. source.buffer = buffer;
  187. source.connect(offlineAudioContext.destination);
  188. source.start();
  189. //渲染音频
  190. const renderedBuffer = await offlineAudioContext.startRendering();
  191. const wavBlob = this.bufferToWav(renderedBuffer);
  192. // blob转成file文件
  193. const audioFile = new File([wavBlob], 'audio.wav', { type: 'audio/wav' });
  194. this.uploadFileMixin(audioFile).then(upload => {
  195. resolve({ audioUrl: upload, fileObj: audioFile })
  196. })
  197. } catch (error) {
  198. console.log("👉", error);
  199. return resolve({ data: 2,err:error })
  200. }
  201. }
  202. reader.readAsArrayBuffer(_file);
  203. } catch (error) {
  204. console.log("👉", error);
  205. return resolve({ data: 2,err:error })
  206. }
  207. })
  208. },
  209. bufferToWav(audioBuffer) {
  210. const numOfChan = audioBuffer.numberOfChannels;
  211. const length = audioBuffer.length * numOfChan * 2;
  212. const buffer = new ArrayBuffer(44 + length);
  213. const view = new DataView(buffer);
  214. const channels = [];
  215. let pos = 0;
  216. // 获取通道数据
  217. for (let i = 0; i < audioBuffer.numberOfChannels; i++) {
  218. channels.push(audioBuffer.getChannelData(i));
  219. }
  220. // 写入WAV头
  221. this.writeUTFBytes(view, 0, 'RIFF');
  222. view.setUint32(4, 44 + length - 8, true);
  223. this.writeUTFBytes(view, 8, 'WAVE');
  224. this.writeUTFBytes(view, 12, 'fmt ');
  225. view.setUint32(16, 16, true);
  226. view.setUint16(20, 1, true);
  227. view.setUint16(22, numOfChan, true);
  228. view.setUint32(24, audioBuffer.sampleRate, true);
  229. view.setUint32(28, audioBuffer.sampleRate * 2 * numOfChan, true);
  230. view.setUint16(32, numOfChan * 2, true);
  231. view.setUint16(34, 16, true);
  232. this.writeUTFBytes(view, 36, 'data');
  233. view.setUint32(40, length, true);
  234. // 写入PCM数据
  235. pos = 44;
  236. for (let i = 0; i < audioBuffer.length; i++) {
  237. for (let j = 0; j < numOfChan; j++) {
  238. const sample = Math.max(-1, Math.min(1, channels[j][i]));
  239. view.setInt16(pos, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);
  240. pos += 2;
  241. }
  242. }
  243. return new Blob([buffer], { type: 'audio/wav' });
  244. },
  245. writeUTFBytes(view, offset, string) {
  246. for (let i = 0; i < string.length; i++) {
  247. view.setUint8(offset + i, string.charCodeAt(i));
  248. }
  249. },
  250. getFile(url) {
  251. return new Promise((resolve, reject) => {
  252. var credentials = {
  253. accessKeyId: "AKIATLPEDU37QV5CHLMH",
  254. secretAccessKey: "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"
  255. }; //秘钥形式的登录上传
  256. window.AWS.config.update(credentials);
  257. window.AWS.config.region = "cn-northwest-1"; //设置区域
  258. let url2 = url;
  259. let _url2 = "";
  260. if (
  261. url2.indexOf("https://view.officeapps.live.com/op/view.aspx?src=") != -1
  262. ) {
  263. _url2 = url2.split(
  264. "https://view.officeapps.live.com/op/view.aspx?src="
  265. )[1];
  266. } else {
  267. _url2 = url2;
  268. }
  269. var s3 = new window.AWS.S3({ params: { Bucket: "ccrb" } });
  270. let name = decodeURIComponent(
  271. _url2.split("https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/")[1]
  272. );
  273. var params = {
  274. Bucket: "ccrb",
  275. Key: name
  276. };
  277. s3.getObject(params, function (err, data) {
  278. if (err) {
  279. console.log(err, err.stack);
  280. resolve({ data: 1 });
  281. } else {
  282. const fileContent = data.Body.toString("utf-8");
  283. resolve({ data: fileContent });
  284. } // sxuccessful response
  285. });
  286. // axios({
  287. });
  288. },
  289. getFileBody(url) {
  290. return new Promise((resolve, reject) => {
  291. var credentials = {
  292. accessKeyId: "AKIATLPEDU37QV5CHLMH",
  293. secretAccessKey: "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",
  294. }; //秘钥形式的登录上传
  295. window.AWS.config.update(credentials);
  296. window.AWS.config.region = "cn-northwest-1"; //设置区域
  297. let url2 = url;
  298. let _url2 = "";
  299. if (
  300. url2.indexOf("https://view.officeapps.live.com/op/view.aspx?src=") != -1
  301. ) {
  302. _url2 = url2.split(
  303. "https://view.officeapps.live.com/op/view.aspx?src="
  304. )[1];
  305. } else {
  306. _url2 = url2;
  307. }
  308. var s3 = new window.AWS.S3({ params: { Bucket: "ccrb" } });
  309. let name = decodeURIComponent(_url2.split("https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/")[1])
  310. var params = {
  311. Bucket: "ccrb",
  312. Key: name
  313. };
  314. s3.getObject(params, function (err, data) {
  315. if (err) {
  316. console.log(err, err.stack)
  317. resolve({ data: 1 });
  318. } else {
  319. resolve({ data: data.Body });
  320. console.log(data);
  321. } // sxuccessful response
  322. });
  323. });
  324. },
  325. getAnalysisMixin(obj) {
  326. return new Promise(async (resolve) => {
  327. let { fileId, assistantData, content, analysisData, baseMessage } = obj;
  328. let type = 0;
  329. let tips = ""
  330. analysisData.mId = assistantData.id;
  331. // console.log("处理数据👉", fileId, assistantData, content, analysisData, baseMessage)
  332. if (['f8795150-699c-11ef-b873-005056b86db5', '01928d2b-699d-11ef-b873-005056b86db5', '069af7b9-699d-11ef-b873-005056b86db5', 'bfe844b1-7a45-11ef-9b30-005056b86db5'].includes(assistantData.id)) {//S-T分析:课堂时间分配 S-T分析:师生互动分析 S-T分析:教学模式分析 课堂活动光谱图
  333. try {
  334. let _result = [];
  335. let _data = content;
  336. let _div = document.createElement("div");
  337. _div.innerHTML = _data;
  338. let _tableRows = _div.querySelectorAll(`table tbody tr`);
  339. _tableRows.forEach((i, index) => {
  340. if (index == 0) return;
  341. let obj = {
  342. index: i.cells[0].textContent,
  343. startTime: i.cells[1].textContent,
  344. endTime: i.cells[2].textContent,
  345. message: i.cells[3].textContent,
  346. time: i.cells[4].textContent,
  347. role: i.cells[5] ? i.cells[5].textContent : "",
  348. behavior: i.cells[6] ? i.cells[6].textContent : ""
  349. };
  350. _result.push(obj);
  351. });
  352. if (_result.length == 0) return resolve({ data: 1, err: "未找到表格数据" });
  353. if (assistantData.id == "f8795150-699c-11ef-b873-005056b86db5") {//课堂时间分配
  354. let resultData = await this.getTimeAllocationDataMixin(_result, fileId)
  355. if (resultData.message) analysisData.content = resultData.message;
  356. if (resultData.eCharts) analysisData.eChartData = resultData.eCharts;
  357. return resolve({ data: analysisData })
  358. } else if (assistantData.id == "01928d2b-699d-11ef-b873-005056b86db5") {//师生互动分析
  359. let resultData = await this.getInteractionAnalysisData(_result, fileId)
  360. if (resultData.message) analysisData.content = resultData.message;
  361. if (resultData.eCharts) analysisData.eChartData = resultData.eCharts;
  362. return resolve({ data: analysisData })
  363. } else if (assistantData.id == "069af7b9-699d-11ef-b873-005056b86db5") {//教学模式分析
  364. let resultData = await this.getTeachingModeData(_result, fileId)
  365. if (resultData.message) analysisData.content = resultData.message;
  366. if (resultData.RT) analysisData.RT = resultData.RT;
  367. if (resultData.CH) analysisData.CH = resultData.CH;
  368. return resolve({ data: analysisData })
  369. } else if (assistantData.id == "bfe844b1-7a45-11ef-9b30-005056b86db5") {//课堂活动光谱图
  370. let resultData = await this.getSpectrogram(_result, fileId, content, assistantData)
  371. if (resultData.message) analysisData.content = resultData.message;
  372. if (resultData.eCharts) analysisData.getSpectrogram = resultData.eCharts;
  373. return resolve({ data: analysisData })
  374. }
  375. } catch (error) {
  376. return resolve({ data: 1, err: err })
  377. }
  378. } else {
  379. let _msg = `使用文件检索的方式完整的去分析文件内容,并请完全按照要求输出。`;
  380. if (assistantData.tips) {
  381. tips = assistantData.tips;
  382. type = 1;
  383. } else if (assistantData.agentid) {
  384. type = 0;
  385. }
  386. if (assistantData.id === '6b4a9650-48be-11ef-936b-12e77c4cb76b') {
  387. _msg = `使用文件检索的方式完整的去分析文件内容,并基于以下的课堂基本内容,使用cpote课程设计模型改编一堂同主题的课程。
  388. 课堂名称:${baseMessage.courseName} 搜课年级:${baseMessage.grade} 授课科目:${baseMessage.subject}`;
  389. }
  390. let params = {
  391. id:
  392. type == 0
  393. ? assistantData.agentid
  394. : "f8e1ebb2-2e0d-11ef-8bf4-12e77c4cb76b",
  395. message: type == 0 ? _msg : tips,
  396. session_name: uuidv4(),
  397. userId: this.userId,
  398. file_ids: fileId ? [fileId] : [],
  399. model: "gpt-4o-2024-11-20",
  400. sound_url: "",
  401. temperature: 0.2,
  402. top_p: 1,
  403. max_completion_tokens: 4096,
  404. stream: false,
  405. uid: uuidv4()
  406. };
  407. this.ajax
  408. .post("https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat", params)
  409. .then(async res => {
  410. let _data = res.data;
  411. analysisData.content = _data.message;
  412. if (['1', '2', '3'].includes(assistantData.echartsType)) {
  413. let echartsData = await this.getEChartsDataMixin(assistantData.echartsType, _data.message)
  414. if (echartsData.data == 1) {
  415. console.log(`生成表格失败${echartsData.err}`)
  416. resolve({ data: analysisData })
  417. } else {
  418. analysisData.eChartData = echartsData.data;
  419. resolve({ data: analysisData })
  420. }
  421. } else {
  422. resolve({ data: analysisData })
  423. }
  424. })
  425. .catch(err => {
  426. resolve({ data: 1, err: err })
  427. });
  428. }
  429. })
  430. },
  431. getContentTableMixin(content) {
  432. return new Promise(resolve => {
  433. let _content = content;
  434. const md = new markdownIt();
  435. let _contentHtml = md.render(_content);
  436. let _contentTableList = [];
  437. const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/g; // 匹配表格行,[\s\S] 匹配所有字符
  438. const cellRegex = /<(th|td)[^>]*>([\s\S]*?)<\/\1>/g; // 匹配单元格,[\s\S] 匹配所有字符
  439. let rowMatch;
  440. while ((rowMatch = rowRegex.exec(_contentHtml)) !== null) {
  441. const rowContent = rowMatch[1]; // 每一行的内容
  442. const rowData = [];
  443. let cellMatch;
  444. // 匹配每个单元格 (th 或 td)
  445. while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
  446. let _text = cellMatch[2].trim();
  447. _text = _text.replace(/&[a-zA-Z]+;/g, "");
  448. _text = _text.replace(/<\/?[^>]+(>|$)/g, "");
  449. rowData.push(_text); // 将每个单元格的内容添加到当前行的数组中
  450. }
  451. // 如果该行有数据,推送到 _contentTableList 中
  452. if (rowData.length) {
  453. _contentTableList.push(rowData);
  454. }
  455. }
  456. // 输出提取的表格数据
  457. resolve(_contentTableList);
  458. });
  459. },
  460. getEChartsDataMixin(type = "1", content) {
  461. return new Promise((resolve) => {
  462. if (type === "1") {
  463. //词云图
  464. return this.getContentTableMixin(content).then(res => {
  465. try {
  466. if (res.length <= 0) {
  467. return resolve({ data: 0 })//未找到表格数据
  468. }
  469. let _result = [];
  470. res.forEach((item, index) => {
  471. if (index == 0) return; //去掉表头
  472. let _valueItem = item[2] ? item[2] : item[1];
  473. let _value = _valueItem.match(/(\d+)/);
  474. _value = _value ? parseInt(_value[0]) : 0;
  475. _result.push({
  476. value: _value,
  477. name: item[0],
  478. textStyle: { color: this.getRandomColorMixin() }
  479. });
  480. });
  481. let _option = {
  482. tooltip: {
  483. show: false
  484. },
  485. series: [
  486. {
  487. type: "wordCloud",
  488. sizeRange: [14, 38],
  489. rotationRange: [0, 0],
  490. keepAspect: false,
  491. shape: "circle",
  492. left: "center",
  493. top: "center",
  494. right: null,
  495. bottom: null,
  496. width: "100%",
  497. height: "100%",
  498. // rotationRange: [-90, 90],
  499. rotationStep: 20,
  500. data: _result
  501. }
  502. ]
  503. };
  504. resolve({ data: _option })
  505. } catch (e) {
  506. resolve({ data: 1, err: e })
  507. }
  508. });
  509. } else if (type == "2") {
  510. //雷达图
  511. //雷达图
  512. return this.getContentTableMixin(content).then(res => {
  513. try {
  514. if (res.length <= 0) {
  515. return resolve({ data: 0 })//未找到表格数据
  516. }
  517. let radarData = [];
  518. let seriesData = { value: [] };
  519. res.forEach((item, index) => {
  520. if (index == 0) return; //去掉表头
  521. radarData.push({ name: item[0], max: 5 });
  522. let _valueItem = item[1] ? item[1] : "0";
  523. let _value = _valueItem.match(/(\d+)/);
  524. _value = _value ? parseInt(_value[0]) : 0;
  525. seriesData.value.push(_value);
  526. });
  527. let _option = {
  528. legend: {
  529. textStyle: {
  530. color: '#000'
  531. }
  532. },
  533. radar: {
  534. // shape: 'circle',
  535. indicator: radarData,
  536. name: {
  537. textStyle: {
  538. color: '#000'
  539. }
  540. }
  541. },
  542. series: [
  543. {
  544. type: "radar",
  545. data: [seriesData],
  546. label: {
  547. color: '#000'
  548. }
  549. }
  550. ]
  551. };
  552. resolve({ data: _option })
  553. } catch (e) {
  554. resolve({ data: 1, err: e })
  555. }
  556. });
  557. } else if (type == "3") {
  558. //能量柱图
  559. return this.getContentTableMixin(content).then(res => {
  560. try {
  561. if (res.length <= 0) {
  562. return resolve({ data: 0 })//未找到表格数据
  563. }
  564. let _data = [];
  565. let stepList = [];
  566. stepList = this.calculateTopValuesMixin(res.length - 1);
  567. res.forEach((item, index) => {
  568. if (index == 0) return;
  569. let _valueItem = item[1] ? item[1] : "0";
  570. let _value = _valueItem.match(/(\d+)/);
  571. _value = _value ? parseInt(_value[0]) : 0;
  572. // 求百分比
  573. _value = Math.floor((_value / 5).toFixed(2) * 100);
  574. _data.push({
  575. value: _value,
  576. name: item[0],
  577. title: { offsetCenter: ["0%", `${stepList[index - 1]}%`] },
  578. detail: {
  579. valueAnimation: true,
  580. offsetCenter: ["0%", `${stepList[index - 1] + 15}%`]
  581. }
  582. });
  583. });
  584. let _option = {
  585. series: [
  586. {
  587. type: "gauge",
  588. startAngle: 90,
  589. endAngle: -270,
  590. pointer: {
  591. show: false
  592. },
  593. progress: {
  594. show: true,
  595. overlap: false,
  596. roundCap: true,
  597. clip: false,
  598. itemStyle: {
  599. borderWidth: 1,
  600. borderColor: "#464646"
  601. }
  602. },
  603. axisLine: {
  604. lineStyle: {
  605. width: 40
  606. }
  607. },
  608. splitLine: {
  609. show: false,
  610. distance: 0,
  611. length: 10
  612. },
  613. axisTick: {
  614. show: false
  615. },
  616. axisLabel: {
  617. show: false,
  618. distance: 50
  619. },
  620. data: _data,
  621. title: {
  622. fontSize: 14,
  623. color: "#000"
  624. },
  625. detail: {
  626. width: 50,
  627. height: 14,
  628. fontSize: 14,
  629. color: "#000",
  630. borderColor: "inherit",
  631. borderRadius: 20,
  632. borderWidth: 1,
  633. formatter: "{value}%"
  634. }
  635. }
  636. ]
  637. };
  638. resolve({ data: _option })
  639. } catch (e) {
  640. resolve({ data: 1, err: e })
  641. }
  642. });
  643. }
  644. })
  645. },
  646. getRandomColorMixin() {
  647. // 生成一个随机的 0 到 255 的数字,并将其转换为两位的十六进制形式
  648. const randomHex = () => {
  649. const hex = Math.floor(Math.random() * 256).toString(16);
  650. return hex.length === 1 ? "0" + hex : hex; // 确保每个部分有两位
  651. };
  652. // 组合三种颜色(红、绿、蓝)的随机值
  653. return `#${randomHex()}${randomHex()}${randomHex()}`;
  654. },
  655. calculateTopValuesMixin(len, minTop = -80, maxTop = 70, maxStep = 40) {
  656. const length = len;
  657. const middleIndex = Math.floor(length / 2); // 中间位置的索引
  658. const totalRange = maxTop - minTop;
  659. let step = totalRange / (length - 1); // 默认间隔
  660. // 如果间隔大于 10%,则设置为最大 10%
  661. if (step > maxStep) {
  662. step = maxStep;
  663. }
  664. const topValues = [];
  665. // 计算中间元素的top值
  666. const middleTop = (minTop + maxTop) / 2;
  667. topValues[middleIndex] = middleTop;
  668. // 从中间向两边扩展,确保每个元素的top值
  669. for (let i = middleIndex - 1; i >= 0; i--) {
  670. topValues[i] = topValues[i + 1] - step; // 向上扩展
  671. }
  672. for (let i = middleIndex + 1; i < length; i++) {
  673. topValues[i] = topValues[i - 1] + step; // 向下扩展
  674. }
  675. return topValues;
  676. },
  677. // 课堂时间分配
  678. getTimeAllocationDataMixin(_dataList, fileId) {
  679. return new Promise(async (resolve) => {
  680. let _data = _dataList.reduce(
  681. (pre, cur) => {
  682. if (cur.role == "学生") {
  683. pre[1].value += this.convertToSeconds(cur.time);
  684. } else if (cur.role == "老师") {
  685. pre[0].value += this.convertToSeconds(cur.time);
  686. }
  687. return pre;
  688. },
  689. [
  690. { value: 0, name: "老师" },
  691. { value: 0, name: "学生" }
  692. ]
  693. );
  694. let _dataPercentage = JSON.parse(JSON.stringify(_data));
  695. _data.forEach((i, index) => {
  696. _dataPercentage[index].percentage =
  697. (
  698. (i.value / _data.reduce((pre, cur) => pre + cur.value, 0)) *
  699. 100
  700. ).toFixed(2) + "%";
  701. });
  702. const _option = {
  703. tooltip: {
  704. left: "center",
  705. trigger: "item",
  706. formatter: "{a} <br/>{b}: {d}%",
  707. textStyle: {
  708. color: '#000000'
  709. }
  710. },
  711. legend: {
  712. top: "5%",
  713. left: "center",
  714. textStyle: {
  715. color: '#000000'
  716. }
  717. },
  718. series: [
  719. {
  720. name: "课堂时间分配",
  721. type: "pie",
  722. radius: ["40%", "70%"],
  723. label: {
  724. formatter: "{b}: {d}%",
  725. color: '#000000'
  726. },
  727. emphasis: {
  728. label: {
  729. show: true,
  730. formatter: "{b}: {d}%",
  731. color: '#000000'
  732. },
  733. itemStyle: {
  734. shadowBlur: 10,
  735. shadowOffsetX: 0,
  736. shadowColor: "rgba(0, 0, 0, 0.5)"
  737. }
  738. },
  739. data: _data
  740. }
  741. ]
  742. };
  743. let _msg = `这是某一节课的师生时间占比,请你分析,写出结论,并给出指导建议。请使用3句完整的话,分析并给出建议。 请注意,当老师或学生的时间占比在【40~59%】之间的时候,也认为师生占比约为1:1,各占50%,师生时间占比比较均衡。
  744. 师生时间占比数据:
  745. 老师占比:${_dataPercentage[0].percentage}
  746. 学生占比:${_dataPercentage[1].percentage}
  747. `;
  748. let message = await this.getAiContentMixin({ _msg: _msg, fileId: fileId })
  749. if (message.data == 1) {
  750. return resolve({ eCharts: _option, message: "" })
  751. } else {
  752. return resolve({ eCharts: _option, message: message.data })
  753. }
  754. })
  755. // return this.getAiContent(_msg);
  756. },
  757. // 师生互动分析
  758. getInteractionAnalysisData(_dataList, fileId) {
  759. return new Promise(async (resolve) => {
  760. let _pushData = [0, 0];
  761. let _result = [];
  762. _dataList.forEach(i => {
  763. if (i.role == "老师") {
  764. _pushData[0] += this.convertToSeconds(i.time);
  765. } else if (i.role == "学生") {
  766. _pushData[1] += this.convertToSeconds(i.time);
  767. }
  768. return _result.push(JSON.parse(JSON.stringify(_pushData)));
  769. });
  770. let _flatArray = _result.flat();
  771. const _max = Math.max(..._flatArray);
  772. const _maxValue = Math.ceil(_max / 100) * 100;
  773. const _option = {
  774. xAxis: {
  775. name: "老师", // X轴标题
  776. nameLocation: "end", // 标题位置
  777. scale: true,
  778. min: 0,
  779. max: _maxValue,
  780. axisLabel: {
  781. color: "#000" // 设置字体颜色为#000
  782. },
  783. nameTextStyle: {
  784. color: "#000" // 设置老师字体颜色为#000
  785. }
  786. },
  787. yAxis: {
  788. name: "学生", // Y轴标题
  789. nameLocation: "end", // 标题位置
  790. scale: true,
  791. min: 0,
  792. max: _maxValue,
  793. axisLabel: {
  794. color: "#000" // 设置字体颜色为#000
  795. },
  796. nameTextStyle: {
  797. color: "#000" // 设置学生字体颜色为#000
  798. }
  799. },
  800. grid: {
  801. containLabel: true
  802. },
  803. series: [
  804. {
  805. name: "数据",
  806. step: "start",
  807. data: _result,
  808. type: "line",
  809. lineStyle: {
  810. }
  811. },
  812. {
  813. name: "对角线",
  814. type: "line",
  815. data: [
  816. [0, 0],
  817. [_maxValue, _maxValue]
  818. ],
  819. lineStyle: {
  820. type: "dashed",
  821. },
  822. markLine: {
  823. symbol: ["none", "none"]
  824. }
  825. }
  826. ]
  827. };
  828. let _msg = `
  829. ## 任务
  830. 请你结合 FIAS 相关的知识,根据以下提供给你的课堂原始数据(包含S和t的数据),请你具体描述整个课堂S行为与T行为的持续性与变化性。比如,课堂一开始老师占比主导,大约5分钟之后,进入到学生为主的小组讨论环节。在整个课堂之中,老师与学生的互动比较频繁,老师会频繁询问学生问题,引导学生思考。之后是授课时间与问答时间。等等。
  831. ## 输出要求 请使用自然语言进行描述,使用不超过5句完整的话进行整体性、概括性的描述,不要包含具体的时长信息。总结性概括之后,使用1句话对整个课堂的教师引导行为进行鼓励和评价,再使用1句话给出相应的优化建议。
  832. ## 你的知识库 定义与目的:S-T图,即学生-教师(Student-Teacher)图,主要用于记录和分析课堂上的学生行为(S)与教师行为(T)的时间分布。这种图形能够帮助教育专家和教师可视化课堂互动的流程,从而判断课堂的教学型态,如练习型、对话型、讲授型或混合型。 绘制方法:S-T图的绘制开始于教学的起始时刻,纵轴表示学生行为(S),横轴表示教师行为(T)。实际课堂观察或录像回放中,按照固定时间间隔(通常每30秒)采样,将对应的行为按时间顺序标记在相应的轴上。通过这种方法,可以清晰看到课堂上教师行为与学生行为的交替模式及其随时间的变化。 应用场景:例如,一个典型的应用是在分析不同类型课堂活动时使用S-T图。在讲授型课堂中,教师行为的时间占比会较高,S-T图显示较长的横轴(T行为)延续;而在练习型或对话型课堂中,学生行为的时间占比增高,显示为较长的纵轴(S行为)。
  833. ## 课堂实录
  834. ${JSON.stringify(_dataList)}
  835. `;
  836. let message = await this.getAiContentMixin({ _msg: _msg, fileId: fileId })
  837. if (message.data == 1) {
  838. return resolve({ eCharts: _option, message: "" })
  839. } else {
  840. return resolve({ eCharts: _option, message: message.data })
  841. }
  842. })
  843. },
  844. // 教学模式分析
  845. getTeachingModeData(_dataList, fileId) {
  846. return new Promise(async (resolve) => {
  847. let _continuousTime = 0;
  848. let _totalTime = 0;
  849. let _continuousRole = "老师";
  850. let _teacherTime = 0;
  851. _dataList.forEach((item, index) => {
  852. if (index == 0) {
  853. //第一个
  854. _continuousRole = item.role;
  855. } else if (_dataList.length - 1 == index) {
  856. //最后一个
  857. if (_continuousRole == item.role) {
  858. //连续对话了
  859. _continuousTime += this.convertToSeconds(_dataList[index - 1].time);
  860. _continuousTime += this.convertToSeconds(item.time);
  861. } else {
  862. //没连续对话
  863. if (index >= 2) {
  864. if (_dataList[index - 2].role == _dataList[index - 1].role) {
  865. _continuousTime += this.convertToSeconds(
  866. _dataList[index - 1].time
  867. );
  868. } else {
  869. _continuousRole = item.role;
  870. }
  871. } else {
  872. _continuousRole = item.role;
  873. }
  874. }
  875. } else {
  876. if (_continuousRole == item.role) {
  877. //连续对话了
  878. _continuousTime += this.convertToSeconds(_dataList[index - 1].time);
  879. } else {
  880. //没连续对话
  881. if (index >= 2) {
  882. if (_dataList[index - 2].role == _dataList[index - 1].role) {
  883. _continuousTime += this.convertToSeconds(
  884. _dataList[index - 1].time
  885. );
  886. } else {
  887. _continuousRole = item.role;
  888. }
  889. } else {
  890. _continuousRole = item.role;
  891. }
  892. }
  893. }
  894. if (item.role == "老师") {
  895. _teacherTime += this.convertToSeconds(item.time);
  896. }
  897. _totalTime += this.convertToSeconds(item.time);
  898. });
  899. let _RT = (_teacherTime / _totalTime).toFixed(2);
  900. let _CH = (_continuousTime / _totalTime).toFixed(2);
  901. let _msg = `## 任务
  902. 根据FIAS(弗兰德斯互动分析系统)理论,计算获得某一节课的RT和CH值。请你结合FIAS相关知识进行分析,使用3句完整的话对整个课堂进行分析,需注意包含这些内容:分析该课堂所属的教学模型,描述课堂的整体表现与特征,肯定老师做出的努力,以及给出相应的建议。
  903. ## 你的知识
  904. 根据RT和CH的值,教学模式通常被分为以下几种类型: 练习型:RT ≤ 0.3,表示学生行为占主导,教师行为较少。 讲授型:RT ≥ 0.7,表示教师行为占主导,学生参与较少。 对话型:CH ≥ 0.4,表示师生之间有较多的互动和转换。 混合型:0.3 < RT < 0.7,CH < 0.4,表示教学中既有教师讲授也有学生参与,但两者都不占绝对优势。
  905. ## 数据
  906. RT:${_RT}
  907. CH:${_CH}
  908. `;
  909. let message = await this.getAiContentMixin({ _msg: _msg, fileId: fileId })
  910. if (message.data == 1) {
  911. return resolve({ RT: _RT, CH: _CH, message: "" })
  912. } else {
  913. return resolve({ RT: _RT, CH: _CH, message: message.data })
  914. }
  915. })
  916. },
  917. //光谱图
  918. getSpectrogram(_dataList, fileId, content, assistant) {
  919. return new Promise(async (resolve) => {
  920. try {
  921. this.getContentTableMixin(content).then(async res => {
  922. if (res.length <= 0) {
  923. resolve({ data: 1, err: "无表格数据" })
  924. }
  925. let _tableData = res;
  926. let _delIndex = _tableData.findIndex(i => i.includes("时间点"))
  927. _tableData = _tableData.slice(_delIndex + 1)
  928. let _result = [];
  929. let identity = "老师"; //0:老师 1:学生
  930. let startTime = "";
  931. let endTime = "";
  932. let sumTime = 0;
  933. let upTime = '00:00:00';
  934. _dataList.forEach((item, index) => {
  935. if (index == 0) {
  936. //第一个
  937. identity = item.role;
  938. startTime = item.startTime;
  939. endTime = item.endTime;
  940. sumTime = (this.convertToSeconds(item.endTime) - this.convertToSeconds(upTime));
  941. upTime = item.endTime
  942. // console.log(item.endTime,item.startTime,(this.convertToSeconds(item.endTime) - this.convertToSeconds(item.startTime)))
  943. return;
  944. }
  945. if (item.role == identity) {
  946. //没更换角色
  947. sumTime += (this.convertToSeconds(item.endTime) - this.convertToSeconds(upTime));
  948. endTime = item.endTime;
  949. upTime = item.endTime
  950. // console.log(item.endTime,item.startTime,(this.convertToSeconds(item.endTime) - this.convertToSeconds(item.startTime)))
  951. } else {
  952. //更换角色了
  953. _result.push({
  954. startTime: startTime,
  955. endTime: endTime,
  956. identity: identity,
  957. sumTime: sumTime
  958. });
  959. identity = item.role;
  960. startTime = item.startTime;
  961. endTime = item.endTime;
  962. sumTime = (this.convertToSeconds(item.endTime) - this.convertToSeconds(upTime));
  963. upTime = item.endTime
  964. // console.log(item.endTime,item.startTime,(this.convertToSeconds(item.endTime) - this.convertToSeconds(item.startTime)))
  965. }
  966. if (index == _dataList.length - 1) {
  967. // console.log("👉???",this.convertToSeconds(item.endTime))
  968. }
  969. });
  970. let breakpoint = [];
  971. breakpoint = _tableData.map(i => this.convertToSeconds(i[0]))
  972. _result = _result.filter(
  973. i =>
  974. i.identity == "老师" ||
  975. i.identity == "学生"
  976. );
  977. // let
  978. let _data = {
  979. data: [],
  980. breakpoint: []
  981. };
  982. _data.data = _result.map(i => ({ role: i.identity, type: i.identity == "老师" ? 0 : 1, value: i.sumTime }));
  983. _data.breakpoint = breakpoint;
  984. let _msg = assistant.tips;
  985. let message = await this.getAiContentMixin({ _msg: _msg, fileId: fileId })
  986. if (message.data == 1) {
  987. return resolve({ eCharts: _data, message: "" })
  988. } else {
  989. return resolve({ eCharts: _data, message: message.data })
  990. }
  991. // spectrogramData _data
  992. });
  993. } catch (e) {
  994. return resolve({ data: 1, err: e })
  995. }
  996. })
  997. },
  998. getAiContentMixin(obj) {
  999. return new Promise((resolve) => {
  1000. let { _msg, fileId } = obj
  1001. let parm = {
  1002. id: "f8e1ebb2-2e0d-11ef-8bf4-12e77c4cb76b",
  1003. message: _msg,
  1004. session_name: uuidv4(),
  1005. userId: this.userId,
  1006. file_ids: fileId ? [fileId] : [],
  1007. model: "gpt-4o-2024-11-20",
  1008. sound_url: "",
  1009. temperature: 0.2,
  1010. top_p: 1,
  1011. max_completion_tokens: 4096,
  1012. stream: false,
  1013. uid: uuidv4()
  1014. };
  1015. this.ajax
  1016. .post("https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat", parm)
  1017. .then(res => {
  1018. let _data = res.data;
  1019. resolve({ data: _data.message })
  1020. })
  1021. .catch(err => {
  1022. resolve({ data: 1, err: err })
  1023. });
  1024. })
  1025. },
  1026. convertToSeconds(time) {
  1027. let parts = time.split(":");
  1028. let seconds = +parts[0] * 3600 + +parts[1] * 60 + +parts[2];
  1029. return seconds;
  1030. },
  1031. //创建课堂
  1032. createClassMixin(data) {
  1033. return new Promise((resolve) => {
  1034. let _analysisList = data.analysisList;
  1035. let createJson = _analysisList.map(i => {
  1036. return {
  1037. jsonData: i.jsonData,
  1038. type: i.Type,
  1039. index: i.tIndex
  1040. }
  1041. })
  1042. createJson = createJson.filter(i => !i.isOtherData && converter(i.jsonData.name) != converter("词频词汇分析"));
  1043. let params = {
  1044. tid: uuidv4(),
  1045. userid: this.userId,
  1046. template: createJson
  1047. }
  1048. this.ajax
  1049. .post(
  1050. "https://gpt4.cocorobo.cn/insert_classroom_observation_template",
  1051. params
  1052. )
  1053. .then(res => {
  1054. let _data = res.data.FunctionResponse;
  1055. if (converter(_data.message) == converter("创建成功")) {
  1056. this.ajax
  1057. .post("https://gpt4.cocorobo.cn/insert_classroom_observation", {
  1058. tid: params.tid,
  1059. type: 10,
  1060. index: 0,
  1061. json_data: JSON.stringify({ file_ids: data.file_ids }),
  1062. userid: this.userId
  1063. })
  1064. .then(res2 => {
  1065. let _data2 = res2.data.FunctionResponse;
  1066. if (converter(_data2.message) == converter("创建成功")) {
  1067. let newOption = { id: uuidv4(), label: data.baseMessage.courseName, value: params.tid }
  1068. let params2 = {
  1069. tid: params.tid,
  1070. type: 0,
  1071. }
  1072. this.ajax
  1073. .post(
  1074. "https://gpt4.cocorobo.cn/get_classroom_observation_new",
  1075. params2
  1076. ).then(res3 => {
  1077. let _data = res3.data.FunctionResponse.result.length
  1078. ? JSON.parse(res3.data.FunctionResponse.result)
  1079. : [];
  1080. //替换基础信息
  1081. let _bmData = _data.find(i => i.tIndex == 0);
  1082. let _imageList = _data.find(i => i.tIndex == 1);
  1083. _imageList.jsonData = data.baseMessage.imageList
  1084. if (_bmData) {
  1085. _bmData.jsonData = data.baseMessage;
  1086. delete _bmData.jsonData.imageList;
  1087. if (data.tagList) {
  1088. _bmData.jsonData.dialogTagList = data.tagList;
  1089. } else {
  1090. _bmData.jsonData.dialogTagList = [
  1091. { value: 0, name: "通用课堂分析", loading: false },
  1092. { value: 1, name: "学科课堂分析", loading: false },
  1093. { value: 2, name: "扩展分析", loading: false }
  1094. ]
  1095. }
  1096. }
  1097. let arr = [{ id: _bmData.id, jsonData: JSON.stringify(_bmData.jsonData) }, { id: _imageList.id, jsonData: JSON.stringify(_imageList.jsonData) }]
  1098. let promises = [];
  1099. arr.forEach(i => {
  1100. promises.push(new Promise((resolve) => {
  1101. this.ajax
  1102. .post("https://gpt4.cocorobo.cn/update_classroom_observation", {
  1103. id: i.id,
  1104. json_data: i.jsonData
  1105. })
  1106. .then(res => {
  1107. resolve();
  1108. })
  1109. .catch(e => {
  1110. console.log("保存失败", e);
  1111. resolve()
  1112. });
  1113. }))
  1114. })
  1115. Promise.all(promises).then(res => {
  1116. resolve({ data: newOption, tid: params.tid })
  1117. })
  1118. }).catch(err => {
  1119. console.log("修改基础信息失败", err)
  1120. resolve({ data: 3, err: err })
  1121. })
  1122. }
  1123. }).catch(err => {
  1124. resolve({ data: 2, err: err })
  1125. console.log("存储fileId失败")
  1126. console.log(err)
  1127. })
  1128. }
  1129. }).catch(err => {
  1130. resolve({ data: 1, err: err })
  1131. console.log("创建课堂失败")
  1132. console.log(err)
  1133. })
  1134. })
  1135. },
  1136. //文本转录
  1137. wavAudioToTextAndObjMixin(file) {
  1138. return new Promise(async (resolve) => {
  1139. let iframeRef = this.$refs["iframeRef"];
  1140. iframeRef.contentWindow.window.document.getElementById(
  1141. "languageOptions"
  1142. ).selectedIndex = 2;//默认普通话
  1143. let transcriptionContent = "";
  1144. let tableContent = "";
  1145. let tableList = [];
  1146. let _startTime = 0;
  1147. let _endTime = 0;
  1148. // 转录中
  1149. iframeRef.contentWindow.onRecognizedResult = (e) => {
  1150. let privText = e.privText;
  1151. let privSpeakerId = e.privSpeakerId;
  1152. let privDuration = e.privDuration;
  1153. let privOffset = e.privOffset;
  1154. if (!privText || !privSpeakerId || privSpeakerId == "Unknown") {//不记录
  1155. return;
  1156. }
  1157. _endTime = (privOffset + privDuration) / 10000000;
  1158. tableList.push({
  1159. value: privText,
  1160. startTime: this.updateRecordedTimeMixin({ duration: _startTime }),
  1161. endTime: this.updateRecordedTimeMixin({ duration: _endTime }),
  1162. time: this.updateRecordedTimeMixin({ duration: _endTime - _startTime }),
  1163. role: privSpeakerId,
  1164. code: ""
  1165. });
  1166. _startTime = _endTime;
  1167. transcriptionContent += privText;
  1168. };
  1169. //转录结束
  1170. iframeRef.contentWindow.onSessionStopped = async (e) => {
  1171. tableContent = `<table
  1172. border="0"
  1173. width="100%"
  1174. cellpadding="0"
  1175. cellspacing="0"
  1176. style="text-align: center"
  1177. >
  1178. <tbody>
  1179. <tr>
  1180. <th>序号</th>
  1181. <th>开始时间</th>
  1182. <th>结束时间</th>
  1183. <th>发言内容</th>
  1184. <th>时长</th>
  1185. <th>说话人身份</th>
  1186. <th>行为编码</th>
  1187. </tr>`;
  1188. tableList.forEach((item, index) => {
  1189. tableContent += `<tr>
  1190. <td>${index + 1}</td>
  1191. <td>${item.startTime}</td>
  1192. <td>${item.endTime}</td>
  1193. <td>${item.value}</td>
  1194. <td>${item.time}</td>
  1195. <td>${item.role}</td>
  1196. <td>${item.code}</td>
  1197. </tr>`
  1198. })
  1199. // tableContent += `<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr></tbody></table>`
  1200. var blob = new Blob([tableContent], { type: "text/plain;charset=utf-8" });
  1201. blob.lastModifiedDate = new Date();
  1202. blob.name = `classroomObservation.txt`;
  1203. this.uploadFileMixin(blob).then(upload => {
  1204. resolve({ transcriptionContent: transcriptionContent, editorBarData: { type: "0", url: upload.Location, content: tableContent, tableList: tableList } })
  1205. })
  1206. };
  1207. //开始转录
  1208. iframeRef.contentWindow.ConversationTranscriber({
  1209. files: [file]
  1210. });
  1211. })
  1212. },
  1213. updateRecordedTimeMixin({ duration }) {
  1214. // 更新currentTime,将秒数转换为时分秒格式
  1215. let hours = Math.floor(duration / 3600);
  1216. let minutes = Math.floor((duration % 3600) / 60);
  1217. let seconds = Math.floor(duration % 60);
  1218. // this.recordedForm.time = `${hours.toString().padStart(2, "0")}:${minutes
  1219. // .toString()
  1220. // .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
  1221. return `${hours
  1222. .toString()
  1223. .padStart(2, "0")}:${minutes
  1224. .toString()
  1225. .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
  1226. },
  1227. //自动编码
  1228. automaticCodingMixin(data) {
  1229. return new Promise(async (resolve) => {
  1230. let { tableList } = data;
  1231. let roleObj = {};
  1232. let tableContent = `<table
  1233. border="0"
  1234. width="100%"
  1235. cellpadding="0"
  1236. cellspacing="0"
  1237. style="text-align: center"
  1238. >
  1239. <tbody>
  1240. <tr>
  1241. <th>序号</th>
  1242. <th>开始时间</th>
  1243. <th>结束时间</th>
  1244. <th>发言内容</th>
  1245. <th>时长</th>
  1246. <th>说话人身份</th>
  1247. <th>行为编码</th>
  1248. </tr>`;
  1249. console.log("说话人身份编码开始")
  1250. // 说话人身份编码
  1251. while (tableList.some(i => i.role.indexOf("Guest") != -1 && i.role !== '')) {
  1252. let _ajaxList = tableList.filter(i => i.role.indexOf("Guest") != -1 && i.role !== '').slice(0, 10);
  1253. console.log(`说话人身份编码:`, _ajaxList)
  1254. const params = {
  1255. inputs: {
  1256. options: "老师,学生",
  1257. rows: JSON.stringify(
  1258. _ajaxList.map(i => {
  1259. return { content: i.value, role: i.role };
  1260. })
  1261. )
  1262. },
  1263. response_mode: "blocking",
  1264. user: this.userId
  1265. };
  1266. let roleRes = await this.getWavRoleList(params);
  1267. if (roleRes.data === 1) continue;;
  1268. let _roleResult = roleRes.data.data.outputs.result;
  1269. let _numRole = [];
  1270. _roleResult.forEach((txt, index) => {
  1271. let _oldRole = _ajaxList[index].role;
  1272. if (_numRole.map(i => i.role).includes(_oldRole)) {
  1273. let _findIndex = _numRole.findIndex(
  1274. i => i.role == _oldRole
  1275. );
  1276. if (txt == "学生") {
  1277. _numRole[_findIndex].s += 1;
  1278. } else if (txt == "老师") {
  1279. _numRole[_findIndex].t += 1;
  1280. }
  1281. } else {
  1282. if (txt == "学生") {
  1283. _numRole.push({ role: _oldRole, t: 0, s: 1 });
  1284. } else if (txt == "老师") {
  1285. _numRole.push({ role: _oldRole, t: 1, s: 0 });
  1286. }
  1287. }
  1288. });
  1289. //根据数量判断是老师还是学生
  1290. _numRole.forEach(i => {
  1291. if (i.t > i.s) {
  1292. roleObj[i.role] = "老师";
  1293. } else if (i.t < i.s) {
  1294. roleObj[i.role] = "学生";
  1295. }
  1296. });
  1297. //已经有的role
  1298. let roleKeys = Object.keys(roleObj);
  1299. tableList.forEach(i => {
  1300. if (roleKeys.includes(i.role)) {
  1301. i.role = roleObj[i.role];
  1302. }
  1303. });
  1304. }
  1305. console.log("说话人身份编码完成")
  1306. console.log("说话人行为编码开始")
  1307. //说话人行为编码
  1308. while (tableList.some(i => i.code == "" && i.role.indexOf("Guest") == -1 && i.value != "")) {
  1309. let _ajaxList = tableList.filter(i => i.code == "" && i.role.indexOf("Guest") == -1 && i.value != "").slice(0, 10);
  1310. console.log(`说话人行为编码:`, _ajaxList)
  1311. let params = {
  1312. inputs: {
  1313. rows: JSON.stringify(
  1314. _ajaxList.map(i => ({
  1315. content: i.value,
  1316. role: i.role
  1317. }))
  1318. ),
  1319. options: "老师讲课,老师提问或点名,老师板书或操作,老师评价或反馈,老师其他,学生发言,学生小组活动,学生自主学习,学生汇报分享,学生其他",
  1320. attention: "- 先根据说话人角色判断,再在对应角色的选项中选择选项\n- 如果没有合适的选项,默认使用`老师其他`或者`学生其他`"
  1321. },
  1322. response_mode: "blocking",
  1323. user: this.userId
  1324. };
  1325. let _codeRes = await this.getBehavioralCoding(params)
  1326. if (_codeRes.data === 1) continue;
  1327. const _codeResult = _codeRes.data.data.outputs.result;
  1328. _ajaxList.forEach((item, index) => {
  1329. let _findIndex = tableList.findIndex(i => i.index === item.index);
  1330. if (_findIndex != -1) {
  1331. tableList[_findIndex].code = _codeResult[index];
  1332. }
  1333. })
  1334. }
  1335. console.log("说话人行为编码完成")
  1336. tableList.forEach((item, index) => {
  1337. tableContent += `<tr>
  1338. <td>${index + 1}</td>
  1339. <td>${item.startTime}</td>
  1340. <td>${item.endTime}</td>
  1341. <td>${item.value}</td>
  1342. <td>${item.time}</td>
  1343. <td>${item.role}</td>
  1344. <td>${item.code}</td>
  1345. </tr>`
  1346. })
  1347. tableContent += `<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr></tbody></table>`
  1348. var blob = new Blob([tableContent], { type: "text/plain;charset=utf-8" });
  1349. blob.lastModifiedDate = new Date();
  1350. blob.name = `classroomObservation.txt`;
  1351. this.uploadFileMixin(blob).then(upload => {
  1352. resolve({ editorBarData: { type: "0", url: upload.Location, content: tableContent, tableList: tableList } })
  1353. })
  1354. })
  1355. },
  1356. //文本的自动编码
  1357. automaticCodingForTextMixin(data) {
  1358. return new Promise(async (resolve) => {
  1359. let { tableList } = data;
  1360. let roleObj = {};
  1361. let tableContent = `<table
  1362. border="0"
  1363. width="100%"
  1364. cellpadding="0"
  1365. cellspacing="0"
  1366. style="text-align: center"
  1367. >
  1368. <tbody>
  1369. <tr>
  1370. <th>序号</th>
  1371. <th>开始时间</th>
  1372. <th>结束时间</th>
  1373. <th>发言内容</th>
  1374. <th>时长</th>
  1375. <th>说话人身份</th>
  1376. <th>行为编码</th>
  1377. </tr>`;
  1378. console.log("说话人身份编码开始")
  1379. // 说话人身份编码
  1380. while (tableList.some(i => i.role == '')) {
  1381. let _ajaxList = tableList.filter(i => i.role == '').slice(0, 10);
  1382. console.log(`说话人身份编码:`, _ajaxList)
  1383. const params = {
  1384. inputs: {
  1385. options: "老师,学生",
  1386. rows: JSON.stringify(
  1387. _ajaxList.map(i => {
  1388. return { content: i.value, role: i.role };
  1389. })
  1390. )
  1391. },
  1392. response_mode: "blocking",
  1393. user: this.userId
  1394. };
  1395. let roleRes = await this.getWavRoleList(params);
  1396. if (roleRes.data === 1) continue;;
  1397. let _roleResult = roleRes.data.data.outputs.result;
  1398. _ajaxList.forEach((item, index) => {
  1399. let _findIndex = tableList.findIndex(i => i.index === item.index);
  1400. if (_findIndex != -1) {
  1401. tableList[_findIndex].role = _roleResult[index];
  1402. }
  1403. })
  1404. // let _numRole = [];
  1405. // _roleResult.forEach((txt, index) => {
  1406. // let _oldRole = _ajaxList[index].role;
  1407. // if (_numRole.map(i => i.role).includes(_oldRole)) {
  1408. // let _findIndex = _numRole.findIndex(
  1409. // i => i.role == _oldRole
  1410. // );
  1411. // if (txt == "学生") {
  1412. // _numRole[_findIndex].s += 1;
  1413. // } else if (txt == "老师") {
  1414. // _numRole[_findIndex].t += 1;
  1415. // }
  1416. // } else {
  1417. // if (txt == "学生") {
  1418. // _numRole.push({ role: _oldRole, t: 0, s: 1 });
  1419. // } else if (txt == "老师") {
  1420. // _numRole.push({ role: _oldRole, t: 1, s: 0 });
  1421. // }
  1422. // }
  1423. // });
  1424. //根据数量判断是老师还是学生
  1425. // _numRole.forEach(i => {
  1426. // if (i.t > i.s) {
  1427. // roleObj[i.role] = "老师";
  1428. // } else if (i.t < i.s) {
  1429. // roleObj[i.role] = "学生";
  1430. // }
  1431. // });
  1432. //已经有的role
  1433. // let roleKeys = Object.keys(roleObj);
  1434. // tableList.forEach(i => {
  1435. // if (roleKeys.includes(i.role)) {
  1436. // i.role = roleObj[i.role];
  1437. // }
  1438. // });
  1439. }
  1440. console.log("说话人身份编码完成")
  1441. console.log("allRole",tableList.map(i=>i.role))
  1442. console.log("说话人行为编码开始")
  1443. //说话人行为编码
  1444. while (tableList.some(i => i.code == "" && i.role.indexOf("Guest") == -1 && i.value != "")) {
  1445. let _ajaxList = tableList.filter(i => i.code == "" && i.role.indexOf("Guest") == -1 && i.value != "").slice(0, 10);
  1446. console.log(`说话人行为编码:`, _ajaxList)
  1447. let params = {
  1448. inputs: {
  1449. rows: JSON.stringify(
  1450. _ajaxList.map(i => ({
  1451. content: i.value,
  1452. role: i.role
  1453. }))
  1454. ),
  1455. options: "老师讲课,老师提问或点名,老师板书或操作,老师评价或反馈,老师其他,学生发言,学生小组活动,学生自主学习,学生汇报分享,学生其他",
  1456. attention: "- 先根据说话人角色判断,再在对应角色的选项中选择选项\n- 如果没有合适的选项,默认使用`老师其他`或者`学生其他`"
  1457. },
  1458. response_mode: "blocking",
  1459. user: this.userId
  1460. };
  1461. let _codeRes = await this.getBehavioralCoding(params)
  1462. if (_codeRes.data === 1) continue;
  1463. const _codeResult = _codeRes.data.data.outputs.result;
  1464. _ajaxList.forEach((item, index) => {
  1465. let _findIndex = tableList.findIndex(i => i.index === item.index);
  1466. if (_findIndex != -1) {
  1467. tableList[_findIndex].code = _codeResult[index];
  1468. }
  1469. })
  1470. }
  1471. console.log("说话人行为编码完成")
  1472. tableList.forEach((item, index) => {
  1473. tableContent += `<tr>
  1474. <td>${index + 1}</td>
  1475. <td>${item.startTime}</td>
  1476. <td>${item.endTime}</td>
  1477. <td>${item.value}</td>
  1478. <td>${item.time}</td>
  1479. <td>${item.role}</td>
  1480. <td>${item.code}</td>
  1481. </tr>`
  1482. })
  1483. // tableContent += `<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr></tbody></table>`
  1484. var blob = new Blob([tableContent], { type: "text/plain;charset=utf-8" });
  1485. blob.lastModifiedDate = new Date();
  1486. blob.name = `classroomObservation.txt`;
  1487. this.uploadFileMixin(blob).then(upload => {
  1488. resolve({ editorBarData: { type: "0", url: upload.Location, content: tableContent, tableList: tableList } })
  1489. })
  1490. })
  1491. },
  1492. getWavRoleList(params) {
  1493. return new Promise((resolve, reject) => {
  1494. this.ajax
  1495. .post("https://dify.cocorobo.cn/v1/workflows/run?key=role", params)
  1496. .then(res => {
  1497. resolve(res);
  1498. })
  1499. .catch(err => {
  1500. console.log("获取说话人身份失败", err)
  1501. resolve({ data: 1 })
  1502. // reject(err);
  1503. });
  1504. });
  1505. },
  1506. getBehavioralCoding(params) {
  1507. return new Promise((resolve, reject) => {
  1508. this.ajax
  1509. .post("https://dify.cocorobo.cn/v1/workflows/run?key=code", params)
  1510. .then(res => {
  1511. resolve(res);
  1512. })
  1513. .catch(err => {
  1514. console.log("获取说话人编码失败", err)
  1515. resolve({ data: 1 })
  1516. });
  1517. });
  1518. },
  1519. //m4a转wav
  1520. audioToWavMixin(fileObj) {
  1521. return new Promise((resolve) => {
  1522. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  1523. const reader = new FileReader();
  1524. reader.onload = (e)=>{
  1525. const arrayBuffer = e.target.result;
  1526. // 解码音频数据
  1527. audioContext.decodeAudioData(arrayBuffer)
  1528. .then(audioBuffer => {
  1529. let wavBlob = this.bufferToWav(audioBuffer);
  1530. let _wavFile = new File([wavBlob], "audio.wav", {
  1531. type: "audio/wav"
  1532. })
  1533. // 在控制台输出WAV文件对象
  1534. resolve({ data: _wavFile })
  1535. console.log('转换后的WAV文件对象:', _wavFile);
  1536. })
  1537. .catch(err => {
  1538. resolve({ data: 1, err: err })
  1539. });
  1540. };
  1541. reader.onerror = (e) => {
  1542. resolve({ data: 1, err: e })
  1543. };
  1544. reader.readAsArrayBuffer(fileObj);
  1545. })
  1546. },
  1547. getQrCodeImageSrc(url) {
  1548. return new Promise((resolve, reject) => {
  1549. let qrcode = new QRCode(document.createElement("div"), {
  1550. text: url, // 需要转换为二维码的内容
  1551. width: 150,
  1552. height: 150,
  1553. colorDark: "#000000",
  1554. colorLight: "#ffffff",
  1555. correctLevel: QRCode.CorrectLevel.H
  1556. });
  1557. let img = qrcode._el.getElementsByTagName("img")[0];
  1558. img.onload = () => {
  1559. resolve(img.src);
  1560. };
  1561. });
  1562. },
  1563. getEChartsImageSrc(option) {
  1564. return new Promise(resolve => {
  1565. try {
  1566. let hiddenDiv = document.createElement("div");
  1567. hiddenDiv.style.width = "400px";
  1568. hiddenDiv.style.height = "400px";
  1569. hiddenDiv.style.position = "absolute";
  1570. hiddenDiv.style.left = "-9999px"; // 隐藏div
  1571. document.body.appendChild(hiddenDiv);
  1572. // 初始化图表
  1573. let myChart = echarts.init(hiddenDiv);
  1574. // 设置图标配置
  1575. myChart.setOption(option);
  1576. // console.log("词云图???",option)
  1577. myChart.on("finished", () => {
  1578. // 获取图表的图片
  1579. let base64Image = myChart.getDataURL({
  1580. type: "png", // 图片格式
  1581. pixelRatio: 0.9, // 图像清晰度
  1582. backgroundColor: "#fff" // 背景颜色
  1583. });
  1584. resolve(base64Image);
  1585. // 清除隐藏的div和图表实例
  1586. document.body.removeChild(hiddenDiv);
  1587. myChart.dispose();
  1588. });
  1589. } catch (error) {
  1590. console.log(error,"error")
  1591. resolve("#")
  1592. }
  1593. });
  1594. },
  1595. getImageSrcToBase64(src) {
  1596. return new Promise((resolve, reject) => {
  1597. const image = new Image();
  1598. image.src = src;
  1599. image.onload = () => {
  1600. const canvas = document.createElement("canvas");
  1601. canvas.width = image.naturalWidth;
  1602. canvas.height = image.naturalHeight;
  1603. canvas.style.width = `${canvas.width / window.devicePixelRatio}px`;
  1604. canvas.style.height = `${canvas.height / window.devicePixelRatio}px`;
  1605. const context = canvas.getContext("2d");
  1606. context.drawImage(image, 0, 0);
  1607. const base64 = canvas.toDataURL("image/png");
  1608. resolve(base64);
  1609. };
  1610. });
  1611. },
  1612. getEChartsSpectrogramImage(data) {
  1613. return new Promise(resolve => {
  1614. try {
  1615. let canvas = document.createElement("canvas");
  1616. let ctx = canvas.getContext("2d");
  1617. canvas.width = 600 * window.devicePixelRatio;
  1618. canvas.height = 200 * window.devicePixelRatio;
  1619. // 缩放绘图上下文
  1620. ctx.scale(1, 1);
  1621. let canvasWidth = canvas.width;
  1622. let canvasWidth2 = canvasWidth - 20;
  1623. let canvasHeight = canvas.height;
  1624. ctx.imageSmoothingEnabled = false;
  1625. ctx.lineWidth = 1;
  1626. // 设置颜色和文字
  1627. const teacherColor = "#5470C6"; // 老师的颜色
  1628. const studentColor = "#91CC75"; // 学生的颜色
  1629. const fontSize = 14; //字体大小
  1630. ctx.fillStyle = teacherColor;
  1631. this.drawRoundedRect(ctx, 0, canvasHeight - 20, 20, 15, 4);
  1632. // ctx.fillRect(0, canvasHeight - 20, 25, 20);
  1633. ctx.fillStyle = "black";
  1634. ctx.font = `${fontSize}px serif`;
  1635. ctx.fillText("老师", 28, canvasHeight - 7);
  1636. ctx.fillStyle = studentColor;
  1637. // ctx.fillRect(100, canvasHeight - 20, 25, 20);
  1638. this.drawRoundedRect(ctx, 100, canvasHeight - 20, 20, 15, 4);
  1639. ctx.fillStyle = "black";
  1640. ctx.font = `${fontSize}px serif`;
  1641. ctx.fillText("学生", 128, canvasHeight - 7);
  1642. let sum = data.data.reduce((pre, cur) => (pre += cur.value), 0);
  1643. // 当前x位置的起始点
  1644. let currentX = 10;
  1645. // 计算并绘制每个区域
  1646. data.data.forEach(i => {
  1647. const segmentWidth = parseFloat(
  1648. (i.value / (sum / canvasWidth2)).toFixed(2)
  1649. );
  1650. ctx.fillStyle = i.type == 0 ? teacherColor : studentColor;
  1651. ctx.fillRect(currentX, 20, segmentWidth, canvasHeight - 100);
  1652. // 更新x位置
  1653. currentX += segmentWidth;
  1654. });
  1655. // 绘制红色垂直线(指定位置)
  1656. // ctx.strokeStyle = "red";
  1657. // ctx.lineWidth = 2;
  1658. // data.breakpoint.forEach(i => {
  1659. // const breakpointPo = parseFloat(
  1660. // (i / (sum / canvasWidth2)).toFixed(2)
  1661. // );
  1662. // ctx.beginPath();
  1663. // ctx.moveTo(breakpointPo, 10);
  1664. // ctx.lineTo(breakpointPo, canvasHeight - 70);
  1665. // ctx.stroke();
  1666. // });
  1667. let interval = parseFloat((300 / (sum / canvasWidth2)).toFixed(2));
  1668. //绘制竖线
  1669. let _lastI = 0;
  1670. //绘制竖线
  1671. for (let i = 0; i < canvasWidth2; i += interval) {
  1672. ctx.beginPath();
  1673. ctx.strokeStyle = "#BFBFBF";
  1674. ctx.moveTo(i == 0 ? 10 : i, canvasHeight - 70);
  1675. ctx.lineTo(i == 0 ? 10 : i, canvasHeight - 55);
  1676. ctx.stroke();
  1677. ctx.fillStyle = "#868686";
  1678. let timeLabel = (((i / canvasWidth2) * sum) / 60).toFixed(0); // 时间标识计算
  1679. ctx.font = `${fontSize}px serif`;
  1680. if (i == 0) {
  1681. ctx.fillText(`${timeLabel}min`, i + 10, canvasHeight - 40);
  1682. } else if (i + interval >= canvasWidth2) {
  1683. ctx.fillText(`${timeLabel}min`, i - 20, canvasHeight - 40);
  1684. } else {
  1685. ctx.fillText(`${timeLabel}min`, i - 15, canvasHeight - 40);
  1686. }
  1687. _lastI = i;
  1688. }
  1689. if (canvasWidth2 - _lastI >60) {
  1690. ctx.beginPath();
  1691. ctx.strokeStyle = "#BFBFBF";
  1692. ctx.moveTo(canvasWidth2 + 10, canvasHeight - 70);
  1693. ctx.lineTo(canvasWidth2 + 10, canvasHeight - 55);
  1694. ctx.stroke();
  1695. ctx.fillStyle = "#868686";
  1696. let timeLabel = (sum / 60).toFixed(0); // 时间标识计算
  1697. ctx.font = `${fontSize}px serif`;
  1698. ctx.fillText(`${timeLabel}min`, canvasWidth2 - 20, canvasHeight - 40);
  1699. }
  1700. ctx.beginPath();
  1701. ctx.strokeStyle = "#BFBFBF";
  1702. ctx.moveTo(10, canvasHeight - 55);
  1703. ctx.lineTo(canvasWidth2 + 10, canvasHeight - 55);
  1704. ctx.stroke();
  1705. // 将 canvas 转换为 base64 格式的图片地址
  1706. const base64Image = canvas.toDataURL("image/png");
  1707. resolve(base64Image);
  1708. } catch (e) {
  1709. console.log(e);
  1710. resolve("#");
  1711. }
  1712. });
  1713. },
  1714. getEChartsechartsRTCHImage(data) {
  1715. return new Promise(resolve => {
  1716. try {
  1717. let canvas = document.createElement("canvas");
  1718. let ctx = canvas.getContext("2d");
  1719. canvas.width = 300 * window.devicePixelRatio;
  1720. canvas.height = 350 * window.devicePixelRatio;
  1721. // 缩放绘图上下文
  1722. ctx.scale(1, 1);
  1723. let canvasWidth = canvas.width;
  1724. let canvasHeight = canvas.height;
  1725. let fontColor = "#1a7ad3";
  1726. ctx.imageSmoothingEnabled = false;
  1727. ctx.lineWidth = 1;
  1728. const img = new Image();
  1729. img.src = require("../../../../assets/icon/classroomObservation/rt-ch_echarts2.svg"); //ch_echarts2
  1730. img.onload = () => {
  1731. ctx.drawImage(img, 0, 0, canvasWidth, canvasWidth);
  1732. ctx.beginPath();
  1733. let _showWidth = canvasWidth-((canvasWidth/8.8)*2)
  1734. ctx.fillStyle = fontColor;
  1735. ctx.arc((canvasWidth/8.8)+(_showWidth*parseFloat(data.RT)),(canvasWidth/8.8)+_showWidth-(_showWidth*parseFloat(data.CH)),4,0,2*Math.PI);
  1736. ctx.lineWidth = 0.5; // 设置边框大小
  1737. // ctx.arc((canvasWidth*parseFloat(this.data.RT))+(canvasWidth/8.8),(canvasWidth-(canvasWidth*parseFloat(this.data.CH))+(canvasWidth/8.8)), 4, 0, 2 * Math.PI);
  1738. ctx.fill();
  1739. ctx.stroke();
  1740. ctx.fillStyle = fontColor;
  1741. ctx.font = "italic bold 24px Arial";
  1742. const text = `RT=${data.RT} CH=${data.CH}`;
  1743. const textWidth = ctx.measureText(text).width;
  1744. ctx.fillText(text, (canvasWidth - textWidth) / 2, canvasHeight - canvasWidth / 12);
  1745. const base64Image = canvas.toDataURL("image/png");
  1746. resolve(base64Image);
  1747. };
  1748. } catch (e) {
  1749. console.log(e);
  1750. resolve("#");
  1751. }
  1752. });
  1753. },
  1754. drawRoundedRect(ctx, x, y, width, height, radius) {
  1755. // 限制 radius 的最大值,防止它超过矩形的宽度或高度的一半
  1756. const actualRadius = Math.min(radius, width / 2, height / 2);
  1757. ctx.beginPath();
  1758. ctx.moveTo(x + actualRadius, y); // 起点,矩形顶部的左侧
  1759. // 右上角的弧线
  1760. ctx.arcTo(x + width, y, x + width, y + height, actualRadius);
  1761. // 右下角的弧线
  1762. ctx.arcTo(x + width, y + height, x, y + height, actualRadius);
  1763. // 左下角的弧线
  1764. ctx.arcTo(x, y + height, x, y, actualRadius);
  1765. // 左上角的弧线
  1766. ctx.arcTo(x, y, x + width, y, actualRadius);
  1767. ctx.closePath();
  1768. ctx.fill(); // 填充颜色
  1769. },
  1770. getDocFnPromise(task){
  1771. console.log("处理👉",task)
  1772. return new Promise(async (resolve)=>{
  1773. try {
  1774. let bmData = task.jsonData.baseMessage;
  1775. const md = new markdownIt();
  1776. let dataList = task.jsonData.analysisList;
  1777. let tagList = task.jsonData.tagList?task.jsonData.tagList:[
  1778. { value: 0, name: "通用课堂分析", loading: false },
  1779. { value: 1, name: "学科课堂分析", loading: false },
  1780. { value: 2, name: "扩展分析", loading: false }
  1781. ];
  1782. let showBrief = true;
  1783. // console.log(tagList,"tagList")
  1784. tagList.forEach(i => (i.dataList = []));
  1785. let url = `https://beta.cloud.cocorobo.cn/aigpt/#/classroom_observation_board?tid=${task.jsonData.createId}`;
  1786. const qRCodeSrc = await this.getQrCodeImageSrc(url);
  1787. dataList.sort((a, b) => a.tIndex - b.tIndex);
  1788. dataList.forEach(i1 => {
  1789. tagList.forEach(i2 => {
  1790. if (i2.value == i1.Type) {
  1791. i2.dataList.push(i1);
  1792. }
  1793. });
  1794. });
  1795. let directoryHtml = `<div style="margin-bottom:1in"><div style="text-align:center;font-size:20pt;margin-bottom:0.5in">目录</div>`;
  1796. let analysisHtml = ``;
  1797. // console.log("开始处理文件")
  1798. for (let c = 0; c < tagList.length; c++) {
  1799. // console.log(tagList[c],"tagList[c]")
  1800. let i = tagList[c];
  1801. let dire = `<div>`;
  1802. let tagHtml = `<div style="margin-bottom:0.5in">`;
  1803. if (i.value == 0) {
  1804. i.dataList = i.dataList.filter(i2 => i2.tIndex != 2);
  1805. }
  1806. i.dataList.sort((a, b) => a.tIndex - b.tIndex);
  1807. tagHtml += `<h1 style="font-size:16pt;margin-bottom:-1in">${
  1808. this.tag[i.value]
  1809. }、${i.name}</h1>`;
  1810. dire += `<p style="font-size:14pt;margin-bottom:-0.8in">${
  1811. this.tag[i.value]
  1812. }、${i.name}</p>`;
  1813. for (let d = 0; d < i.dataList.length; d++) {
  1814. let i2 = i.dataList[d];
  1815. // console.log(i.dataList[d],"i.dataList[d]")
  1816. let i2Index = d;
  1817. tagHtml += `<h2 style="font-size:14pt;margin-bottom:-1in">${i2Index +
  1818. 1}、${
  1819. i2.jsonData.anotherName
  1820. ? i2.jsonData.anotherName
  1821. : i2.jsonData.name
  1822. }</h2>`;
  1823. dire += `<p style="font-size:11pt;margin-bottom:-0.8in;margin-left:0.1in">${i2Index +
  1824. 1}、${
  1825. i2.jsonData.anotherName
  1826. ? i2.jsonData.anotherName
  1827. : i2.jsonData.name
  1828. }</p>`;
  1829. if (showBrief && i2.jsonData.result) {
  1830. tagHtml += `<p style="font-size:10.5pt;font-style:italic;margin-bottom:-0.7in;color:#6b798e">${i2.jsonData.result}</p>`;
  1831. }
  1832. if (i2.jsonData.eChartData) {
  1833. tagHtml += `<div style="width:100vw;padding:70%;box-sizing: border-box;text-align:center"><img style="margin:auto" src="${await this.getEChartsImageSrc(
  1834. i2.jsonData.eChartData
  1835. )}"/></div>`;
  1836. }
  1837. if (i2.jsonData.spectrogramData) {
  1838. tagHtml += `<div style="width:100vw;padding:70%;box-sizing: border-box;text-align:center"><img style="margin:auto" src="${await this.getEChartsSpectrogramImage(
  1839. i2.jsonData.spectrogramData
  1840. )}"/></div>`;
  1841. // console.log()
  1842. }
  1843. if (i2.jsonData.CH && i2.jsonData.RT) {
  1844. tagHtml += `<div style="width:100vw;text-align:center;padding:70%;box-sizing: border-box;"><img style="margin:auto" src="${await this.getEChartsechartsRTCHImage(
  1845. {
  1846. RT: i2.jsonData.RT,
  1847. CH: i2.jsonData.CH
  1848. }
  1849. )}"/></div>`;
  1850. }
  1851. let _content = md.render(i2.jsonData.content).replace(/<p>/g, '').replace(/<\/p>/g, '').replace(/<strong>/g, '<span style="font-weight: bold;">').replace(/<\/strong>/g, '</span>');
  1852. tagHtml += `<p style="font-size:10.5pt;margin-bottom:-0.5in">${_content}</p>`;
  1853. }
  1854. tagHtml += "</div>";
  1855. dire += "</div>";
  1856. analysisHtml += tagHtml;
  1857. directoryHtml += dire;
  1858. }
  1859. // console.log("处理分析完成")
  1860. directoryHtml += "</div>";
  1861. let _html = `
  1862. <div>
  1863. <p style="width:100vw;margin-bottom:1.5in">*分析结果仅供参考</p>
  1864. <p style="font-size:28pt;width:100vw;text-align:center;">课堂观察报告</p>
  1865. <p style="font-size:10pt;width:100vw;text-align:center;margin-bottom:0.6in">报告生成时间:${new Date().toLocaleString()}</p>
  1866. <div style="font-size:16pt;width:100vw;text-align:center;margin-bottom:1in">
  1867. <p style="font-size:20pt;margin-bottom:0.7in">《${bmData.courseName}》</p>
  1868. <p style="margin-bottom:-1in">授课老师:${
  1869. bmData.teacherName ? bmData.teacherName : "未填写"
  1870. }</p>
  1871. <p style="margin-bottom:-1in">授课年级:${
  1872. bmData.grade ? bmData.grade : "未填写"
  1873. }</p>
  1874. <p style="margin-bottom:-1in">授课科目:${
  1875. bmData.subject ? bmData.subject : "未填写"
  1876. }</p>
  1877. <p style="margin-bottom:-1in">授课时间:${
  1878. bmData.time ? bmData.time : "未填写"
  1879. }</p>
  1880. </div>
  1881. <div style="font-size:16pt;width:100vw;text-align:center;margin-bottom:0.5in">
  1882. <img src="${qRCodeSrc}" style="width:150px;height:150px;margin:auto;"/>
  1883. <p>扫码查看网页版</p>
  1884. </div>
  1885. </div>
  1886. ${directoryHtml}
  1887. <div>
  1888. ${analysisHtml}
  1889. </div>
  1890. `;
  1891. const content = `<!DOCTYPE html>
  1892. <html xmlns:v='urn:schemas-microsoft-com
  1893. :vml'xmlns:o='urn:schemas-microsoft-com:office
  1894. :office'xmlns:w='urn:schemas-microsoft-com:office
  1895. :word'xmlns:m='http://schemas.microsoft.com/office/2004/12/omml'
  1896. xmlns='http://www.w3.org/TR/REC-html40'
  1897. xmlns='http://www.w3.org/1999/xhtml'>
  1898. <head>
  1899. <meta charset="UTF-8">
  1900. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  1901. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  1902. <title>《${bmData.courseName}》课堂观察报告</title>
  1903. <style>
  1904. *{
  1905. font-family: '宋体';
  1906. margin:0;
  1907. padding:0;
  1908. line-height:1;
  1909. }
  1910. table {
  1911. border-collapse: collapse; /* 折叠边框 */
  1912. width: 100%;
  1913. font-size:10.5pt;
  1914. }
  1915. th, td {
  1916. border: 1px solid black; /* 线条样式 */
  1917. padding: 8px;
  1918. text-align: left;
  1919. font-size:10.5pt;
  1920. }
  1921. ol,ul{
  1922. margin:0;
  1923. padding:0;
  1924. margin-right:-1in;
  1925. }
  1926. li{
  1927. position: relative;
  1928. padding-left: 1.5em; /* 控制项目符号与文本的距离 */
  1929. margin-bottom: 0.5em;
  1930. text-indent: -1.5em; /* 负缩进使文本与符号对齐 */
  1931. mso-special-format: bullet;
  1932. margin-left: 0;
  1933. padding-left: 10pt;
  1934. text-indent: -10pt;
  1935. mso-style-name: "Normal";
  1936. mso-style-priority: 99;
  1937. mso-style-unhide: no;
  1938. mso-style-qformat: yes;
  1939. mso-style-parent: "";
  1940. }
  1941. p{
  1942. margin:0;
  1943. padding:0
  1944. }
  1945. </style>
  1946. </head>
  1947. <body>
  1948. ${_html}
  1949. </body>
  1950. </html>`;
  1951. // console.log("生成blob",task)
  1952. // debugger
  1953. let blob = htmlDocx.asBlob(content);
  1954. const file = new File([blob], `${bmData.courseName}课堂观察报告.docx`, {
  1955. type: ".docx",
  1956. lastModified: new Date().getTime()
  1957. });
  1958. resolve(file)
  1959. } catch (error) {
  1960. console.log("error",error,task)
  1961. resolve(error)
  1962. }
  1963. })
  1964. },
  1965. getFileLastUpdateTime(file){
  1966. console.log(file)
  1967. if(!file)return new Date().toLocaleDateString().replaceAll("/","-") + ' ' + new Date().toLocaleTimeString();
  1968. const lastModifiedTimestamp = file.lastModified;
  1969. console.log("lastModifiedTimestamp",lastModifiedTimestamp)
  1970. if(!lastModifiedTimestamp)return new Date().toLocaleDateString().replaceAll("/","-") + ' ' + new Date().toLocaleTimeString();
  1971. else return new Date(lastModifiedTimestamp).toLocaleDateString().replaceAll("/","-") + ' ' + new Date().toLocaleTimeString();
  1972. },
  1973. //获取词云图
  1974. getWordCloudMapMixin(data){
  1975. return new Promise((resolve)=>{
  1976. const _msg = `NOTICE
  1977. Language: Please use the same language as the user requirement, if the user speaks Chinese, the specific text of your answer should also be in Chinese.
  1978. ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example".
  1979. Instruction: Based on the context, follow "Format example", write content.
  1980. ## 任务
  1981. 请基于以下课堂实录文本(大约5000字),提炼出20-30个关键词,用于绘制词云图。请给出相应的关键词,关键词出现的频次,词云图上的大小。请确保输出的关键字准确反映课堂实录的主要内容和主题。
  1982. ## 要求
  1983. 1. **提取关键词**:从提供的课堂实录文本中提取出20-30个最具代表性的关键字。关键词应该涵盖课堂实录中的主要概念、重要术语和核心主题。尽量选择多样化的关键词,避免过于集中在某一个主题或概念上。
  1984. 2. **词频统计**:计算每个关键字在文本中出现的频率。
  1985. 3. **词汇大小**:根据词频数量,确定每个关键字在词云图中的大小。词频越高,词汇大小数值越大,数值范围1-100。
  1986. 4. **输出格式**:输出结果应包含每个关键字、对应的词频数量以及词汇大小数值。
  1987. ## 输出格式
  1988. ### 输出格式
  1989. [
  1990. {"value":1,"name":"氯化钠","textStyle":{"color":"#ee7959"}},
  1991. {"value":2,"name":"溶液","textStyle":{"color":"#db9b34"}},
  1992. {"value":1,"name":"实验","textStyle":{"color":"#9d9d82"}},
  1993. {"value":3,"name":"质量分数","textStyle":{"color":"#ea5514"}},
  1994. {"value":1,"name":"溶质","textStyle":{"color":"#c8161d"}},
  1995. {"value":2,"name":"氢氧化钠","textStyle":{"color":"#e60012"}},
  1996. {"value":1,"name":"溶解度","textStyle":{"color":"#1e2732"}},
  1997. {"value":4,"name":"饱和溶液","textStyle":{"color":"#e3adb9"}}
  1998. ]
  1999. 请仅仅输出表头,输出关键词和相应的内容,无需其它任何说明文字。
  2000. 冒号、逗号等符号均使用英文字符。
  2001. ### 输出示例
  2002. [
  2003. {"value":1,"name":"氯化钠","textStyle":{"color":"#ee7959"}},
  2004. {"value":2,"name":"溶液","textStyle":{"color":"#db9b34"}},
  2005. {"value":1,"name":"实验","textStyle":{"color":"#9d9d82"}},
  2006. {"value":3,"name":"质量分数","textStyle":{"color":"#ea5514"}},
  2007. {"value":1,"name":"溶质","textStyle":{"color":"#c8161d"}},
  2008. {"value":2,"name":"氢氧化钠","textStyle":{"color":"#e60012"}},
  2009. {"value":1,"name":"溶解度","textStyle":{"color":"#1e2732"}},
  2010. {"value":4,"name":"饱和溶液","textStyle":{"color":"#e3adb9"}}
  2011. ]
  2012. ## 课堂实录
  2013. ${data}
  2014. `;
  2015. const _uuid = uuidv4();
  2016. let params = {
  2017. temperature: 0,
  2018. max_tokens: 4096,
  2019. top_p: 1,
  2020. frequency_penalty: 0,
  2021. presence_penalty: 0,
  2022. messages: [{ role: "user", content: _msg }],
  2023. uid: _uuid,
  2024. mind_map_question: "",
  2025. stream: false,
  2026. model: "gpt-4o-2024-11-20",
  2027. };
  2028. this.ajax
  2029. .post("https://gpt4.cocorobo.cn/chat", params)
  2030. .then((res) => {
  2031. let _data = res.data.FunctionResponse.choices[0];
  2032. let _jsonData = _data.message.content;
  2033. _jsonData = _jsonData.replaceAll("```json", "").replaceAll("```", "");
  2034. let _result = JSON.parse(_jsonData);
  2035. resolve({
  2036. tooltip: {
  2037. show: false,
  2038. },
  2039. series: [
  2040. {
  2041. type: 'wordCloud',
  2042. sizeRange: [14, 38],
  2043. rotationRange: [0, 0],
  2044. keepAspect:false,
  2045. shape: 'circle',
  2046. left: 'center',
  2047. top: 'center',
  2048. right: null,
  2049. bottom: null,
  2050. width: '90%',
  2051. height: '90%',
  2052. rotationRange: [-90, 90],
  2053. rotationStep: 45,
  2054. data: _result,
  2055. },
  2056. ],
  2057. })
  2058. })
  2059. .catch((e) => {
  2060. console.log(e);
  2061. resolve(0)
  2062. })
  2063. })
  2064. }
  2065. }
  2066. };