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}