diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6168b909..ef85e7265e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ## New Features: - Add support for `visual-question-answering`, `document-question-answering`, and `image-to-text` using `gr.Interface.load("models/...")` and `gr.Interface.from_pipeline` by [@osanseviero](https://github.com/osanseviero) in [PR 3887](https://github.com/gradio-app/gradio/pull/3887) +- Add code block support in `gr.Chatbot()`, by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4048](https://github.com/gradio-app/gradio/pull/4048) - Adds the ability to blocklist filepaths (and also improves the allowlist mechanism) by [@abidlabs](https://github.com/abidlabs) in [PR 4047](https://github.com/gradio-app/gradio/pull/4047). - Adds the ability to specify the upload directory via an environment variable by [@abidlabs](https://github.com/abidlabs) in [PR 4047](https://github.com/gradio-app/gradio/pull/4047). @@ -27,6 +28,7 @@ No changes to highlight. ## Breaking Changes: - `gr.HuggingFaceDatasetSaver` behavior changed internally. The `flagging/` folder is not a `.git/` folder anymore when using it. `organization` parameter is now ignored in favor of passing a full dataset id as `dataset_name` (e.g. `"username/my-dataset"`). +- New lines (`\n`) are not automatically converted to `
` in `gr.Markdown()` or `gr.Chatbot()`. For multiple new lines, a developer must add multiple `
` tags. ## Full Changelog: diff --git a/gradio/components.py b/gradio/components.py index e03290a7a1..77069f96cc 100644 --- a/gradio/components.py +++ b/gradio/components.py @@ -20,7 +20,7 @@ from copy import deepcopy from enum import Enum from pathlib import Path from types import ModuleType -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Type +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Type, cast import aiofiles import altair as alt @@ -4593,12 +4593,11 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable): "is_file": True, } elif isinstance(chat_message, str): - children = self.md.parseInline(chat_message)[0].children - if children and any("code" in child.tag for child in children): - return self.md.render(chat_message) - else: - chat_message = chat_message.replace("\n", "
") - return self.md.renderInline(chat_message) + 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/themes/base.py b/gradio/themes/base.py index c96ab7a2f1..85dba4f44b 100644 --- a/gradio/themes/base.py +++ b/gradio/themes/base.py @@ -568,6 +568,8 @@ class Base(ThemeClass): section_header_text_size=None, section_header_text_weight=None, # Component Atoms: These set the style for elements within components. + chatbot_code_background_color=None, + chatbot_code_background_color_dark=None, checkbox_background_color=None, checkbox_background_color_dark=None, checkbox_background_color_focus=None, @@ -800,6 +802,8 @@ class Base(ThemeClass): panel_border_width_dark: The border width of a panel in dark mode. section_header_text_size: The text size of a section header (e.g. tab name). section_header_text_weight: The text weight of a section header (e.g. tab name). + chatbot_code_background_color: The background color of code blocks in the chatbot. + chatbot_code_background_color_dark: The background color of code blocks in the chatbot in dark mode. checkbox_background_color: The background of a checkbox square or radio circle. checkbox_background_color_dark: The background of a checkbox square or radio circle in dark mode. checkbox_background_color_focus: The background of a checkbox square or radio circle when focused. @@ -1201,6 +1205,13 @@ class Base(ThemeClass): self, "section_header_text_weight", "400" ) # Component Atoms + self.chatbot_code_background_color = chatbot_code_background_color or getattr( + self, "chatbot_code_background_color", "*neutral_100" + ) + self.chatbot_code_background_color_dark = ( + chatbot_code_background_color_dark + or getattr(self, "chatbot_code_background_color_dark", "*neutral_800") + ) self.checkbox_background_color = checkbox_background_color or getattr( self, "checkbox_background_color", "*background_fill_primary" ) diff --git a/gradio/utils.py b/gradio/utils.py index 66c3d55910..cc35d1ce35 100644 --- a/gradio/utils.py +++ b/gradio/utils.py @@ -41,6 +41,9 @@ import httpx import matplotlib import requests from markdown_it import MarkdownIt +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_by_name 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 @@ -1007,6 +1010,16 @@ 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: + lexer = get_lexer_by_name("text") + formatter = HtmlFormatter() + + return highlight(code, lexer, formatter) + + def get_markdown_parser() -> MarkdownIt: md = ( MarkdownIt( @@ -1015,7 +1028,7 @@ def get_markdown_parser() -> MarkdownIt: "linkify": True, "typographer": True, "html": True, - "breaks": True, + "highlight": highlight_code, }, ) .use(dollarmath_plugin, renderer=tex2svg, allow_digits=False) diff --git a/gradio/version.txt b/gradio/version.txt index 54d1636c6c..2a5310b5a9 100644 --- a/gradio/version.txt +++ b/gradio/version.txt @@ -1 +1 @@ -3.28.1 +3.28.2 diff --git a/js/chatbot/src/ChatBot.svelte b/js/chatbot/src/ChatBot.svelte index 71c617f613..9437d27d35 100644 --- a/js/chatbot/src/ChatBot.svelte +++ b/js/chatbot/src/ChatBot.svelte @@ -2,6 +2,7 @@ import { beforeUpdate, afterUpdate, createEventDispatcher } from "svelte"; import type { Styles, SelectData } from "@gradio/utils"; import type { FileData } from "@gradio/upload"; + import "./manni.css"; export let value: Array< [string | FileData | null, string | FileData | null] @@ -36,6 +37,31 @@ }); }); } + 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); + }); + node.appendChild(button); + }); }); $: { @@ -56,6 +82,7 @@ {#if value !== null} {#each value as message_pair, i} {#each message_pair as message, j} +
- dispatch("select", { index: [i, j], value: message })} + dispatch("select", { + index: [i, j], + value: message + })} > {#if typeof message === "string"} {@html message} @@ -217,6 +247,13 @@ .dot-flashing:nth-child(3) { animation-delay: 0.66s; } + .message-wrap > div :global(.highlight) { + margin-top: var(--spacing-xs); + margin-bottom: var(--spacing-xs); + border-radius: var(--radius-md); + background: var(--chatbot-code-background-color); + padding-left: var(--spacing-xxl); + } /* Small screen */ @media (max-width: 480px) { diff --git a/js/chatbot/src/manni.css b/js/chatbot/src/manni.css new file mode 100644 index 0000000000..26739ff2b7 --- /dev/null +++ b/js/chatbot/src/manni.css @@ -0,0 +1,231 @@ +.highlight .hll { + background-color: #ffffcc; +} +.highlight .c { + color: #0099ff; + font-style: italic; +} /* Comment */ +.highlight .err { + background-color: #ffaaaa; + color: #aa0000; +} /* Error */ +.highlight .k { + color: #006699; + font-weight: bold; +} /* Keyword */ +.highlight .o { + color: #555555; +} /* Operator */ +.highlight .ch { + color: #0099ff; + font-style: italic; +} /* Comment.Hashbang */ +.highlight .cm { + color: #0099ff; + font-style: italic; +} /* Comment.Multiline */ +.highlight .cp { + color: #009999; +} /* Comment.Preproc */ +.highlight .cpf { + color: #0099ff; + font-style: italic; +} /* Comment.PreprocFile */ +.highlight .c1 { + color: #0099ff; + font-style: italic; +} /* Comment.Single */ +.highlight .cs { + color: #0099ff; + font-style: italic; + font-weight: bold; +} /* Comment.Special */ +.highlight .gd { + border: 1px solid #cc0000; + background-color: #ffcccc; +} /* Generic.Deleted */ +.highlight .ge { + font-style: italic; +} /* Generic.Emph */ +.highlight .gr { + color: #ff0000; +} /* Generic.Error */ +.highlight .gh { + color: #003300; + font-weight: bold; +} /* Generic.Heading */ +.highlight .gi { + border: 1px solid #00cc00; + background-color: #ccffcc; +} /* Generic.Inserted */ +.highlight .go { + color: #aaaaaa; +} /* Generic.Output */ +.highlight .gp { + color: #000099; + font-weight: bold; +} /* Generic.Prompt */ +.highlight .gs { + font-weight: bold; +} /* Generic.Strong */ +.highlight .gu { + color: #003300; + font-weight: bold; +} /* Generic.Subheading */ +.highlight .gt { + color: #99cc66; +} /* Generic.Traceback */ +.highlight .kc { + color: #006699; + font-weight: bold; +} /* Keyword.Constant */ +.highlight .kd { + color: #006699; + font-weight: bold; +} /* Keyword.Declaration */ +.highlight .kn { + color: #006699; + font-weight: bold; +} /* Keyword.Namespace */ +.highlight .kp { + color: #006699; +} /* Keyword.Pseudo */ +.highlight .kr { + color: #006699; + font-weight: bold; +} /* Keyword.Reserved */ +.highlight .kt { + color: #007788; + font-weight: bold; +} /* Keyword.Type */ +.highlight .m { + color: #ff6600; +} /* Literal.Number */ +.highlight .s { + color: #cc3300; +} /* Literal.String */ +.highlight .na { + color: #330099; +} /* Name.Attribute */ +.highlight .nb { + color: #336666; +} /* Name.Builtin */ +.highlight .nc { + color: #00aa88; + font-weight: bold; +} /* Name.Class */ +.highlight .no { + color: #336600; +} /* Name.Constant */ +.highlight .nd { + color: #9999ff; +} /* Name.Decorator */ +.highlight .ni { + color: #999999; + font-weight: bold; +} /* Name.Entity */ +.highlight .ne { + color: #cc0000; + font-weight: bold; +} /* Name.Exception */ +.highlight .nf { + color: #cc00ff; +} /* Name.Function */ +.highlight .nl { + color: #9999ff; +} /* Name.Label */ +.highlight .nn { + color: #00ccff; + font-weight: bold; +} /* Name.Namespace */ +.highlight .nt { + color: #330099; + font-weight: bold; +} /* Name.Tag */ +.highlight .nv { + color: #003333; +} /* Name.Variable */ +.highlight .ow { + color: #000000; + font-weight: bold; +} /* Operator.Word */ +.highlight .w { + color: #bbbbbb; +} /* Text.Whitespace */ +.highlight .mb { + color: #ff6600; +} /* Literal.Number.Bin */ +.highlight .mf { + color: #ff6600; +} /* Literal.Number.Float */ +.highlight .mh { + color: #ff6600; +} /* Literal.Number.Hex */ +.highlight .mi { + color: #ff6600; +} /* Literal.Number.Integer */ +.highlight .mo { + color: #ff6600; +} /* Literal.Number.Oct */ +.highlight .sa { + color: #cc3300; +} /* Literal.String.Affix */ +.highlight .sb { + color: #cc3300; +} /* Literal.String.Backtick */ +.highlight .sc { + color: #cc3300; +} /* Literal.String.Char */ +.highlight .dl { + color: #cc3300; +} /* Literal.String.Delimiter */ +.highlight .sd { + color: #cc3300; + font-style: italic; +} /* Literal.String.Doc */ +.highlight .s2 { + color: #cc3300; +} /* Literal.String.Double */ +.highlight .se { + color: #cc3300; + font-weight: bold; +} /* Literal.String.Escape */ +.highlight .sh { + color: #cc3300; +} /* Literal.String.Heredoc */ +.highlight .si { + color: #aa0000; +} /* Literal.String.Interpol */ +.highlight .sx { + color: #cc3300; +} /* Literal.String.Other */ +.highlight .sr { + color: #33aaaa; +} /* Literal.String.Regex */ +.highlight .s1 { + color: #cc3300; +} /* Literal.String.Single */ +.highlight .ss { + color: #ffcc33; +} /* Literal.String.Symbol */ +.highlight .bp { + color: #336666; +} /* Name.Builtin.Pseudo */ +.highlight .fm { + color: #cc00ff; +} /* Name.Function.Magic */ +.highlight .vc { + color: #003333; +} /* Name.Variable.Class */ +.highlight .vg { + color: #003333; +} /* Name.Variable.Global */ +.highlight .vi { + color: #003333; +} /* Name.Variable.Instance */ +.highlight .vm { + color: #003333; +} /* Name.Variable.Magic */ +.highlight .il { + color: #ff6600; +} /* Literal.Number.Integer.Long */ diff --git a/requirements.txt b/requirements.txt index bdf75ec5e9..4f1439c402 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ httpx huggingface_hub>=0.13.0 Jinja2 markdown-it-py[linkify]>=2.0.0 +pygments>=2.12.0 mdit-py-plugins<=0.3.3 markupsafe matplotlib diff --git a/test/test_components.py b/test/test_components.py index 2b0dde62d4..c8f99a76e6 100644 --- a/test/test_components.py +++ b/test/test_components.py @@ -1775,7 +1775,7 @@ class TestChatbot: """ chatbot = gr.Chatbot() assert chatbot.postprocess([["You are **cool**\nand fun", "so are *you*"]]) == [ - ["You are cool
and fun", "so are you"] + ["You are cool\nand fun", "so are you"] ] multimodal_msg = [