Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add event listener support to render blocks #8243

Merged
merged 131 commits into from May 22, 2024
Merged

Add event listener support to render blocks #8243

merged 131 commits into from May 22, 2024

Conversation

aliabid94
Copy link
Collaborator

@aliabid94 aliabid94 commented May 8, 2024

This PR allows adding event listeners inside render blocks. Take a look at the code from demo/render_merge below:

import gradio as gr

with gr.Blocks() as demo:
    text_count = gr.Slider(1, 5, step=1, label="Textbox Count")

    @gr.render(inputs=[text_count], triggers=[text_count.change])
    def render_count(count):
        boxes = []
        for i in range(count):
            box = gr.Textbox(key=i, label=f"Box {i}")
            boxes.append(box)

        def merge(*args):
            return " ".join(args)
        
        merge_btn.click(merge, boxes, output)

        def clear():
            return [""] * count
                
        clear_btn.click(clear, None, boxes)

        def countup():
            return [i for i in range(count)]
        
        count_btn.click(countup, None, boxes)

    with gr.Row():
        merge_btn = gr.Button("Merge")
        clear_btn = gr.Button("Clear")
        count_btn = gr.Button("Count")
        
    output = gr.Textbox()

This is done by removing previous event listeners from a render block and attaching the new event listeners on every re-render. A previous assumption is no longer true: that dependencies is a static list of event listeners for an app, and event listener can be identified by it's fn_index which is it's index within this list of listeners. We've replaced this with a concept of an id for each dependency instead of using indices, and different sessions are able to extend the default set of event listeners by adding new dependencies with different ids.

Closes: #4689
Closes: #7739
Closes: #2570
Closes: #2107 (once #8297 is merged in)

Ali Abid and others added 4 commits May 20, 2024 14:40
* state changes

* changes

---------

Co-authored-by: Ali Abid <aliabid94@gmail.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
@abidlabs
Copy link
Member

Actually 2 small things I noticed:

  • The triggers parameter in gr.render() requires a list (even though inputs and outputs don't) --> it should convert singletons to lists automatically
  • Normally, when a component is used as an input, we render it in interactive mode. However, this does not apply if a component is an input in gr.render()

E.g. the slider here is not interactive automatically;

import gradio as gr

with gr.Blocks() as demo:    
    s = gr.Slider(1, 4, step=1)
    
    @gr.render(inputs=s, triggers=[s.change])
    def inference(num):
        with gr.Row():
            for i in range(num):
                gr.Textbox()

demo.launch()        

@abidlabs
Copy link
Member

One other thing I noticed was that a @gr.render automatically creates a gr.Column for the rendered components, even if the parent BlocksContext is a gr.Row.

I.e. this does not work as expected:

import gradio as gr

with gr.Blocks() as demo:
    b = gr.Button("Add Textbox")
    num = gr.State(0)
    
    b.click(lambda x:x+1, num, num)
    
    with gr.Row():
        @gr.render(inputs=num)
        def show_textbox(n):
            for i in range(n):
                gr.Textbox()

demo.launch()

(it renders the textboxes in a column). However, this works as expected:

import gradio as gr

with gr.Blocks() as demo:
    b = gr.Button("Add Textbox")
    num = gr.State(0)
    
    b.click(lambda x:x+1, num, num)
    
    @gr.render(inputs=num)
    def show_textbox(n):
        with gr.Row():
            for i in range(n):
                gr.Textbox()

demo.launch()

@abidlabs
Copy link
Member

And one other thing that I noticed is that the trigger_mode="always_last" behavior doesn't seem to apply to gr.render(). Here's what I mean. I built a Space with this code:

import gradio as gr

with gr.Blocks() as demo:
    gr.Markdown("# Train a Text Classifier with Synthetic Data")
    labels = gr.Dropdown(choices=[], value=[], label="Classes", allow_custom_value=True, multiselect=True)
    
    @gr.render(inputs=[labels])
    def show_textbox(labels_):
        with gr.Row():
            for label in labels_:
                gr.TextArea(label=f"Samples for class: {label}")
        if len(labels_)>=2:
            with gr.Row():
                gr.Button("TRAIN!", variant="primary")

demo.launch()

If I quickly change the value of the dropdown by backspacing and deleting the choices, you can see that a couple of the textareas are still visible at the end:

Screen.Recording.2024-05-21.at.11.33.06.PM.mov

Copy link
Member

@pngwn pngwn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested and works well as far as I can tell, frontend looks good to me too! Thanks for this @aliabid94 and thanks for going through it with me!

@aliabid94
Copy link
Collaborator Author

Thanks for the reviews! Will add docs, and some more tests and cleanup in follow up PR

@aliabid94 aliabid94 merged commit 55f664f into main May 22, 2024
8 checks passed
@aliabid94 aliabid94 deleted the render_deps branch May 22, 2024 21:51
@pngwn pngwn mentioned this pull request May 22, 2024
@acylam
Copy link

acylam commented May 24, 2024

And one other thing that I noticed is that the trigger_mode="always_last" behavior doesn't seem to apply to gr.render(). Here's what I mean. I built a Space with this code:

import gradio as gr

with gr.Blocks() as demo:
    gr.Markdown("# Train a Text Classifier with Synthetic Data")
    labels = gr.Dropdown(choices=[], value=[], label="Classes", allow_custom_value=True, multiselect=True)
    
    @gr.render(inputs=[labels])
    def show_textbox(labels_):
        with gr.Row():
            for label in labels_:
                gr.TextArea(label=f"Samples for class: {label}")
        if len(labels_)>=2:
            with gr.Row():
                gr.Button("TRAIN!", variant="primary")

demo.launch()

If I quickly change the value of the dropdown by backspacing and deleting the choices, you can see that a couple of the textareas are still visible at the end:
Screen.Recording.2024-05-21.at.11.33.06.PM.mov

Awesome feature! I'm just wondering if there's a way to define the event listener outside gr.Blocks? This is crucial for readability in large applications.

@abidlabs
Copy link
Member

Hi @acylam great point. One thing that you can do is import Blocks from other files and .render() them (this .render() is unrelated to gr.render() -- sorry!) inside the main Blocks object. Like this:

Say this is your utils.py file:

import gradio as gr

with gr.Blocks() as func_blocks:
    labels = gr.Dropdown(choices=[], value=[], label="Classes", allow_custom_value=True, multiselect=True)
    
    @gr.render(inputs=[labels])
    def show_textbox(labels_):
        with gr.Row():
            for label in labels_:
                gr.TextArea(label=f"Samples for class: {label}")
        if len(labels_)>=2:
            with gr.Row():
                gr.Button("TRAIN!", variant="primary")
    

and in your main app.py file, you could do:

import gradio as gr
from utils import func_blocks

with gr.Blocks() as demo:
    gr.Markdown("# Train a Text Classifier with Synthetic Data")
    
    func_blocks.render()

demo.launch()

Does this work for you? cc @aliabid94 if he has any other recommendations

@acylam
Copy link

acylam commented May 27, 2024

Hi @acylam great point. One thing that you can do is import Blocks from other files and .render() them (this .render() is unrelated to gr.render() -- sorry!) inside the main Blocks object. Like this:

Say this is your utils.py file:

import gradio as gr

with gr.Blocks() as func_blocks:
    labels = gr.Dropdown(choices=[], value=[], label="Classes", allow_custom_value=True, multiselect=True)
    
    @gr.render(inputs=[labels])
    def show_textbox(labels_):
        with gr.Row():
            for label in labels_:
                gr.TextArea(label=f"Samples for class: {label}")
        if len(labels_)>=2:
            with gr.Row():
                gr.Button("TRAIN!", variant="primary")
    

and in your main app.py file, you could do:

import gradio as gr
from utils import func_blocks

with gr.Blocks() as demo:
    gr.Markdown("# Train a Text Classifier with Synthetic Data")
    
    func_blocks.render()

demo.launch()

Does this work for you? cc @aliabid94 if he has any other recommendations

@abidlabs Thanks for the quick reply. This works, but I was hoping for a way to separate event handling from UI elements. Kind of like how you'd define the structure of the UI in HTML and the event handling logic in JavaScript in traditional web development? So ideally, gr.Markdown and labels=gr.Dropdown will be defined in one file, and show_text will be defined in a separate file. I hope I'm making sense. It could also be I'm completely missing the design of gradio.

cc @aliabid94

@pngwn
Copy link
Member

pngwn commented May 27, 2024

I've searched high and low and I can't find the issue or notes anywhere. I did find this message in slack that I wrote a long time ago:

Eventually i envisage many kinds of custom components:

  • fully custom with a new python + html/js/css
    • either extending from existing gradio components or fully custom implementations
  • pure frontend components that use some gradio python class. Basically a new frontend for existing components
  • Pure python components.
    • Something like the above, composition for components with a new interface
    • or new python implementation for an existing component that will use an existing frontend. (Maybe is changes stuff like preprocessing or w/e)

This one seems relevant here:

composition of [python] components with a new interface

The idea being that you could somehow contain a component within a function or block and wrap the events (or just forward them) with a new interface for better abstraction.

The original use case was:

  • wrap some components in a block, function or class.
  • define new events for that abstraction that are triggered by the events of the base components.
  • Reuse that abstraction anywhere using the new user-defined event interface.

gr.Render was but a dream back then but I could see the above approach working for render as well.

Imo it is very valuable for complex apps and probably the last missing piece in terms of making gradio a really expressive way of building complex UI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
v: patch A change that requires a patch release
Projects
None yet
7 participants