Avaiga / taipy

Turns Data and AI algorithms into production-ready web applications in no time.
https://www.taipy.io
Apache License 2.0
10.94k stars 775 forks source link

[OTHER] How to dynamic add / remove visual elements? #1426

Closed failable closed 2 months ago

failable commented 2 months ago

What would you like to share or ask?

When user select different collection dropdown, I'd like to display all the columns of that collection using multiple inputs to accept user input values to filter and query. Since different collections have different numbers of columns, how can I dynamically adjust the numbers of inputs ?

Thanks.

Code of Conduct

FlorianJacta commented 2 months ago

It seems like you need partials. Please read this article.

If I understand correctly you want to dynamically change your page by creating input visual elements.

failable commented 2 months ago

@FlorianJacta Hello, I want to adjust the numbers of the visual elements, not the items of a single element like selector.

In streamlit, it looks like

image image image

Notice how the numbers of text input elements changed when the different collections dropdown is selected.

Revalent code:

def sidebar_qdrant() -> tuple[QdrantClient, str, dict | None, int]:
    qdrant_url = os.getenv("QDRANT_URL", "http://localhost:6333")
    qdrant_url = st.sidebar.text_input("Qdrant url", qdrant_url)
    qdrant_client = QdrantClient(qdrant_url)

    collections = qdrant_client.get_collections().collections
    collection_names = [x.name for x in collections]
    collection_name = st.sidebar.selectbox("Collection", collection_names)
    collection_info = qdrant_client.get_collection(collection_name)

    filterable_payload_types = ["keyword", "text"]
    payload_schema = {
        k: v
        for k, v in collection_info.payload_schema.items()
        if v.data_type in filterable_payload_types
    }
    # Fixed field ordering for better user experience.
    payload_schema = dict(
        sorted(payload_schema.items(), key=lambda x: (x[1].data_type, x[0])),
    )
    query_filter = None
    if payload_schema:
        st.sidebar.markdown("### Filters")

        filters = []
        for field, schema in payload_schema.items():
            value = st.sidebar.text_input(
                f"{field} ({str.capitalize(schema.data_type)})",
            ) # <--------------------------- !!!!!! Dynamically created text inputs
            if value:
                filters += [
                    create_filter(field, schema, value),
                ]

        if filters:
            query_filter = {
                "must": filters,
            }

    limit = st.sidebar.text_input("Limit", 10)

    return qdrant_client, collection_name, query_filter, limit
FlorianJacta commented 2 months ago

You can almost use the same code that you use in Streamlit by having a partial that you can change in real-time. Some documentation on it: https://docs.taipy.io/en/develop/tutorials/visuals/4_partials/

failable commented 2 months ago

Thanks for the link! I'm going to figure it out. Will comment back if I need further help.

FlorianJacta commented 2 months ago

The only difficult part is to bind these variables to Python variables. To do so, you can use a dictionary with all the keys you need for all your forms. Or you can dynamically change your dictionary. You have to create this forms variable containing every variable of your form. Also, you have to separate the logic of the page and the logic of your app interactivity.

The code below does not work; this is just to demonstrate what I mean.

def sidebar_qdrant(state) -> tuple[QdrantClient, str, dict | None, int]:
    with tgb.Page() as new_partial:
        qdrant_url = os.getenv("QDRANT_URL", "http://localhost:6333")
        # tgb.input(label="Qdrant URL", "{qdrant_url}") # - create it somewhere else
        qdrant_client = QdrantClient(state.qdrant_url)

        collections = qdrant_client.get_collections().collections
        collection_names = [x.name for x in collections]
        collection_name = st.sidebar.selectbox("Collection", collection_names)
        collection_info = qdrant_client.get_collection(collection_name)

        filterable_payload_types = ["keyword", "text"]
        payload_schema = {
            k: v
            for k, v in collection_info.payload_schema.items()
            if v.data_type in filterable_payload_types
        }
        # Fixed field ordering for better user experience.
        payload_schema = dict(
            sorted(payload_schema.items(), key=lambda x: (x[1].data_type, x[0])),
        )
        query_filter = None
        if payload_schema:
            tgb.text("### Filters", mode="md")

            filters = []
            for field, schema in payload_schema.items():
                tgb.input(
                    value="{forms."+str(field)+"}",
                    label=f"{field} ({str.capitalize(schema.data_type)})",
                ) # <--------------------------- !!!!!! Dynamically created text inputs
                if value:
                    filters += [
                        create_filter(field, schema, value),
                    ]

            if filters:
                query_filter = {
                    "must": filters,
                }

        tgb.input("{forms.limit}", label="Limit")

    state.forms = payload_schema
    state.your_partial.update_content(state, new_partial)
failable commented 2 months ago

Hello, I came up with this https://gist.github.com/failable/0379edf7a5d82024a69a50194295372f

Any idea why I got two of the filter parts? The below one is not updated when I change the collection name.

Thanks.

Screenshot 2024-06-19 at 11 18 34
FlorianJacta commented 2 months ago

Could you rephrase? I am not sure to understand what you mean. Why is there a blue and a white part?

failable commented 2 months ago

Why is there a blue and a white part?

Yes. The white part seems to be the correct one while the blue part is not.

FlorianJacta commented 2 months ago

Is the blue part not supposed to be there at all, or is it just incorrect? Can you do some print inside your "for"?

failable commented 2 months ago

Is the blue part not supposed to be there at all

Yes

When I launch the app, the logs look like

image

and the page looks like

image

When I change the collection to news, the logs look like

image

and the page look like

image

The filters of the last collection is left in the blue part. And the blue part is not supposed to be there at all when the app is launched.

FlorianJacta commented 2 months ago

The blueprint seems to be created when the on_init is called. Try to change this

gui.add_partial("filter_parial") to gui.add_partial("")

And try to erase the indent you have with the section about the GUI so it is at the level of your if.

        gui = Gui(page=page)
        filter_partial = gui.add_partial("filter_partial")
        gui.run(
            title="Qdrant viewer",
            dark_mode=False,
            debug=True,
            use_reloader=True,
            port=5001,
        )

Then, try to put use_reloader to False.

failable commented 2 months ago

Try to change this gui.add_partial("filter_parial") to gui.add_partial("")

Not working

Then, try to put use_reloader to False.

Not working

And try to erase the indent you have with the section about the GUI so it is at the level of your if.

This works!

Thanks for taking time to dig into this issue!

FlorianJacta commented 2 months ago

Great!