From 9fff1e0fe8c60ed27b7693aee63afd7cc9279f1a Mon Sep 17 00:00:00 2001 From: Dawood Khan Date: Wed, 4 Jan 2023 19:13:46 -0500 Subject: [PATCH] Enable multi-select on gradio.Dropdown (#2871) * multiselect dropdown * fixes * more fixes * changes * changelog * formatting * format notebooks * type fixes * notebok fix * remove console log * notebook fix * type fix * Revert "format notebooks" This reverts commit fb8762ecffbc727425d435262e60be4ea7feec6e. * notebook fix * bug fixes * Update CHANGELOG.md * Excluding untracked files from demo notebook check action (#2897) * excluding untracked files from wget * changelog * fix setting default values * typeability and arrow key support * python types * reformat * another type check * minor fixes + interactive false fix * change remove token styling * separate multiselect into separate file * style fixes * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * some more style fixes * small bug fix * addressed pr comments * fix active color highlighting Co-authored-by: Ali Abdalla Co-authored-by: Abubakar Abid --- CHANGELOG.md | 11 +- demo/sentence_builder/run.ipynb | 2 +- demo/sentence_builder/run.py | 2 +- gradio/components.py | 110 +++++++++- test/test_components.py | 50 +++++ .../app/src/components/Column/Column.svelte | 2 +- .../src/components/Dropdown/Dropdown.svelte | 4 +- .../app/src/components/Form/Form.svelte | 2 +- ui/packages/atoms/src/Block.svelte | 6 +- ui/packages/form/src/Dropdown.svelte | 41 ++-- ui/packages/form/src/MultiSelect.svelte | 195 ++++++++++++++++++ 11 files changed, 391 insertions(+), 34 deletions(-) create mode 100644 ui/packages/form/src/MultiSelect.svelte diff --git a/CHANGELOG.md b/CHANGELOG.md index c02b2a2fcf..ce8b488e25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## New Features: -* Send custom progress updates by adding a `gr.Progress` argument after the input arguments to any function. Example: +### Send custom progress updates by adding a `gr.Progress` argument after the input arguments to any function. Example: ```python def reverse(word, progress=gr.Progress()): @@ -21,6 +21,15 @@ Progress indicator bar by [@aliabid94](https://github.com/aliabid94) in [PR 2750 * Added `title` argument to `TabbedInterface` by @MohamedAliRashad in [#2888](https://github.com/gradio-app/gradio/pull/2888) * Add support for specifying file extensions for `gr.File` and `gr.UploadButton`, using `file_types` parameter (e.g `gr.File(file_count="multiple", file_types=["text", ".json", ".csv"])`) by @dawoodkhan82 in [#2901](https://github.com/gradio-app/gradio/pull/2901) +* Added `multiselect` option to `Dropdown` by @dawoodkhan82 in [#2871](https://github.com/gradio-app/gradio/pull/2871) + +### With `multiselect` set to `true` a user can now select multiple options from the `gr.Dropdown` component. + +```python +gr.Dropdown(["angola", "pakistan", "canada"], multiselect=True, value=["angola"]) +``` +Screenshot 2023-01-03 at 4 14 36 PM + ## Bug Fixes: * Fixed bug where an error opening an audio file led to a crash by [@FelixDombek](https://github.com/FelixDombek) in [PR 2898](https://github.com/gradio-app/gradio/pull/2898) diff --git a/demo/sentence_builder/run.ipynb b/demo/sentence_builder/run.ipynb index c292e91615..1f42db8294 100644 --- a/demo/sentence_builder/run.ipynb +++ b/demo/sentence_builder/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: sentence_builder"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "\n", "def sentence_builder(quantity, animal, place, activity_list, morning):\n", " return f\"\"\"The {quantity} {animal}s went to the {place} where they {\" and \".join(activity_list)} until the {\"morning\" if morning else \"night\"}\"\"\"\n", "\n", "\n", "demo = gr.Interface(\n", " sentence_builder,\n", " [\n", " gr.Slider(2, 20, value=4),\n", " gr.Dropdown([\"cat\", \"dog\", \"bird\"]),\n", " gr.Radio([\"park\", \"zoo\", \"road\"]),\n", " gr.CheckboxGroup([\"ran\", \"swam\", \"ate\", \"slept\"]),\n", " gr.Checkbox(label=\"Is it the morning?\"),\n", " ],\n", " \"text\",\n", " examples=[\n", " [2, \"cat\", \"park\", [\"ran\", \"swam\"], True],\n", " [4, \"dog\", \"zoo\", [\"ate\", \"swam\"], False],\n", " [10, \"bird\", \"road\", [\"ran\"], False],\n", " [8, \"cat\", \"zoo\", [\"ate\"], True],\n", " ],\n", ")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: sentence_builder"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "\n", "def sentence_builder(quantity, animal, place, activity_list, morning):\n", " return f\"\"\"The {quantity} {animal}s went to the {place} where they {\" and \".join(activity_list)} until the {\"morning\" if morning else \"night\"}\"\"\"\n", "\n", "\n", "demo = gr.Interface(\n", " sentence_builder,\n", " [\n", " gr.Slider(2, 20, value=4),\n", " gr.Dropdown([\"cat\", \"dog\", \"bird\"]),\n", " gr.Radio([\"park\", \"zoo\", \"road\"]),\n", " gr.Dropdown([\"ran\", \"swam\", \"ate\", \"slept\"], value=[\"swam\", \"slept\"], multiselect=True),\n", " gr.Checkbox(label=\"Is it the morning?\"),\n", " ],\n", " \"text\",\n", " examples=[\n", " [2, \"cat\", \"park\", [\"ran\", \"swam\"], True],\n", " [4, \"dog\", \"zoo\", [\"ate\", \"swam\"], False],\n", " [10, \"bird\", \"road\", [\"ran\"], False],\n", " [8, \"cat\", \"zoo\", [\"ate\"], True],\n", " ],\n", ")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/sentence_builder/run.py b/demo/sentence_builder/run.py index 2b1385deca..36b49ba772 100644 --- a/demo/sentence_builder/run.py +++ b/demo/sentence_builder/run.py @@ -11,7 +11,7 @@ demo = gr.Interface( gr.Slider(2, 20, value=4), gr.Dropdown(["cat", "dog", "bird"]), gr.Radio(["park", "zoo", "road"]), - gr.CheckboxGroup(["ran", "swam", "ate", "slept"]), + gr.Dropdown(["ran", "swam", "ate", "slept"], value=["swam", "slept"], multiselect=True), gr.Checkbox(label="Is it the morning?"), ], "text", diff --git a/gradio/components.py b/gradio/components.py index ac31d12ae3..eb814a97f4 100644 --- a/gradio/components.py +++ b/gradio/components.py @@ -1167,9 +1167,9 @@ class Radio( @document("change", "style") -class Dropdown(Radio): +class Dropdown(Changeable, IOComponent, SimpleSerializable, FormComponent): """ - Creates a dropdown of which only one entry can be selected. + Creates a dropdown of choices from which entries can be selected. Preprocessing: passes the value of the selected dropdown entry as a {str} or its index as an {int} into the function, depending on `type`. Postprocessing: expects a {str} corresponding to the value of the dropdown entry to be selected. Examples-format: a {str} representing the drop down value to select. @@ -1178,10 +1178,11 @@ class Dropdown(Radio): def __init__( self, - choices: List[str] | None = None, + choices: str | List[str] | None = None, *, - value: str | Callable | None = None, + value: str | List[str] | Callable | None = None, type: str = "value", + multiselect: bool | None = None, label: str | None = None, every: float | None = None, show_label: bool = True, @@ -1193,8 +1194,9 @@ class Dropdown(Radio): """ Parameters: choices: list of options to select from. - value: default value selected in dropdown. If None, no value is selected by default. If callable, the function will be called whenever the app loads to set the initial value of the component. + value: default value(s) selected in dropdown. If None, no value is selected by default. If callable, the function will be called whenever the app loads to set the initial value of the component. type: Type of value to be returned by component. "value" returns the string of the choice selected, "index" returns the index of the choice selected. + multiselect: if True, multiple choices can be selected. 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. @@ -1202,19 +1204,109 @@ class Dropdown(Radio): 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. """ - Radio.__init__( + self.choices = choices or [] + valid_types = ["value", "index"] + if type not in valid_types: + raise ValueError( + f"Invalid value for parameter `type`: {type}. Please choose from one of: {valid_types}" + ) + self.type = type + self.multiselect = multiselect + if multiselect: + if isinstance(value, str): + value = [value] + self.test_input = self.choices[0] if len(self.choices) else None + self.interpret_by_tokens = False + IOComponent.__init__( self, - value=value, - choices=choices, - type=type, label=label, every=every, show_label=show_label, interactive=interactive, visible=visible, elem_id=elem_id, + value=value, **kwargs, ) + self.cleared_value = self.value + + def get_config(self): + return { + "choices": self.choices, + "value": self.value, + "multiselect": self.multiselect, + **IOComponent.get_config(self), + } + + @staticmethod + def update( + value: Any | Literal[_Keywords.NO_VALUE] | None = _Keywords.NO_VALUE, + choices: str | List[str] | None = None, + label: str | None = None, + show_label: bool | None = None, + interactive: bool | None = None, + visible: bool | None = None, + ): + updated_config = { + "choices": choices, + "label": label, + "show_label": show_label, + "interactive": interactive, + "visible": visible, + "value": value, + "__type__": "update", + } + return IOComponent.add_interactive_to_config(updated_config, interactive) + + def generate_sample(self): + return self.choices[0] + + def preprocess( + self, x: str | List[str] + ) -> str | int | List[str] | List[int] | None: + """ + Parameters: + x: selected choice(s) + Returns: + selected choice(s) as string or index within choice list or list of string or indices + """ + if self.type == "value": + return x + elif self.type == "index": + if x is None: + return None + elif self.multiselect: + return [self.choices.index(c) for c in x] + else: + if isinstance(x, str): + return self.choices.index(x) + else: + raise ValueError( + "Unknown type: " + + str(self.type) + + ". Please choose from: 'value', 'index'." + ) + + def set_interpret_parameters(self): + """ + Calculates interpretation score of each choice by comparing the output against each of the outputs when alternative choices are selected. + """ + return self + + def get_interpretation_neighbors(self, x): + choices = list(self.choices) + choices.remove(x) + return choices, {} + + def get_interpretation_scores( + self, x, neighbors, scores: List[float | None], **kwargs + ) -> List: + """ + Returns: + Each value represents the interpretation score corresponding to each choice. + """ + scores.insert(self.choices.index(x), None) + return scores def style(self, *, container: bool | None = None, **kwargs): """ diff --git a/test/test_components.py b/test/test_components.py index a91fae8fc0..a5d49dbdbf 100644 --- a/test/test_components.py +++ b/test/test_components.py @@ -554,6 +554,56 @@ class TestRadio: assert scores == [-2.0, None, 2.0] +class TestDropdown: + def test_component_functions(self): + """ + Preprocess, postprocess, serialize, generate_sample, get_config + """ + dropdown_input = gr.Dropdown(["a", "b", "c"], multiselect=True) + assert dropdown_input.preprocess("a") == "a" + assert dropdown_input.postprocess("a") == "a" + + dropdown_input_multiselect = gr.Dropdown(["a", "b", "c"], multiselect=True) + assert dropdown_input_multiselect.preprocess(["a", "c"]) == ["a", "c"] + assert dropdown_input_multiselect.postprocess(["a", "c"]) == ["a", "c"] + assert dropdown_input_multiselect.serialize(["a", "c"], True) == ["a", "c"] + assert isinstance(dropdown_input_multiselect.generate_sample(), str) + dropdown_input_multiselect = gr.Dropdown( + value=["a", "c"], + choices=["a", "b", "c"], + label="Select Your Inputs", + ) + assert dropdown_input_multiselect.get_config() == { + "choices": ["a", "b", "c"], + "value": ["a", "c"], + "name": "dropdown", + "show_label": True, + "label": "Select Your Inputs", + "style": {}, + "elem_id": None, + "visible": True, + "interactive": None, + "root_url": None, + "multiselect": None, + } + with pytest.raises(ValueError): + gr.Dropdown(["a"], type="unknown") + + dropdown = gr.Dropdown(choices=["a", "b"], value="c") + assert dropdown.get_config()["value"] == "c" + assert dropdown.postprocess("a") == "a" + + def test_in_interface(self): + """ + Interface, process + """ + checkboxes_input = gr.CheckboxGroup(["a", "b", "c"]) + iface = gr.Interface(lambda x: "|".join(x), checkboxes_input, "textbox") + assert iface(["a", "c"]) == "a|c" + assert iface([]) == "" + _ = gr.CheckboxGroup(["a", "b", "c"], type="index") + + class TestImage: def test_component_functions(self): """ diff --git a/ui/packages/app/src/components/Column/Column.svelte b/ui/packages/app/src/components/Column/Column.svelte index 9cca0efedf..9faf8d90b9 100644 --- a/ui/packages/app/src/components/Column/Column.svelte +++ b/ui/packages/app/src/components/Column/Column.svelte @@ -12,7 +12,7 @@
= []; + export let multiselect: boolean = false; export let choices: Array; export let show_label: boolean; export let style: Styles = {}; @@ -27,6 +28,7 @@ diff --git a/ui/packages/atoms/src/Block.svelte b/ui/packages/atoms/src/Block.svelte index 168cb63ebd..4af24a4d06 100644 --- a/ui/packages/atoms/src/Block.svelte +++ b/ui/packages/atoms/src/Block.svelte @@ -45,9 +45,9 @@ data-testid={test_id} id={elem_id} class:!hidden={visible === false} - class="gr-block gr-box relative w-full overflow-hidden {styles[ - variant - ]} {styles[color]} {classes}" + class="gr-block gr-box relative w-full {styles[variant]} {styles[ + color + ]} {classes}" class:gr-padded={padding} style={size_style || null} > diff --git a/ui/packages/form/src/Dropdown.svelte b/ui/packages/form/src/Dropdown.svelte index 96a1fc1b82..b0b67c9b2c 100644 --- a/ui/packages/form/src/Dropdown.svelte +++ b/ui/packages/form/src/Dropdown.svelte @@ -1,27 +1,36 @@ + diff --git a/ui/packages/form/src/MultiSelect.svelte b/ui/packages/form/src/MultiSelect.svelte new file mode 100644 index 0000000000..212c336674 --- /dev/null +++ b/ui/packages/form/src/MultiSelect.svelte @@ -0,0 +1,195 @@ + + +
+
+ {#if Array.isArray(value)} + {#each value as s} +
+
+ + + +
+ {s} +
+ {/each} + {/if} +
+ +
+ + + +
+ +
+
+ + {#if showOptions && !disabled} +
    + {#each filtered as choice} +
  • + + {choice} +
  • + {/each} +
+ {/if} +