Skip to content

Commit

Permalink
Allow interactive input in gr.HighlightedText (#5400)
Browse files Browse the repository at this point in the history
* add preprocess param to highlighted_text.py

* add params

* static tweaks

* add interactive highlight container

* highlight selection logic

* allow editing label value and move shared funcs

* add changeset

* remove py code

* wait for input render

* remove redundant event listeners

* accessibility enhancements and remove label logic

* add keyboard navigation and interaction

* merge adjacent empty elements and split input element

* add interactive support for scores mode

* remove merge adjacent logic and move to frontend

* tweak

* add changeset

* format backend

* tweaks

* backend test tweaks

* set the interactive default to None

* BE tweak

* unit tests and stories

* be formatting

* fix label errors

* tweak

* fix tests

* fix tests

* fix test

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
  • Loading branch information
3 people committed Sep 13, 2023
1 parent 05715f5 commit d112e26
Show file tree
Hide file tree
Showing 13 changed files with 943 additions and 48 deletions.
7 changes: 7 additions & 0 deletions .changeset/ripe-ideas-rest.md
@@ -0,0 +1,7 @@
---
"@gradio/app": minor
"@gradio/highlightedtext": minor
"gradio": minor
---

feat:Allow interactive input in `gr.HighlightedText`
15 changes: 9 additions & 6 deletions gradio/components/highlighted_text.py
Expand Up @@ -11,11 +11,7 @@

from gradio.components.base import IOComponent, _Keywords
from gradio.deprecation import warn_style_method_deprecation
from gradio.events import (
Changeable,
EventListenerMethod,
Selectable,
)
from gradio.events import Changeable, EventListenerMethod, Selectable

set_documentation_group("component")

Expand All @@ -24,7 +20,7 @@
class HighlightedText(Changeable, Selectable, IOComponent, JSONSerializable):
"""
Displays text that contains spans that are highlighted by category or numerical value.
Preprocessing: this component does *not* accept input.
Preprocessing: passes a list of tuples as a {List[Tuple[str, float | str | None]]]} into the function. If no labels are provided, the text will be displayed as a single span.
Postprocessing: expects a {List[Tuple[str, float | str]]]} consisting of spans of text and their associated labels, or a {Dict} with two keys: (1) "text" whose value is the complete text, and (2) "entities", which is a list of dictionaries, each of which have the keys: "entity" (consisting of the entity label, can alternatively be called "entity_group"), "start" (the character index where the label starts), and "end" (the character index where the label ends). Entities should not overlap.
Demos: diff_texts, text_analysis
Expand All @@ -49,6 +45,7 @@ def __init__(
visible: bool = True,
elem_id: str | None = None,
elem_classes: list[str] | str | None = None,
interactive: bool | None = None,
**kwargs,
):
"""
Expand All @@ -66,6 +63,8 @@ def __init__(
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.
interactive: If True, the component will be editable, and allow user to select spans of text and label them.
"""
self.color_map = color_map
self.show_legend = show_legend
Expand All @@ -89,6 +88,7 @@ def __init__(
elem_id=elem_id,
elem_classes=elem_classes,
value=value,
interactive=interactive,
**kwargs,
)

Expand All @@ -98,6 +98,7 @@ def get_config(self):
"show_legend": self.show_legend,
"value": self.value,
"selectable": self.selectable,
"combine_adjacent": self.combine_adjacent,
**IOComponent.get_config(self),
}

Expand All @@ -115,6 +116,7 @@ def update(
scale: int | None = None,
min_width: int | None = None,
visible: bool | None = None,
interactive: bool | None = None,
):
updated_config = {
"color_map": color_map,
Expand All @@ -126,6 +128,7 @@ def update(
"min_width": min_width,
"visible": visible,
"value": value,
"interactive": interactive,
"__type__": "update",
}
return updated_config
Expand Down
3 changes: 2 additions & 1 deletion js/app/src/components/directory.ts
Expand Up @@ -62,7 +62,8 @@ export const component_map = {
static: () => import("@gradio/group/static")
},
highlightedtext: {
static: () => import("@gradio/highlightedtext/static")
static: () => import("@gradio/highlightedtext/static"),
interactive: () => import("@gradio/highlightedtext/interactive")
},
html: {
static: () => import("@gradio/html/static")
Expand Down
68 changes: 65 additions & 3 deletions js/highlightedtext/HighlightedText.stories.svelte
@@ -1,6 +1,6 @@
<script>
import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
import HighlightedText from "./static";
import HighlightedText from "./interactive/InteractiveHighlightedText.svelte";
import { Gradio } from "../app/src/gradio_helper";
</script>

Expand All @@ -11,7 +11,7 @@
value={[
["zebras", "+"],
["dogs", "-"],
["elephants", "+"]
["elephants", "+"],
]}
gradio={new Gradio(
0,
Expand All @@ -30,10 +30,72 @@
<Story
name="Highlighted Text with new lines"
args={{
value: [["zebras", "+"], ["\n"], ["dogs", "-"], ["\n"], ["elephants", "+"]]
value: [["zebras", "+"], ["\n"], ["dogs", "-"], ["\n"], ["elephants", "+"]],
}}
/>
<Story
name="Highlighted Text with color map"
args={{ color_map: { "+": "green", "-": "red" } }}
/>

<Story
name="Highlighted Text with combine adjacent"
args={{
value: [
["The", null],
["quick", "adjective"],
[" sneaky", "adjective"],
["fox", "subject"],
[" jumped ", "past tense verb"],
["over the", null],
["lazy dog", "object"],
],
combine_adjacent: true,
}}
/>

<Story
name="Highlighted Text without combine adjacent"
args={{
value: [
["The", null],
["quick", "adjective"],
[" sneaky", "adjective"],
["fox", "subject"],
[" jumped ", "past tense verb"],
["over the", null],
["lazy dog", "object"],
],
}}
/>

<Story
name="Highlighted Text with combine adjacent and new lines"
args={{
value: [
["The", null],
["quick", "adjective"],
[" sneaky", "adjective"],
["fox", "subject"],
["\n"],
["jumped", "past tense verb"],
["\n"],
["over the", null],
["lazy dog", "object"],
],
combine_adjacent: true,
}}
/>

<Story
name="Highlighted Text in scores mode"
args={{
value: [
["the", -1],
["quick", 1],
["fox", 0.3],
],

show_legend: true,
}}
/>
75 changes: 75 additions & 0 deletions js/highlightedtext/highlightedtext.test.ts
@@ -0,0 +1,75 @@
import { test, describe, assert, afterEach } from "vitest";
import { cleanup, fireEvent, render } from "@gradio/tootils";
import { setupi18n } from "../app/src/i18n";

import HighlightedText from "./interactive";
import type { LoadingStatus } from "@gradio/statustracker";

const loading_status: LoadingStatus = {
eta: 0,
queue_position: 1,
queue_size: 1,
status: "complete" as LoadingStatus["status"],
scroll_to_output: false,
visible: true,
fn_index: 0,
show_progress: "full"
};

describe("HighlightedText", () => {
afterEach(() => cleanup());

setupi18n();

test("renders provided text and labels", async () => {
const { getByText, getByTestId, getAllByText } = await render(
HighlightedText,
{
loading_status,
value: [
["The", null],
["quick", "adjective"],
[" sneaky", "adjective"],
["fox", "subject"],
[" jumped ", "past tense verb"],
["over the", null],
["lazy dog", "object"]
]
}
);

const quick = getByText("quick");
const adjectiveLabels = getAllByText("adjective");

assert.exists(quick);
assert.exists(adjectiveLabels);
assert.equal(adjectiveLabels.length, 2);
});

test("renders labels with remove label buttons which trigger change", async () => {
const { getAllByText, listen } = await render(HighlightedText, {
loading_status,
value: [
["The", null],
["quick", "adjective"],
[" sneaky", "adjective"],
["fox", "subject"],
[" jumped ", "past tense verb"],
["over the", null],
["lazy dog", "object"]
]
});

const mock = listen("change");

const removeButtons = getAllByText("脳");

assert.equal(removeButtons.length, 5);

assert.equal(mock.callCount, 0);

fireEvent.click(removeButtons[0]);

assert.equal(mock.callCount, 1);
});
});

0 comments on commit d112e26

Please sign in to comment.