holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.73k stars 514 forks source link

Enable me to create Viewer widgets #7030

Open MarcSkovMadsen opened 2 months ago

MarcSkovMadsen commented 2 months ago

I've many, many times been wanting to create custom widgets using the Viewer. But I've experienced just as many times its not possible.

For example now to help P720 in https://discourse.holoviz.org/t/panel-chat-interface/7505 I would like to create a custom widget and use that for the ChatInterface. But it cannot work as the Viewer cannot be made to work like a Widget.

Minimum, Reproducible Example

import panel as pn
import param

pn.extension()

class PreferenceInput(pn.viewable.Layoutable, pn.widgets.WidgetBase, pn.viewable.Viewer):
    value = param.Parameter()

    def __init__(self, **params):
        super().__init__(**params)

        self._preference_widget = pn.widgets.RadioBoxGroup(options=["blue", "red", "green"], value="blue", name="preference")
        pn.bind(self._update_value, self._preference_widget)
        self._layout = pn.Column(
            "What is your preference?",
            self._preference_widget,
        )

    def _update_value(self, preference):
        self.value = "My preference is {preference}."

    def __panel__(self):
        return self._layout

preference_input = PreferenceInput()

def even_or_odd(contents, user, instance):
    if len(contents) % 2 == 0:
        return "Even number of characters."
    return "Odd number of characters."

pn.chat.ChatInterface(callback=even_or_odd, widgets=[preference_input]).servable()

image

It works fine until I click send. Then I get

  File "/home/jovyan/repos/private/panel/panel/chat/interface.py", line 405, in _click_send
    value = active_widget.value

In this case the problem is that when a Viewer is put in a row its replaced with its __panel__(). Thus then the assumptions about how to retrieve the ChatInterface.active_widget property breaks down. In this case you could probable "fix" the active_widget implementation. But in my experience its a problem all over the place that its not possible to get a Viewer to behave like a widget.

image

As a bonus its also hard and not documented how to get a Viewer to be Layoutable. That is always another friction. The ChatInterface also assumes that about the input widgets. It assumes there is a sizing_mode on the widget.

ahuang11 commented 2 months ago

Just skimmed this issue; wondering whether you can inherit from Widget rather than Viewer, or what the benefit of using Viewer over Widget is?

MarcSkovMadsen commented 2 months ago

Its a good question. If that is possible it should be documented.

I don't believe its possible though. The Widget is for creating widgets from Bokeh JavaScript/ Typescript models?

ahuang11 commented 2 months ago

Here's my take using CompositeWidget.

import panel as pn
import param

pn.extension()

class PreferenceInput(pn.widgets.CompositeWidget):
    value = param.Parameter()

    def __init__(self, **params):
        super().__init__(**params)

        self._preference_widget = pn.widgets.RadioBoxGroup(
            options=["blue", "red", "green"], value="blue", name="preference"
        )
        pn.bind(self._update_value, self._preference_widget, watch=True)
        self._composite[:] = [
            "What is your preference?",
            self._preference_widget,
        ]

    def _update_value(self, preference):
        self.value = f"My preference is {preference}."

preference_input = PreferenceInput()

def even_or_odd(contents, user, instance):
    if len(contents) % 2 == 0:
        return "Even number of characters."
    return "Odd number of characters."

pn.chat.ChatInterface(callback=even_or_odd, widgets=[preference_input]).servable()
image
MarcSkovMadsen commented 2 months ago

That is great.

This should be documented.

Why do we have both the Viewer and the CompositeWidget @philippjfr ?