From 7145327058a8152045c7f14bda64832e2bc5913b Mon Sep 17 00:00:00 2001 From: Dawood Khan Date: Thu, 18 May 2023 08:55:46 -0700 Subject: [PATCH] Using marked for chatbot markdown parsing (#4150) --- CHANGELOG.md | 1 + gradio/components.py | 4 - gradio/utils.py | 14 -- js/app/src/components/Chatbot/Chatbot.svelte | 3 + js/chatbot/package.json | 7 +- js/chatbot/src/ChatBot.svelte | 122 +++++++--- js/chatbot/src/Copy.svelte | 59 +++++ js/chatbot/src/manni.css | 231 ------------------- pnpm-lock.yaml | 34 +++ test/test_components.py | 2 +- 10 files changed, 195 insertions(+), 282 deletions(-) create mode 100644 js/chatbot/src/Copy.svelte delete mode 100644 js/chatbot/src/manni.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d20a74d15..23ca848a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ No changes to highlight. ## Other Changes: +- Change `gr.Chatbot()` markdown parsing to frontend using `marked` library and `prism` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4150](https://github.com/gradio-app/gradio/pull/4150) - Update the js client by [@pngwn](https://github.com/pngwn) in [PR 3899](https://github.com/gradio-app/gradio/pull/3899) - Fix documentation for the shape of the numpy array produced by the `Image` component by [@der3318](https://github.com/der3318) in [PR 4204](https://github.com/gradio-app/gradio/pull/4204). - Updates the timeout for websocket messaging from 1 second to 5 seconds by [@abidlabs](https://github.com/abidlabs) in [PR 4235](https://github.com/gradio-app/gradio/pull/4235) diff --git a/gradio/components.py b/gradio/components.py index 60a69131a9..2f9f03553b 100644 --- a/gradio/components.py +++ b/gradio/components.py @@ -4526,7 +4526,6 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable): warnings.warn( "The 'color_map' parameter has been deprecated.", ) - self.md = utils.get_markdown_parser() self.select: EventListenerMethod """ Event listener for when the user selects message from Chatbot. @@ -4628,9 +4627,6 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable): } elif isinstance(chat_message, str): chat_message = inspect.cleandoc(chat_message) - chat_message = cast(str, self.md.render(chat_message)) - if chat_message.startswith("

") and chat_message.endswith("

\n"): - chat_message = chat_message[3:-5] return chat_message else: raise ValueError(f"Invalid message for Chatbot component: {chat_message}") diff --git a/gradio/utils.py b/gradio/utils.py index cd751c80a5..90d8657713 100644 --- a/gradio/utils.py +++ b/gradio/utils.py @@ -39,9 +39,6 @@ from markdown_it import MarkdownIt from mdit_py_plugins.dollarmath.index import dollarmath_plugin from mdit_py_plugins.footnote.index import footnote_plugin from pydantic import BaseModel, parse_obj_as -from pygments import highlight -from pygments.formatters import HtmlFormatter -from pygments.lexers import get_lexer_by_name import gradio from gradio.context import Context @@ -890,16 +887,6 @@ def get_serializer_name(block: Block) -> str | None: return cls.__name__ -def highlight_code(code, name, attrs): - try: - lexer = get_lexer_by_name(name) - except Exception: - lexer = get_lexer_by_name("text") - formatter = HtmlFormatter() - - return highlight(code, lexer, formatter) - - def get_markdown_parser() -> MarkdownIt: md = ( MarkdownIt( @@ -908,7 +895,6 @@ def get_markdown_parser() -> MarkdownIt: "linkify": True, "typographer": True, "html": True, - "highlight": highlight_code, }, ) .use(dollarmath_plugin, renderer=tex2svg, allow_digits=False) diff --git a/js/app/src/components/Chatbot/Chatbot.svelte b/js/app/src/components/Chatbot/Chatbot.svelte index c88adcef2b..9920adf545 100644 --- a/js/app/src/components/Chatbot/Chatbot.svelte +++ b/js/app/src/components/Chatbot/Chatbot.svelte @@ -3,6 +3,7 @@ import { Block, BlockLabel } from "@gradio/atoms"; import type { LoadingStatus } from "../StatusTracker/types"; import type { Styles } from "@gradio/utils"; + import type { ThemeMode } from "js/app/src/components/types"; import { Chat } from "@gradio/icons"; import type { FileData } from "@gradio/upload"; import { normalise_file } from "@gradio/upload"; @@ -20,6 +21,7 @@ export let root: string; export let root_url: null | string; export let selectable: boolean = false; + export let theme_mode: ThemeMode; const redirect_src_url = (src: string) => src.replace('src="/file', `src="${root}file`); @@ -50,6 +52,7 @@ + import { marked } from "marked"; + import Prism from "prismjs"; + import "prismjs/components/prism-python"; import { beforeUpdate, afterUpdate, createEventDispatcher } from "svelte"; import type { Styles, SelectData } from "@gradio/utils"; + import type { ThemeMode } from "js/app/src/components/types"; import type { FileData } from "@gradio/upload"; - import "./manni.css"; + import Copy from "./Copy.svelte"; + + const code_highlight_css = { + light: () => import("prismjs/themes/prism.css"), + dark: () => import("prismjs/themes/prism-dark.css") + }; export let value: Array< [string | FileData | null, string | FileData | null] @@ -14,6 +23,32 @@ export let feedback: Array | null = null; export let style: Styles = {}; export let selectable: boolean = false; + export let theme_mode: ThemeMode; + + $: if (theme_mode == "dark") { + code_highlight_css.dark(); + } else { + code_highlight_css.light(); + } + marked.setOptions({ + renderer: new marked.Renderer(), + gfm: true, + breaks: true, + pedantic: false, + sanitize: true, + smartLists: true, + smartypants: false + }); + + marked.setOptions({ + highlight: (code: string, lang: string) => { + if (Prism.languages[lang]) { + return Prism.highlight(code, Prism.languages[lang], lang); + } else { + return code; + } + } + }); let div: HTMLDivElement; let autoscroll: Boolean; @@ -39,28 +74,22 @@ } div.querySelectorAll("pre > code").forEach((n) => { let code_node = n as HTMLElement; - let node = n.parentElement as HTMLElement; - node.style.position = "relative"; - const button = document.createElement("button"); - button.className = "copy-button"; - button.innerHTML = "Copy"; - button.style.position = "absolute"; - button.style.right = "0"; - button.style.top = "0"; - button.style.zIndex = "1"; - button.style.padding = "var(--spacing-md)"; - button.style.marginTop = "12px"; - button.style.fontSize = "var(--text-sm)"; - button.style.borderBottomLeftRadius = "var(--radius-sm)"; - button.style.backgroundColor = "var(--block-label-background-fill)"; - button.addEventListener("click", () => { - navigator.clipboard.writeText(code_node.innerText.trimEnd()); - button.innerHTML = "Copied!"; - setTimeout(() => { - button.innerHTML = "Copy"; - }, 1000); + const copy_div = document.createElement("div"); + new Copy({ + target: copy_div, + props: { + value: code_node.innerText.trimEnd() + } }); - node.appendChild(button); + let node = n.parentElement as HTMLElement; + copy_div.style.position = "absolute"; + copy_div.style.right = "0"; + copy_div.style.top = "0"; + copy_div.style.zIndex = "1"; + copy_div.style.padding = "var(--spacing-md)"; + copy_div.style.borderBottomLeftRadius = "var(--radius-sm)"; + node.style.position = "relative"; + node.appendChild(copy_div); }); }); @@ -96,7 +125,7 @@ })} > {#if typeof message === "string"} - {@html message} + {@html marked.parse(message)} {#if feedback && j == 1}