1/* 2 * Copyright (C) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF unknown KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { BaseElement, element } from '../../base-ui/BaseElement'; 17import { SpStatisticsHttpUtil } from '../../statistics/util/SpStatisticsHttpUtil'; 18import { threadPool } from '../database/SqlLite'; 19import { SpAiAnalysisPageHtml } from './SpAiAnalysisPage.html' 20import { getTimeString } from './trace/sheet/TabPaneCurrentSelection'; 21import { WebSocketManager } from '../../webSocket/WebSocketManager'; 22import { TypeConstants } from '../../webSocket/Constants'; 23import { TraceRow } from './trace/base/TraceRow'; 24import { SpSystemTrace } from './SpSystemTrace'; 25 26@element('sp-ai-analysis') 27export class SpAiAnalysisPage extends BaseElement { 28 private askQuestion: Element | null | undefined; 29 private aiAnswerBox: HTMLDivElement | null | undefined; 30 private newChatEl: HTMLImageElement | null | undefined; 31 private chatWindow: HTMLDivElement | null | undefined; 32 private inputEl: HTMLTextAreaElement | null | undefined; 33 private chatImg: HTMLImageElement | null | undefined; 34 private reportBar: HTMLImageElement | null | undefined; 35 private reportImg: HTMLImageElement | null | undefined; 36 private sendImg: HTMLImageElement | null | undefined; 37 private draftBtn: HTMLDivElement | null | undefined; 38 private downloadBtn: HTMLDivElement | null | undefined; 39 private draftList: HTMLDivElement | null | undefined; 40 private noDataEl: HTMLDivElement | null | undefined; 41 private loginTipEl: HTMLDivElement | null | undefined; 42 private loadingItem: HTMLDivElement | null | undefined; 43 private startTimeEl: HTMLSpanElement | null | undefined; 44 private endTimeEl: HTMLSpanElement | null | undefined; 45 private question: string = ''; 46 private token: string = ''; 47 // 是否点击了新建聊天 48 private isNewChat: boolean = false; 49 isCtrlDown: boolean = false; 50 static isRepeatedly: boolean = false; 51 // 拼接下载内容 52 private reportContent: string = ''; 53 private md: unknown; 54 // 监听选中时间范围变化 55 static selectChangeListener(startTime: number, endTime: number) { 56 let startEl = document.querySelector("body > sp-application")!.shadowRoot!.querySelector("#sp-ai-analysis")!.shadowRoot?.querySelector("div.chatBox > div > div.report_details > div.selectionBox > div.startBox > span"); 57 startEl!.innerHTML = getTimeString(startTime).toString(); 58 let endEl = document.querySelector("body > sp-application")!.shadowRoot!.querySelector("#sp-ai-analysis")!.shadowRoot?.querySelector("div.chatBox > div > div.report_details > div.selectionBox > div.endBox > span"); 59 endEl!.innerHTML = getTimeString(endTime).toString(); 60 } 61 initElements(): void { 62 this.md = require('markdown-it')({ 63 html: true, 64 typographer: true 65 }); 66 let chatBar = this.shadowRoot?.querySelector('.chatBar'); 67 this.askQuestion = this.shadowRoot?.querySelector('.ask_question'); 68 this.reportBar = this.shadowRoot?.querySelector('.report'); 69 let reportDetails = this.shadowRoot?.querySelector('.report_details'); 70 let chatInputBox = this.shadowRoot?.querySelector('.chatInputBox'); 71 this.chatWindow = this.shadowRoot?.querySelector('.ask_question'); 72 this.inputEl = this.shadowRoot?.querySelector('.inputText'); 73 this.chatImg = this.shadowRoot?.querySelector('.chatBar')?.getElementsByTagName('img')[0]; 74 this.reportImg = this.shadowRoot?.querySelector('.report')?.getElementsByTagName('img')[0]; 75 this.sendImg = document.querySelector("body > sp-application")!.shadowRoot!.querySelector("#sp-ai-analysis")!.shadowRoot?.querySelector("div.chatInputBox > div.chatInput > img"); 76 this.newChatEl = document.querySelector("body > sp-application")!.shadowRoot!.querySelector("#sp-ai-analysis")!.shadowRoot?.querySelector("div.chatBox > div > div.chatInputBox > div.chatConfig > div > div.newChat > img"); 77 // 诊断按钮 78 this.draftBtn = this.shadowRoot?.querySelector('.analysisBtn'); 79 // 下载报告按钮 80 this.downloadBtn = this.shadowRoot?.querySelector('.downloadBtn'); 81 // 报告列表 82 this.draftList = this.shadowRoot?.querySelector('.data-record'); 83 // 空数据页面 84 this.noDataEl = this.shadowRoot?.querySelector('.no-data'); 85 // 未连接提示弹窗 86 this.loginTipEl = this.shadowRoot?.querySelector('.loginTip'); 87 // 时间展示区域 88 this.startTimeEl = this.shadowRoot?.querySelector('.startTime'); 89 this.startTimeEl!.innerHTML = getTimeString(TraceRow.range?.startNS!); 90 this.endTimeEl = this.shadowRoot?.querySelector('.endTime'); 91 this.endTimeEl!.innerHTML = getTimeString(TraceRow.range?.endNS!); 92 93 // 发送消息图标点击事件 94 this.sendImg?.addEventListener('click', () => { 95 this.sendMessage(); 96 }) 97 98 // 新建对话按钮点击事件 99 this.newChatEl?.addEventListener('click', () => { 100 this.isNewChat = true; 101 this.token = ''; 102 this.askQuestion!.innerHTML = ''; 103 this.createAiChatBox('有什么可以帮助您的吗?'); 104 }) 105 106 // 输入框发送消息 107 this.inputEl?.addEventListener('keydown', (e) => { 108 if (e.key.toLocaleLowerCase() === 'control' || e.keyCode === 17) { 109 this.isCtrlDown = true; 110 } 111 if (this.isCtrlDown) { 112 if (e.key.toLocaleLowerCase() === 'enter') { 113 this.inputEl!.value += '\n'; 114 } 115 } else { 116 if (e.key.toLocaleLowerCase() === 'enter') { 117 this.sendMessage(); 118 // 禁止默认的回车换行 119 e.preventDefault(); 120 }; 121 }; 122 }); 123 124 // 输入框聚焦/失焦--防止触发页面快捷键 125 this.inputEl?.addEventListener('focus', () => { 126 SpSystemTrace.isAiAsk = true; 127 }); 128 129 this.inputEl?.addEventListener('blur', () => { 130 SpSystemTrace.isAiAsk = false; 131 }) 132 133 // 监听浏览器刷新,清除db数据 134 window.onbeforeunload = function () { 135 caches.delete(`${window.localStorage.getItem('fileName')}.db`); 136 sessionStorage.removeItem('fileName'); 137 } 138 139 // 监听ctrl抬起 140 this.inputEl?.addEventListener('keyup', (e) => { 141 if (e.key.toLocaleLowerCase() === 'control' || e.keyCode === 17) { 142 this.isCtrlDown = false; 143 }; 144 }); 145 146 // 下载诊断报告按钮监听 147 this.downloadBtn?.addEventListener('click', (e) => { 148 let a = document.createElement('a'); 149 a.href = URL.createObjectURL(new Blob([this.reportContent])); 150 a.download = window.sessionStorage.getItem('fileName')! + '诊断报告'; 151 a.click(); 152 }) 153 154 // 诊断按钮 155 this.draftBtn?.addEventListener('click', async (e) => { 156 // 取消已经存在的诊断 157 this.draftList!.innerHTML = ''; 158 // 没有登陆,弹窗提示,退出逻辑 159 if (!WebSocketManager.getInstance()?.isReady()) { 160 this.loginTipEl!.style.visibility = 'visible'; 161 setTimeout(() => { 162 this.loginTipEl!.style.visibility = 'hidden'; 163 }, 1000); 164 return; 165 } 166 // 同一个trace非第一次诊断,无需再发db文件过去 167 if (SpAiAnalysisPage.isRepeatedly) { 168 this.initiateDiagnosis(); 169 } else { 170 // 首次诊断 171 WebSocketManager.getInstance()!.registerMessageListener(TypeConstants.DIAGNOSIS_TYPE, this.webSocketCallBack); 172 // 看缓存中有没有db,没有的话拿一个进行诊断并存缓存 173 let fileName = sessionStorage.getItem('fileName'); 174 caches.match(`${fileName}.db`).then(async (res) => { 175 if (!res) { 176 this.cacheDb(fileName); 177 } 178 WebSocketManager.getInstance()!.sendMessage(TypeConstants.DIAGNOSIS_TYPE, TypeConstants.SENDDB_CMD, new TextEncoder().encode(await res!.text())); 179 }); 180 }; 181 // 隐藏nodata 182 this.noDataEl!.style.display = 'none'; 183 // 加载中的loading模块 184 let loadingDiv = document.createElement('div'); 185 loadingDiv.className = 'loadingBox'; 186 loadingDiv.innerHTML = '<lit-loading style="position:absolute;top:45%;left:45%;z-index:999"></lit-loading>'; 187 let loadingItem = document.createElement('div'); 188 loadingItem.className = 'loadingItem'; 189 this.loadingItem = loadingItem; 190 loadingItem!.appendChild(loadingDiv); 191 this.draftList?.appendChild(loadingItem); 192 }) 193 194 // 侧边栏诊断点击事件 *************优化,考虑多个按钮 195 this.reportBar!.addEventListener('click', () => { 196 this.reportImg!.src = 'img/report_active.png'; 197 this.chatImg!.src = 'img/talk.png'; 198 this.reportBar!.classList.add('active'); 199 chatBar!.classList.remove('active'); 200 //@ts-ignore 201 this.askQuestion!.style.display = 'none'; 202 //@ts-ignore 203 chatInputBox!.style.display = 'none'; 204 //@ts-ignore 205 reportDetails!.style.display = 'block'; 206 }); 207 208 // 侧边栏聊天点击事件 209 chatBar!.addEventListener('click', () => { 210 this.reportImg!.src = 'img/report.png'; 211 this.chatImg!.src = 'img/talk_active.png'; 212 this.reportBar!.classList.remove('active'); 213 chatBar!.classList.add('active'); 214 //@ts-ignore 215 this.askQuestion!.style.display = 'block'; 216 //@ts-ignore 217 chatInputBox!.style.display = 'block'; 218 //@ts-ignore 219 reportDetails!.style.display = 'none'; 220 }); 221 222 223 } 224 225 // 点击诊断之后,重置 226 reset() { 227 this.reportContent = ''; 228 this.downloadBtn!.style.display = 'none'; 229 } 230 231 // 发送消息 232 async sendMessage() { 233 if (this.inputEl!.value != '') { 234 if (this.isNewChat) { 235 this.isNewChat = false; 236 } 237 this.question = JSON.parse(JSON.stringify(this.inputEl!.value)); 238 this.createChatBox(); 239 this.createAiChatBox('AI智能分析中...'); 240 this.chatWindow!.scrollTop = this.chatWindow!.scrollHeight; 241 // 没有token 242 if (this.token === '') { 243 this.token = await SpStatisticsHttpUtil.getAItoken(); 244 if (this.token === '') { 245 this.aiAnswerBox!.firstElementChild!.innerHTML = '获取token失败'; 246 return; 247 } 248 } 249 this.answer(); 250 } 251 } 252 253 // ai对话 254 async answer() { 255 let requestBody = { 256 token: this.token, 257 question: this.question, 258 collection: 'smart_perf_test', 259 scope: 'smartperf' 260 }; 261 let answer = await SpStatisticsHttpUtil.askAi(requestBody); 262 if (answer !== '') { 263 if (!this.isNewChat) { 264 // @ts-ignore 265 this.aiAnswerBox!.firstElementChild!.innerHTML = this.md!.render(answer); 266 // 滚动条滚到底部 267 this.chatWindow!.scrollTop = this.chatWindow!.scrollHeight; 268 } 269 } else { 270 this.aiAnswerBox!.firstElementChild!.innerHTML = '服务器异常'; 271 this.chatWindow!.scrollTop = this.chatWindow!.scrollHeight; 272 } 273 } 274 275 // 创建用户聊天对话气泡 276 createChatBox() { 277 // 生成头像 278 let headerDiv = document.createElement('div'); 279 headerDiv.className = 'userHeader headerDiv'; 280 // 生成聊天内容框 281 let newQuestion = document.createElement('div'); 282 newQuestion.className = "usersay"; 283 // @ts-ignore 284 newQuestion!.innerHTML = this.inputEl!.value; 285 // 生成聊天气泡三角 286 let triangleDiv = document.createElement('div'); 287 newQuestion.appendChild(triangleDiv); 288 triangleDiv.className = 'userTriangle'; 289 // 单条消息模块,最大的div,包含头像、消息、清除浮动元素 290 let newMessage = document.createElement('div'); 291 newMessage.className = 'usermessage message'; 292 // @ts-ignore 293 this.inputEl!.value = ''; 294 newMessage.appendChild(headerDiv); 295 newMessage.appendChild(newQuestion); 296 let claerDiv = document.createElement('div'); 297 claerDiv.className = 'clear'; 298 newMessage.appendChild(claerDiv); 299 this.askQuestion?.appendChild(newMessage); 300 } 301 302 // 创建ai助手聊天对话气泡 303 createAiChatBox(aiText: string) { 304 // 生成ai头像 305 let headerDiv = document.createElement('div'); 306 headerDiv.className = 'aiHeader headerDiv'; 307 headerDiv.innerHTML = `<img class='headerImg' src = "img/logo.png" title=""></img>` 308 let newQuestion = document.createElement('div'); 309 newQuestion.className = "systemSay"; 310 // @ts-ignore 311 newQuestion!.innerHTML = `<div>${aiText}</div>`; 312 let triangleDiv = document.createElement('div'); 313 newQuestion.appendChild(triangleDiv); 314 triangleDiv.className = 'aiTriangle'; 315 let newMessage = document.createElement('div'); 316 newMessage.className = 'aiMessage message'; 317 newMessage.appendChild(headerDiv); 318 newMessage.appendChild(newQuestion); 319 let claerDiv = document.createElement('div'); 320 claerDiv.className = 'clear'; 321 this.aiAnswerBox = newQuestion; 322 newMessage.appendChild(claerDiv); 323 this.askQuestion?.appendChild(newMessage); 324 } 325 326 // 页面渲染诊断结果 327 async renderData(dataList: any) { 328 for (let i = 0; i < dataList.length; i++) { 329 let itemDiv = document.createElement('div'); 330 itemDiv!.style.visibility = 'hidden'; 331 itemDiv.className = 'analysisItem'; 332 // 生成标题 333 let titleDiv = document.createElement('div'); 334 titleDiv.className = 'title item-name'; 335 titleDiv!.innerText = `问题${i + 1}:${dataList[i].type}`; 336 // 生成时间 337 let timeDiv = document.createElement('div'); 338 timeDiv.className = 'item two'; 339 // 获取每一个诊断项的时间 340 let timeList = new Array(); 341 dataList[i].trace_info.forEach((v: any) => { 342 timeList.push(getTimeString(v.ts / 1000000)); 343 }); 344 timeDiv!.innerHTML = `<span class="item-name">发生时间:</span>${timeList.join(',')}` 345 // 生成问题原因 346 let reasonDiv = document.createElement('div'); 347 reasonDiv.className = 'item'; 348 reasonDiv!.innerHTML = `<span class="item-name">问题原因:</span>${dataList[i].description}`; 349 itemDiv.appendChild(titleDiv); 350 itemDiv.appendChild(timeDiv); 351 itemDiv.appendChild(reasonDiv); 352 // 生成优化建议 353 let suggestonDiv = document.createElement('div'); 354 suggestonDiv.className = 'item two'; 355 let suggestionText = ''; 356 this.token = await SpStatisticsHttpUtil.getAItoken(); 357 suggestionText = await this.getSuggestion(dataList[i].description, itemDiv, suggestonDiv); 358 this.reportContent += `问题${i + 1}:${dataList[i].type}\n\n时间:${timeList.join(',')}\n\n问题原因:${dataList[i].description}\n\n优化建议:${suggestionText}\n\n\n`; 359 } 360 this.loadingItem!.style.display = 'none'; 361 this.downloadBtn!.style.display = 'inline-block'; 362 } 363 364 // 发送请求获取优化建议并渲染页面 365 async getSuggestion(description: string, itemDiv: HTMLDivElement | null | undefined, suggestonDiv: HTMLDivElement | null | undefined) { 366 let suggestion = await SpStatisticsHttpUtil.askAi({ 367 token: this.token, 368 question: description + ',请问该怎么优化?', 369 collection: '' 370 }); 371 suggestonDiv!.innerHTML = `<span class="item-name">优化建议:</span>${suggestion}`; 372 itemDiv!.appendChild(suggestonDiv!); 373 // 吧loading放到最后面 374 this.draftList!.insertBefore(itemDiv!, this.loadingItem!); 375 itemDiv!.style.visibility = 'visible'; 376 itemDiv!.style.animation = 'opcityliner 3s'; 377 return suggestion; 378 } 379 380 cacheDb(fileName: string | null) { 381 threadPool.submit( 382 'download-db', 383 '', 384 {}, 385 (reqBufferDB: Uint8Array) => { 386 // 存入缓存 387 caches.open(`${fileName}.db`).then((cache) => { 388 let headers = new Headers(); 389 headers.append('Content-Type', 'application/octet-stream'); 390 headers.append('Content-Transfer-Encoding', 'binary') 391 return cache 392 .put( 393 `${fileName}.db`, 394 new Response(reqBufferDB, { 395 status: 200, 396 }) 397 ); 398 }); 399 }, 400 'download-db' 401 ); 402 } 403 404 // websocket通信回调注册 405 webSocketCallBack = (cmd: number, result: Uint8Array) => { 406 const decoder = new TextDecoder(); 407 const jsonString = decoder.decode(result); 408 let jsonRes = JSON.parse(jsonString); 409 // db文件写入成功 410 if (cmd === 2) { 411 SpAiAnalysisPage.isRepeatedly = true; 412 this.initiateDiagnosis(); 413 } 414 // 诊断结果,resultCode===0:失败;resultCode===1:成功 415 if (cmd === 4) { 416 // 需要处理 417 if (jsonRes.resultCode !== 0) { 418 console.log('错误'); 419 } 420 let dataList = JSON.parse(jsonRes.resultMessage); 421 // 整理数据,渲染数据 422 this.renderData(dataList); 423 } 424 } 425 426 // 发起诊断 427 initiateDiagnosis() { 428 let requestBodyObj = { 429 type: 0 430 } 431 let requestBodyString = JSON.stringify(requestBodyObj); 432 let requestBody = new TextEncoder().encode(requestBodyString); 433 WebSocketManager.getInstance()!.sendMessage(TypeConstants.DIAGNOSIS_TYPE, TypeConstants.DIAGNOSIS_CMD, requestBody); 434 } 435 436 initHtml(): string { 437 return SpAiAnalysisPageHtml; 438 } 439}