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 ChatInput and Status components #69

Merged
merged 6 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/assets/thumbnails/component_chat_input.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/thumbnails/component_status.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/videos/component_chat_input.mp4
Binary file not shown.
Binary file added docs/assets/videos/component_status.mp4
Binary file not shown.
24 changes: 24 additions & 0 deletions docs/examples/components/component_chat_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
The `ChatInput` widget is a combination of a `TextInput` widget and a `Button`.
When the input is submitted the `TextInput` widget is cleared and ready to accept
a new input.

If you need a `ChatInput` widget you can copy the code from
[here](https://github.com/holoviz-topics/panel-chat-examples/blob/main/panel_chat_examples/components/chat_input.py).
"""
import panel as pn

from panel_chat_examples.components import ChatInput

pn.extension(design="material")

chat_input = ChatInput(placeholder="Say something")


def message(prompt):
if not prompt:
return ""
return f"User has sent the following prompt: **{prompt}**"


pn.Column(pn.bind(message, chat_input.param.value), chat_input, margin=25).servable()
36 changes: 36 additions & 0 deletions docs/examples/components/component_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
The `Status` *indicator* can report progress in steps and with
detailed context.

If you need a `Status` widget you can copy the code from
[here](https://github.com/holoviz-topics/panel-chat-examples/blob/main/panel_chat_examples/components/chat_input/components/status.py).
"""
import time

import panel as pn

from panel_chat_examples.components import Status

status = Status("Downloading data...", sizing_mode="stretch_width")


def run(_):
with status.report() as progress:
status.collapsed = False
progress("Searching for data...")
time.sleep(1.5)
progress("Downloading data...")
time.sleep(1.5)
progress("Validating data...")
time.sleep(1.5)
status.collapsed = True


run_button = pn.widgets.Button(
name="Run", on_click=run, button_type="primary", button_style="outline"
)

pn.Column(
status,
run_button,
).servable()
7 changes: 7 additions & 0 deletions panel_chat_examples/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from panel_chat_examples.components.chat_input import ChatInput
from panel_chat_examples.components.status import Status

__all__ = [
"ChatInput",
"Status",
]
74 changes: 74 additions & 0 deletions panel_chat_examples/components/chat_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""The `ChatInput` widget is a combination of a `TextInput` widget and a `Button`.
When the input is submitted the `TextInput` widget is cleared and ready to accept
a new input."""
import panel as pn
import param


class ChatInput(pn.viewable.Viewer):
"""The `ChatInput` widget is a combination of a `TextInput` widget and a `Button`.
When the input is submitted the `TextInput` widget is cleared and ready to accept
a new input."""

value: str = param.String(doc="""The text value""")

disabled: bool = param.Boolean(
doc="""
Whether or not the widget is disabled. Default is False"""
)
max_length = param.Integer(
default=5000,
doc="""
The max_length of the text input""",
)
placeholder = param.String(
"Send a message",
doc="""
An initial placeholder to display in the TextInput""",
)

def __init__(self, **params):
layout_params = {
key: value
for key, value in params.items()
if key not in ["value", "placeholder", "disabled", "max_length"]
}
params = {
key: value for key, value in params.items() if key not in layout_params
}

super().__init__(**params)

self._text_input = pn.widgets.TextInput(
align="center",
disabled=self.param.disabled,
max_length=self.param.max_length,
name="Message",
placeholder=self.param.placeholder,
sizing_mode="stretch_width",
)
self._submit_button = pn.widgets.Button(
align="center",
disabled=self.param.disabled,
icon="send",
margin=(18, 5, 10, 0),
name="",
sizing_mode="fixed",
)
pn.bind(
self._update_value,
value=self._text_input,
event=self._submit_button,
watch=True,
)

self._layout = pn.Row(
self._text_input, self._submit_button, align="center", **layout_params
)

def __panel__(self):
return self._layout

def _update_value(self, value, event):
self.value = value or self.value
self._text_input.value = ""
152 changes: 152 additions & 0 deletions panel_chat_examples/components/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""
The `Status` *indicator* can report progress in steps and with
detailed context."""
from contextlib import contextmanager

import panel as pn
import param
from panel.widgets.indicators import LoadingSpinner

COLORS = {
"running": "green",
"complete": "black",
"error": "red",
"next": "lightgray",
}
STATUS_PARAMETERS = ["value", "title", "collapsed", "bgcolor", "color", "steps", "step"]


class Status(pn.viewable.Viewer):
"""The `Status` *indicator* can report progress in steps and with
detailed context."""

value = param.Selector(
default="complete",
objects=["complete", "running", "error"],
doc="""
The current state of the Status indicator. One of 'complete',
'running' or 'error'""",
)
title = param.String(doc="The title shown in the card header")

bgcolor = param.ObjectSelector(
default=LoadingSpinner.param.bgcolor.default,
objects=LoadingSpinner.param.bgcolor.objects,
doc="""The background color of the LoadingSpinner""",
)
color = param.ObjectSelector(
default="success",
objects=LoadingSpinner.param.color.objects,
doc="""The color of the LoadingSpinner""",
)
collapsed = param.Boolean(
default=True, doc="""Whether or not the Card is collapsed"""
)

steps = param.List(constant=False, doc="""A list of (markdown) string steps""")
step = param.Parameter(constant=True, doc="""The current step""")

def __init__(self, title: str, **params):
params["title"] = title
params["steps"] = params.get("steps", [])
layout_params = {
key: value for key, value in params.items() if key not in STATUS_PARAMETERS
}
params = {
key: value for key, value in params.items() if key not in layout_params
}
super().__init__(**params)

self._indicators = {
"running": pn.indicators.LoadingSpinner(
value=True,
color=self.param.color,
bgcolor=self.param.bgcolor,
size=25,
# margin=(15, 0, 0, 0),
),
"complete": "✔️",
"error": "❌",
}

self._title_pane = pn.pane.Markdown(self.param.title, align="center")
self._header_row = pn.Row(
pn.panel(self._indicator, sizing_mode="fixed", width=40, align="center"),
self._title_pane,
sizing_mode="stretch_width",
margin=(0, 5),
)
self._details_pane = pn.pane.HTML(
self._details, margin=(10, 5, 10, 55), sizing_mode="stretch_width"
)
self._layout = pn.Card(
self._details_pane,
header=self._header_row,
collapsed=self.param.collapsed,
**layout_params,
)

def __panel__(self):
return self._layout

@param.depends("value")
def _indicator(self):
return self._indicators[self.value]

@property
def _step_color(self):
return COLORS[self.value]

def _step_index(self):
if self.step not in self.steps:
return 0
return self.steps.index(self.step)

@param.depends("step", "value")
def _details(self):
steps = self.steps

if not steps:
return ""

index = self._step_index()

html = ""
for step in steps[:index]:
html += f"<div style='color:{COLORS['complete']}'>{step}</div>"
step = steps[index]
html += f"<div style='color:{self._step_color}'>{step}</div>"
for step in steps[index + 1 :]:
html += f"<div style='color:{COLORS['next']};'>{step}</div>"

return html

def progress(self, step: str):
with param.edit_constant(self):
self.value = "running"
if step not in self.steps:
self.steps = self.steps + [step]
self.step = step

def reset(self):
with param.edit_constant(self):
self.steps = []
self.value = self.param.value.default

def start(self):
with param.edit_constant(self):
self.step = None
self.value = "running"

def complete(self):
self.value = "complete"

@contextmanager
def report(self):
self.start()
try:
yield self.progress
except Exception:
self.value = "error"
else:
self.complete()
26 changes: 26 additions & 0 deletions tests/ui/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ def basic_streaming_chat_async(page: Page):
page.get_by_text("Echoing User: Hello World").inner_text()


def component_chat_input(page: Page):
text_input = page.get_by_placeholder("Say something")

text_input.fill("Hello World")
page.wait_for_timeout(TIMEOUT)
text_input.press("Enter")
page.get_by_text("User has sent the following prompt: Hello World").wait_for()

text_input.fill("Could you please repeat that?")
page.wait_for_timeout(TIMEOUT)
text_input.press("Enter")
page.get_by_text(
"User has sent the following prompt: Could you please repeat that?"
).wait_for()


def component_environment_widget(page: Page):
langchain = page.get_by_role("textbox").nth(0)
langchain.fill("some-secret")
Expand All @@ -61,6 +77,12 @@ def component_environment_widget(page: Page):
page.wait_for_timeout(4 * TIMEOUT)


def component_status(page: Page):
page.get_by_role("button", name="Run").dispatch_event("click")
page.get_by_text("Validating data...").wait_for()
page.wait_for_timeout(TIMEOUT)


def feature_chained_response(page: Page):
chat = ChatInterface(page)
chat.send("Hello World")
Expand Down Expand Up @@ -202,7 +224,9 @@ def openai_two_bots(page: Page):
"basic_chat.py": basic_chat,
"basic_streaming_chat_async.py": basic_streaming_chat_async,
"basic_streaming_chat.py": basic_streaming_chat,
"component_chat_input.py": component_chat_input,
"component_environment_widget.py": component_environment_widget,
"component_status.py": component_status,
"feature_chained_response.py": feature_chained_response,
"feature_delayed_placeholder.py": feature_delayed_placeholder,
"feature_replace_response.py": feature_replace_response,
Expand All @@ -225,7 +249,9 @@ def openai_two_bots(page: Page):
"basic_chat.py": 1.8,
"basic_streaming_chat_async.py": 1.8,
"basic_streaming_chat.py": 1.8,
"component_chat_input.py": 2,
"component_environment_widget.py": 1.25,
"component_status.py": 2,
"feature_chained_response.py": 1.8,
"feature_delayed_placeholder.py": 1.8,
"feature_replace_response.py": 1.8,
Expand Down