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 activity indicator to ChatMessage #6153

Merged
merged 5 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions examples/reference/chat/ChatFeed.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"* **`placeholder_threshold`** (float): Min duration in seconds of buffering before displaying the placeholder. If 0, the placeholder will be disabled. Defaults to 0.2.\n",
"* **`auto_scroll_limit`** (int): Max pixel distance from the latest object in the Column to activate automatic scrolling upon update. Setting to 0 disables auto-scrolling.\n",
"* **`scroll_button_threshold`** (int): Min pixel distance from the latest object in the Column to display the scroll button. Setting to 0 disables the scroll button.\n",
"* **`show_activity_dot`** (bool): Whether to show an activity dot on the ChatMessage while streaming the callback response.\n",
"* **`view_latest`** (bool): Whether to scroll to the latest object on init. If not enabled the view will be on the first object. Defaults to True.\n",
"\n",
"#### Methods\n",
Expand Down
1 change: 1 addition & 0 deletions examples/reference/chat/ChatMessage.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"* **`show_timestamp`** (bool): Whether to display the timestamp of the message.\n",
"* **`show_reaction_icons`** (bool): Whether to display the reaction icons.\n",
"* **`show_copy_icon`** (bool): Whether to show the copy icon.\n",
"* **`show_activity_dot`** (bool): Whether to show the activity dot.\n",
"* **`name`** (str): The title or name of the chat message widget, if any.\n",
"\n",
"___"
Expand Down
35 changes: 23 additions & 12 deletions panel/chat/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ class ChatFeed(ListPanel):
display the scroll button. Setting to 0
disables the scroll button.""")

show_activity_dot = param.Boolean(default=True, doc="""
Whether to show an activity dot on the ChatMessage while
streaming the callback response.""")

view_latest = param.Boolean(default=True, doc="""
Whether to scroll to the latest object on init. If not
enabled the view will be on the first object.""")
Expand Down Expand Up @@ -334,6 +338,7 @@ def _build_message(
message_params["avatar"] = avatar
if self.width:
message_params["width"] = int(self.width - 80)

message = ChatMessage(**message_params)
return message

Expand Down Expand Up @@ -401,18 +406,24 @@ async def _serialize_response(self, response: Any) -> ChatMessage | None:
updating the message's value.
"""
response_message = None
if isasyncgen(response):
self._callback_state = CallbackState.GENERATING
async for token in response:
response_message = self._upsert_message(token, response_message)
elif isgenerator(response):
self._callback_state = CallbackState.GENERATING
for token in response:
response_message = self._upsert_message(token, response_message)
elif isawaitable(response):
response_message = self._upsert_message(await response, response_message)
else:
response_message = self._upsert_message(response, response_message)
try:
if isasyncgen(response):
self._callback_state = CallbackState.GENERATING
async for token in response:
response_message = self._upsert_message(token, response_message)
response_message.show_activity_dot = self.show_activity_dot
elif isgenerator(response):
self._callback_state = CallbackState.GENERATING
for token in response:
response_message = self._upsert_message(token, response_message)
response_message.show_activity_dot = self.show_activity_dot
elif isawaitable(response):
response_message = self._upsert_message(await response, response_message)
else:
response_message = self._upsert_message(response, response_message)
finally:
if response_message:
response_message.show_activity_dot = False
return response_message

async def _schedule_placeholder(
Expand Down
7 changes: 7 additions & 0 deletions panel/chat/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ class ChatMessage(PaneBase):
show_copy_icon = param.Boolean(default=True, doc="""
Whether to display the copy icon.""")

show_activity_dot = param.Boolean(default=False, doc="""
Whether to show the activity dot.""")

renderers = param.HookList(doc="""
A callable or list of callables that accept the object and return a
Panel object to render the object. If a list is provided, will
Expand Down Expand Up @@ -240,6 +243,9 @@ def __init__(self, object=None, **params):
self._build_layout()

def _build_layout(self):
self._activity_dot = HTML(
"●", css_classes=["activity-dot"], visible=self.param.show_activity_dot
)
self._left_col = left_col = Column(
self._render_avatar(),
max_width=60,
Expand Down Expand Up @@ -275,6 +281,7 @@ def _build_layout(self):
Row(
self._user_html,
self.chat_copy_icon,
self._activity_dot,
stylesheets=self._stylesheets,
sizing_mode="stretch_width",
css_classes=["header"]
Expand Down
20 changes: 20 additions & 0 deletions panel/dist/css/chat_message.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,23 @@
margin-block: 0px;
margin-inline: 2px;
}

@keyframes fadeOut {
0% {
opacity: 1;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}

.activity-dot {
display: inline-block;
animation: fadeOut 2s infinite cubic-bezier(0.68, -0.55, 0.27, 1.55);
color: #32cd32b2;
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
font-size: 1.25em;
margin-block: 0px;
}
4 changes: 4 additions & 0 deletions panel/tests/chat/test_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,12 +501,14 @@ async def echo(contents, user, instance):
for char in contents:
message += char
yield message
assert instance.objects[-1].show_activity_dot

chat_feed.callback = echo
chat_feed.send("Message", respond=True)
await asyncio.sleep(0.5)
assert len(chat_feed.objects) == 2
assert chat_feed.objects[1].object == "Message"
assert not chat_feed.objects[-1].show_activity_dot

@pytest.mark.asyncio
async def test_async_generator(self, chat_feed):
Expand All @@ -519,12 +521,14 @@ async def echo(contents, user, instance):
async for char in async_gen(contents):
message += char
yield message
assert instance.objects[-1].show_activity_dot

chat_feed.callback = echo
chat_feed.send("Message", respond=True)
await asyncio.sleep(0.5)
assert len(chat_feed.objects) == 2
assert chat_feed.objects[1].object == "Message"
assert not chat_feed.objects[-1].show_activity_dot

def test_placeholder_disabled(self, chat_feed):
def echo(contents, user, instance):
Expand Down
Loading