From b0a78f2b30b61d197c6672d7693534d40bc4b164 Mon Sep 17 00:00:00 2001 From: Marrrrrrrrrrtin <90629140+MartinRepo@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:08:22 +0000 Subject: [PATCH] feat: improve messages render performance (#777) --- web/src/ChatBox.js | 74 +---------------------------- web/src/ChatEditPage.js | 4 ++ web/src/ChatListPage.js | 4 ++ web/src/ChatMessageRender.js | 91 ++++++++++++++++++++++++++++++++++++ web/src/ChatPage.js | 10 ++++ 5 files changed, 110 insertions(+), 73 deletions(-) create mode 100644 web/src/ChatMessageRender.js diff --git a/web/src/ChatBox.js b/web/src/ChatBox.js index d0a0f3072..6c9d01eef 100644 --- a/web/src/ChatBox.js +++ b/web/src/ChatBox.js @@ -15,27 +15,10 @@ import React from "react"; import {Avatar, ChatContainer, ConversationHeader, MainContainer, Message, MessageInput, MessageList} from "@chatscope/chat-ui-kit-react"; import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; -import {marked} from "marked"; -import DOMPurify from "dompurify"; -import katex from "katex"; -import "katex/dist/katex.min.css"; -import hljs from "highlight.js"; -import "highlight.js/styles/atom-one-dark-reasonable.css"; import * as Conf from "./Conf"; import * as Setting from "./Setting"; import i18next from "i18next"; -marked.setOptions({ - renderer: new marked.Renderer(), - gfm: true, - tables: true, - breaks: true, - pedantic: false, - sanitize: false, - smartLists: true, - smartypants: true, -}); - class ChatBox extends React.Component { constructor(props) { super(props); @@ -117,61 +100,6 @@ class ChatBox extends React.Component { reader.readAsDataURL(file); }; - renderMarkdown(text) { - if (text === "") { - text = this.state.dots; - } - const rawHtml = marked(text); - let cleanHtml = DOMPurify.sanitize(rawHtml); - /* replace
with , reduce paragraph spacing. */ - cleanHtml = cleanHtml.replace(//g, "
/g, ""); - return cleanHtml; - } - - renderLatex(text) { - const inlineLatexRegex = /\(\s*(([a-zA-Z])|(\\.+?)|([^)]*?[_^!].*?))\s*\)/g; - const blockLatexRegex = /\[\s*(.+?)\s*\]/g; - - text = text.replace(blockLatexRegex, (match, formula) => { - try { - return katex.renderToString(formula, {throwOnError: false, displayMode: true}); - } catch (error) { - return match; - } - }); - - return text.replace(inlineLatexRegex, (match, formula) => { - try { - return katex.renderToString(formula, {throwOnError: false, displayMode: false}); - } catch (error) { - return match; - } - }); - } - - renderCode(text) { - const tempDiv = document.createElement("div"); - tempDiv.innerHTML = text; - tempDiv.querySelectorAll("pre code").forEach((block) => { - hljs.highlightBlock(block); - }); - return tempDiv.innerHTML; - } - - renderText(text) { - let html; - html = this.renderMarkdown(text); - html = this.renderLatex(html); - html = this.renderCode(html); - return ; - } - render() { let title = Setting.getUrlParam("title"); if (title === null) { @@ -202,7 +130,7 @@ class ChatBox extends React.Component { }} avatarPosition={message.author === "AI" ? "tl" : "tr"}>- {this.renderText(message.text)} + {message.text === "" ? this.state.dots : message.html} ))} diff --git a/web/src/ChatEditPage.js b/web/src/ChatEditPage.js index 4e91fdb8a..71d8d7c16 100644 --- a/web/src/ChatEditPage.js +++ b/web/src/ChatEditPage.js @@ -18,6 +18,7 @@ import * as ChatBackend from "./backend/ChatBackend"; import * as Setting from "./Setting"; import i18next from "i18next"; import ChatBox from "./ChatBox"; +import {renderText} from "./ChatMessageRender"; import * as MessageBackend from "./backend/MessageBackend"; const {Option} = Select; @@ -56,6 +57,9 @@ class ChatEditPage extends React.Component { getMessages(chatName) { MessageBackend.getChatMessages("admin", chatName) .then((res) => { + res.data.map((message) => { + message.html = renderText(message.text); + }); this.setState({ messages: res.data, }); diff --git a/web/src/ChatListPage.js b/web/src/ChatListPage.js index 040446639..f7a0a71de 100644 --- a/web/src/ChatListPage.js +++ b/web/src/ChatListPage.js @@ -22,6 +22,7 @@ import i18next from "i18next"; import * as Conf from "./Conf"; import * as MessageBackend from "./backend/MessageBackend"; import ChatBox from "./ChatBox"; +import {renderText} from "./ChatMessageRender"; class ChatListPage extends React.Component { constructor(props) { @@ -66,6 +67,9 @@ class ChatListPage extends React.Component { MessageBackend.getChatMessages("admin", chatName) .then((res) => { const messagesMap = this.state.messagesMap; + res.data.map((message) => { + message.html = renderText(message.text); + }); messagesMap[chatName] = res.data; this.setState({ messagesMap: messagesMap, diff --git a/web/src/ChatMessageRender.js b/web/src/ChatMessageRender.js new file mode 100644 index 000000000..7158484d9 --- /dev/null +++ b/web/src/ChatMessageRender.js @@ -0,0 +1,91 @@ +// Copyright 2023 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {marked} from "marked"; +import DOMPurify from "dompurify"; +import katex from "katex"; +import "katex/dist/katex.min.css"; +import hljs from "highlight.js"; +import "highlight.js/styles/atom-one-dark-reasonable.css"; + +marked.setOptions({ + renderer: new marked.Renderer(), + gfm: true, + tables: true, + breaks: true, + pedantic: false, + sanitize: false, + smartLists: true, + smartypants: true, +}); + +export function renderMarkdown(text) { + const rawHtml = marked(text); + let cleanHtml = DOMPurify.sanitize(rawHtml); + /* replace with , reduce paragraph spacing. */ + cleanHtml = cleanHtml.replace(//g, "
").replace(/<\/p>/g, ""); + /* h2 is larger than h1, h2 is the largest, so replace h1 with h2, and set margin as 20px. */ + cleanHtml = cleanHtml.replace(//g, "
").replace(/<(h[1-6])>/g, "<$1 style='margin-top: 20px; margin-bottom: 20px'>"); + /* adjust margin and internal gap for unordered list and ordered list. */ + cleanHtml = cleanHtml.replace(/<(ul)>/g, "
").replace(/<(ol)>/g, "
"); + /* adjust code block, for auto line feed. */ + cleanHtml = cleanHtml.replace(/
/g, ""); + return cleanHtml; +} + +export function renderLatex(text) { + const inlineLatexRegex = /\(\s*(([a-zA-Z])|(\\.+?)|([^)]*?[_^!].*?))\s*\)/g; + const blockLatexRegex = /\[\s*(.+?)\s*\]/g; + + text = text.replace(blockLatexRegex, (match, formula) => { + try { + return katex.renderToString(formula, { + throwOnError: false, + displayMode: true, + }); + } catch (error) { + return match; + } + }); + + return text.replace(inlineLatexRegex, (match, formula) => { + try { + return katex.renderToString(formula, { + throwOnError: false, + displayMode: false, + }); + } catch (error) { + return match; + } + }); +} + +export function renderCode(text) { + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = text; + tempDiv.querySelectorAll("pre code").forEach((block) => { + hljs.highlightBlock(block); + }); + return tempDiv.innerHTML; +} + +export function renderText(text) { + let html; + html = renderMarkdown(text); + html = renderLatex(html); + html = renderCode(html); + return ( + + ); +} diff --git a/web/src/ChatPage.js b/web/src/ChatPage.js index 03cfc2952..474a05707 100644 --- a/web/src/ChatPage.js +++ b/web/src/ChatPage.js @@ -17,6 +17,7 @@ import {Modal, Spin} from "antd"; import moment from "moment"; import ChatMenu from "./ChatMenu"; import ChatBox from "./ChatBox"; +import {renderText} from "./ChatMessageRender"; import * as Setting from "./Setting"; import * as ChatBackend from "./backend/ChatBackend"; import * as MessageBackend from "./backend/MessageBackend"; @@ -150,6 +151,9 @@ class ChatPage extends BaseListPage { getMessages(chat) { MessageBackend.getChatMessages("admin", chat.name) .then((res) => { + res.data.map((message) => { + message.html = renderText(message.text); + }); this.setState({ messages: res.data, }); @@ -178,6 +182,9 @@ class ChatPage extends BaseListPage { text += jsonData.text; lastMessage2.text = text; res.data[res.data.length - 1] = lastMessage2; + res.data.map((message) => { + message.html = renderText(message.text); + }); this.setState({ messages: res.data, disableInput: false, @@ -188,6 +195,9 @@ class ChatPage extends BaseListPage { const lastMessage2 = Setting.deepCopy(lastMessage); lastMessage2.text = error; res.data[res.data.length - 1] = lastMessage2; + res.data.map((message) => { + message.html = renderText(message.text); + }); this.setState({ messages: res.data, disableInput: true,