Add RTL support to Textbox, Markdown, Chatbot (#4933)

* add rtl support

* redo

* remove param

* clog

* fix backend tests; add textbox story

* add textbox story

* fix textbox story

* fixed textbox story, markdown

* markdown story

* format

* fixes'

* Update CHANGELOG.md

* update docstrings

* fix tests

* fix static checks

* fix tests
This commit is contained in:
Abubakar Abid 2023-07-17 12:53:23 -04:00 committed by GitHub
parent 7f6d0e19ad
commit b16732ffb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 248 additions and 20 deletions

View File

@ -5,5 +5,5 @@ export default {
},
testMatch: /.*.spec.ts/,
testDir: "..",
globalSetup: "./playwright-setup.js",
globalSetup: "./playwright-setup.js"
};

View File

@ -3,6 +3,21 @@
## New Features:
- Chatbot messages now show hyperlinks to download files uploaded to `gr.Chatbot()` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4848](https://github.com/gradio-app/gradio/pull/4848)
- Cached examples now work with generators and async generators by [@abidlabs](https://github.com/abidlabs) in [PR 4927](https://github.com/gradio-app/gradio/pull/4927)
- Add RTL support to `gr.Markdown`, `gr.Chatbot`, `gr.Textbox` (via the `rtl` boolean parameter) and text-alignment to `gr.Textbox`(via the string `text_align` parameter) by [@abidlabs](https://github.com/abidlabs) in [PR 4933](https://github.com/gradio-app/gradio/pull/4933)
Examples of usage:
```py
with gr.Blocks() as demo:
gr.Textbox(interactive=True, text_align="right")
demo.launch()
```
```py
with gr.Blocks() as demo:
gr.Markdown("سلام", rtl=True)
demo.launch()
```
## Bug Fixes:

View File

@ -51,12 +51,14 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable):
elem_classes: list[str] | str | None = None,
height: int | None = None,
latex_delimiters: list[dict[str, str | bool]] | None = None,
rtl: bool = False,
show_share_button: bool | None = None,
**kwargs,
):
"""
Parameters:
value: Default value to show in chatbot. If callable, the function will be called whenever the app loads to set the initial value of the component.
color_map: This parameter is deprecated.
label: component name in interface.
every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. Queue must be enabled. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.
show_label: if True, will display label.
@ -68,6 +70,7 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable):
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
height: height of the component in pixels.
latex_delimiters: A list of dicts of the form {"left": open delimiter (str), "right": close delimiter (str), "display": whether to display in newline (bool)} that will be used to render LaTeX expressions. If not provided, `latex_delimiters` is set to `[{ "left": "$$", "right": "$$", "display": True }]`, so only expressions enclosed in $$ delimiters will be rendered as LaTeX, and in a new line. Pass in an empty list to disable LaTeX rendering. For more information, see the [KaTeX documentation](https://katex.org/docs/autorender.html).
rtl: If True, sets the direction of the rendered text to right-to-left. Default is False, which renders text left-to-right.
show_share_button: If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.
"""
if color_map is not None:
@ -79,6 +82,7 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable):
See EventData documentation on how to use this event data.
"""
self.height = height
self.rtl = rtl
if latex_delimiters is None:
latex_delimiters = [{"left": "$$", "right": "$$", "display": True}]
self.latex_delimiters = latex_delimiters
@ -110,6 +114,7 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable):
"selectable": self.selectable,
"height": self.height,
"show_share_button": self.show_share_button,
"rtl": self.rtl,
**IOComponent.get_config(self),
}
@ -125,6 +130,7 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable):
min_width: int | None = None,
visible: bool | None = None,
height: int | None = None,
rtl: bool | None = None,
show_share_button: bool | None = None,
):
updated_config = {
@ -137,6 +143,7 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable):
"value": value,
"height": height,
"show_share_button": show_share_button,
"rtl": rtl,
"__type__": "update",
}
return updated_config

View File

@ -35,6 +35,7 @@ class Markdown(IOComponent, Changeable, StringSerializable):
visible: bool = True,
elem_id: str | None = None,
elem_classes: list[str] | str | None = None,
rtl: bool = False,
**kwargs,
):
"""
@ -43,8 +44,10 @@ class Markdown(IOComponent, Changeable, StringSerializable):
visible: If False, component will be hidden.
elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
rtl: If True, sets the direction of the rendered text to right-to-left. Default is False, which renders text left-to-right.
"""
self.md = utils.get_markdown_parser()
self.rtl = rtl
IOComponent.__init__(
self,
visible=visible,
@ -69,6 +72,7 @@ class Markdown(IOComponent, Changeable, StringSerializable):
def get_config(self):
return {
"value": self.value,
"rtl": self.rtl,
**Component.get_config(self),
}
@ -76,10 +80,12 @@ class Markdown(IOComponent, Changeable, StringSerializable):
def update(
value: Any | Literal[_Keywords.NO_VALUE] | None = _Keywords.NO_VALUE,
visible: bool | None = None,
rtl: bool | None = None,
):
updated_config = {
"visible": visible,
"value": value,
"rtl": rtl,
"__type__": "update",
}
return updated_config

View File

@ -68,6 +68,8 @@ class Textbox(
elem_id: str | None = None,
elem_classes: list[str] | str | None = None,
type: Literal["text", "password", "email"] = "text",
text_align: Literal["left", "right"] | None = None,
rtl: bool = False,
show_copy_button: bool = False,
**kwargs,
):
@ -89,6 +91,8 @@ class Textbox(
elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
type: The type of textbox. One of: 'text', 'password', 'email', Default is 'text'.
text_align: How to align the text in the textbox, can be: "left", "right", or None (default). If None, the alignment is left if `rtl` is False, or right if `rtl` is True. Can only be changed if `type` is "text".
rtl: If True and `type` is "text", sets the direction of the text to right-to-left (cursor appears on the left of the text). Default is False, which renders cursor on the right.
show_copy_button: If True, includes a copy button to copy the text in the textbox. Only applies if show_label is True.
"""
if type not in ["text", "password", "email"]:
@ -125,6 +129,8 @@ class Textbox(
)
TokenInterpretable.__init__(self)
self.type = type
self.rtl = rtl
self.text_align = text_align
def get_config(self):
return {
@ -134,6 +140,8 @@ class Textbox(
"value": self.value,
"type": self.type,
"show_copy_button": self.show_copy_button,
"text_align": self.text_align,
"rtl": self.rtl,
**IOComponent.get_config(self),
}
@ -152,6 +160,8 @@ class Textbox(
visible: bool | None = None,
interactive: bool | None = None,
type: Literal["text", "password", "email"] | None = None,
text_align: Literal["left", "right"] | None = None,
rtl: bool | None = None,
show_copy_button: bool | None = None,
):
return {
@ -169,6 +179,8 @@ class Textbox(
"type": type,
"interactive": interactive,
"show_copy_button": show_copy_button,
"text_align": text_align,
"rtl": rtl,
"__type__": "update",
}

View File

@ -11,6 +11,7 @@ XRAY_CONFIG = {
"value": "<h1>Detect Disease From Scan</h1>\n<p>With this model you can lorem ipsum</p>\n<ul>\n<li>ipsum 1</li>\n<li>ipsum 2</li>\n</ul>\n",
"name": "markdown",
"visible": True,
"rtl": False,
},
"serializer": "StringSerializable",
"api_info": {"info": {"type": "string"}, "serialized_info": False},
@ -195,6 +196,7 @@ XRAY_CONFIG = {
"name": "textbox",
"show_copy_button": False,
"visible": True,
"rtl": False,
},
"serializer": "StringSerializable",
"api_info": {"info": {"type": "string"}, "serialized_info": False},
@ -327,6 +329,7 @@ XRAY_CONFIG_DIFF_IDS = {
"value": "<h1>Detect Disease From Scan</h1>\n<p>With this model you can lorem ipsum</p>\n<ul>\n<li>ipsum 1</li>\n<li>ipsum 2</li>\n</ul>\n",
"name": "markdown",
"visible": True,
"rtl": False,
},
"serializer": "StringSerializable",
"api_info": {"info": {"type": "string"}, "serialized_info": False},
@ -511,6 +514,7 @@ XRAY_CONFIG_DIFF_IDS = {
"name": "textbox",
"show_copy_button": False,
"visible": True,
"rtl": False,
},
"serializer": "StringSerializable",
"api_info": {"info": {"type": "string"}, "serialized_info": False},
@ -643,6 +647,7 @@ XRAY_CONFIG_WITH_MISTAKE = {
"props": {
"value": "<h1>Detect Disease From Scan</h1>\n<p>With this model you can lorem ipsum</p>\n<ul>\n<li>ipsum 1</li>\n<li>ipsum 2</li>\n</ul>\n",
"name": "markdown",
"rtl": False,
},
},
{
@ -774,6 +779,7 @@ XRAY_CONFIG_WITH_MISTAKE = {
"name": "textbox",
"show_copy_button": False,
"type": "text",
"rtl": False,
},
},
],

View File

@ -0,0 +1,34 @@
<script>
import {Meta, Template, Story } from "@storybook/addon-svelte-csf";
import Markdown from "./Markdown.svelte";
</script>
<Meta
title="Components/Markdown"
component={Markdown}
argTypes={{
rtl: {
options: [true, false],
description: "Whether to render right-to-left",
control: { type: "boolean" },
defaultValue: false
},
}}
/>
<Template let:args>
<Markdown
{...args}
value="Here's some <strong>bold</strong> text. And some <em>italics</em> and some <code>code</code>"
/>
</Template>
<Story
name="Simple inline Markdown with HTML"
/>
<Story
name="Right aligned Markdown with HTML"
args={{ rtl: true }}
/>

View File

@ -11,6 +11,7 @@
export let visible: boolean = true;
export let value: string = "";
export let loading_status: LoadingStatus;
export let rtl = false;
const dispatch = createEventDispatcher<{ change: undefined }>();
@ -26,6 +27,7 @@
{elem_id}
{elem_classes}
{visible}
{rtl}
on:change
/>
</div>

View File

@ -0,0 +1,82 @@
<script>
import {Meta, Template, Story } from "@storybook/addon-svelte-csf";
import Textbox from "./Textbox.svelte";
</script>
<Meta
title="Components/Textbox"
component={Textbox}
argTypes={{
label: {
control: "text",
description: "The textbox label",
name: "label",
},
show_label: {
options: [true, false],
description: "Whether to show the label",
control: { type: "boolean" },
defaultValue: true
},
type: {
options: ["text", "email", "password"],
description: "The type of textbox",
control: { type: "select" },
defaultValue: "text"
},
text_align: {
options: ["left", "right"],
description: "Whether to align the text left or right",
control: { type: "select" },
defaultValue: "left"
},
lines: {
options: [1, 5, 10, 20],
description: "The number of lines to display in the textbox",
control: { type: "select" },
defaultValue: 1
},
max_lines: {
options: [1, 5, 10, 20],
description: "The maximum number of lines to allow users to type in the textbox",
control: { type: "select" },
defaultValue: 1
},
rtl: {
options: [true, false],
description: "Whether to render right-to-left",
control: { type: "boolean" },
defaultValue: false
},
}}
/>
<Template let:args>
<Textbox
{...args}
value="hello world"
/>
</Template>
<Story
name="Textbox with label"
args={{ label: "My simple label", show_label: true }}
/>
<Story
name="Textbox with 5 lines and max 5 lines"
args={{ lines: 5, max_lines: 5 }}
/>
<Story
name="Password input"
args={{ type: "password", lines: 1, max_lines: 1 }}
/>
<Story
name="Right aligned textbox"
args={{ text_align: "right" }}
/>
<Story
name="RTL textbox"
args={{ rtl: true }}
/>

View File

@ -24,6 +24,8 @@
export let loading_status: LoadingStatus | undefined = undefined;
export let mode: "static" | "dynamic";
export let value_is_output: boolean = false;
export let rtl = false;
export let text_align: "left" | "right" | undefined = undefined;
</script>
<Block {visible} {elem_id} {elem_classes} {scale} {min_width} {container}>
@ -39,6 +41,8 @@
{show_label}
{lines}
{type}
{rtl}
{text_align}
max_lines={!max_lines && mode === "static" ? lines + 1 : max_lines}
{placeholder}
{show_copy_button}

View File

@ -0,0 +1,51 @@
<script>
import {Meta, Template, Story } from "@storybook/addon-svelte-csf";
import Chatbot from "./Index.svelte";
</script>
<Meta
title="Components/Chatbot"
component={Chatbot}
argTypes={{
label: {
control: "text",
description: "The textbox label",
name: "label",
},
show_label: {
options: [true, false],
description: "Whether to show the label",
control: { type: "boolean" },
defaultValue: true
},
rtl: {
options: [true, false],
description: "Whether to render right-to-left",
control: { type: "boolean" },
defaultValue: false
},
}}
/>
<Template let:args>
<Chatbot
{...args}
value={[["Can you write a function in Python?", "```py\ndef test():\n\tprint(x)\n```"], ["Can you do math?", "$$1+1=2$$"]]}
/>
</Template>
<Story
name="Chatbot with math enabled"
args={{ latex_delimiters: [{ "left": "$$", "right": "$$", "display": true }] }}
/>
<Story
name="Chatbot with math disabled, small height"
args={{ latex_delimiters: [], height: 200 }}
/>
<Story
name="Chatbot with text rendered right-to-left"
args={{ rtl: true, latex_delimiters: [{ "left": "$$", "right": "$$", "display": true }]}}
/>

View File

@ -28,6 +28,7 @@
export let selectable = false;
export let theme_mode: ThemeMode;
export let show_share_button = false;
export let rtl = false;
const redirect_src_url = (src: string): string =>
src.replace('src="/file', `src="${root}file`);
@ -82,6 +83,7 @@
value={_value}
{latex_delimiters}
pending_message={loading_status?.status === "pending"}
{rtl}
on:change
on:select
on:share

View File

@ -25,6 +25,7 @@
export let selectable = false;
export let show_share_button = false;
export let theme_mode: ThemeMode;
export let rtl = false;
$: if (theme_mode == "dark") {
code_highlight_css.dark();
@ -103,7 +104,8 @@
class:hide={message === null}
class:selectable
on:click={() => handle_select(i, j, message)}
>
dir={rtl ? "rtl" : "ltr"}
>
{#if typeof message === "string"}
<Markdown {message} {latex_delimiters} on:load={scroll} />
{#if feedback && j == 1}
@ -403,4 +405,5 @@
top: 6px;
right: 6px;
}
</style>

View File

@ -16,6 +16,8 @@
export let max_lines: number;
export let type: "text" | "password" | "email" = "text";
export let show_copy_button: boolean = false;
export let rtl = false;
export let text_align: "left" | "right" | undefined = undefined;
let el: HTMLTextAreaElement | HTMLInputElement;
let copied = false;
@ -142,6 +144,7 @@
data-testid="textbox"
type="text"
class="scroll-hide"
dir={rtl ? "rtl" : "ltr"}
bind:value
bind:this={el}
{placeholder}
@ -149,7 +152,8 @@
on:keypress={handle_keypress}
on:blur={handle_blur}
on:select={handle_select}
/>
style={text_align ? "text-align: " + text_align : ""}
/>
{:else if type === "password"}
<input
data-testid="password"
@ -191,6 +195,7 @@
data-testid="textbox"
use:text_area_resize={value}
class="scroll-hide"
dir={rtl ? "rtl" : "ltr"}
bind:value
bind:this={el}
{placeholder}
@ -199,7 +204,8 @@
on:keypress={handle_keypress}
on:blur={handle_blur}
on:select={handle_select}
/>
style={text_align ? "text-align: " + text_align : ""}
/>
{/if}
</label>

View File

@ -5,6 +5,7 @@
export let visible: boolean = true;
export let value: string;
export let min_height = false;
export let rtl = false;
const dispatch = createEventDispatcher<{ change: undefined }>();
@ -20,6 +21,7 @@
class:hide={!visible}
bind:this={target}
data-testid="markdown"
dir={rtl ? "rtl" : "ltr"}
>
{@html value}
</div>
@ -48,4 +50,5 @@
.hide {
display: none;
}
</style>

View File

@ -1,4 +1,4 @@
lockfileVersion: '6.0'
lockfileVersion: '6.1'
settings:
autoInstallPeers: true
@ -5289,7 +5289,7 @@ packages:
ts-dedent: 2.2.0
util-deprecate: 1.0.2
watchpack: 2.4.0
ws: 8.13.0
ws: 8.13.0(bufferutil@4.0.7)
transitivePeerDependencies:
- bufferutil
- encoding
@ -5763,7 +5763,7 @@ packages:
peerDependencies:
'@sveltejs/kit': ^1.0.0
dependencies:
'@sveltejs/kit': 1.16.3(svelte@3.59.2)(vite@4.3.9)
'@sveltejs/kit': 1.16.3(svelte@3.57.0)(vite@4.3.5)
import-meta-resolve: 3.0.0
dev: true
@ -10231,7 +10231,7 @@ packages:
whatwg-encoding: 2.0.0
whatwg-mimetype: 3.0.0
whatwg-url: 12.0.1
ws: 8.13.0
ws: 8.13.0(bufferutil@4.0.7)
xml-name-validator: 4.0.0
transitivePeerDependencies:
- bufferutil
@ -14738,18 +14738,6 @@ packages:
async-limiter: 1.0.1
dev: true
/ws@8.13.0:
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
/ws@8.13.0(bufferutil@4.0.7):
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
engines: {node: '>=10.0.0'}

View File

@ -1076,6 +1076,8 @@ class TestSpecificUpdate:
"type": None,
"interactive": False,
"show_copy_button": None,
"rtl": None,
"text_align": None,
"__type__": "update",
}
@ -1097,6 +1099,8 @@ class TestSpecificUpdate:
"type": None,
"interactive": True,
"show_copy_button": None,
"rtl": None,
"text_align": None,
"__type__": "update",
}

View File

@ -103,6 +103,8 @@ class TestTextbox:
"visible": True,
"interactive": None,
"root_url": None,
"rtl": False,
"text_align": None,
}
@pytest.mark.asyncio
@ -1976,6 +1978,7 @@ class TestChatbot:
"root_url": None,
"selectable": False,
"latex_delimiters": [{"display": True, "left": "$$", "right": "$$"}],
"rtl": False,
}