From 24fb5f956b073a022ee37cb08ea4f31096c4dd23 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 29 Jul 2024 15:33:29 -0500 Subject: [PATCH] Get `ui.Chat()` working inside Shiny modules (#1582) --- CHANGELOG.md | 2 ++ shiny/ui/_chat.py | 8 +++-- .../shiny/components/chat/module/app.py | 32 +++++++++++++++++++ .../chat/module/test_chat_module.py | 18 +++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 tests/playwright/shiny/components/chat/module/app.py create mode 100644 tests/playwright/shiny/components/chat/module/test_chat_module.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 39868f90b..18b6137f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * An empty `ui.input_date()` value no longer crashes Shiny. (#1528) +* `ui.Chat()` now works as expected inside Shiny modules. (#1582) + * Fixed bug where calling `.update_filter(None)` on a data frame renderer did not visually reset non-numeric column filters. (It did reset the column's filtering, just not the label). Now it resets filter's label. (#1557) * Require shinyswatch >= 0.7.0 and updated examples accordingly. (#1558) diff --git a/shiny/ui/_chat.py b/shiny/ui/_chat.py index 61088e7d0..bf0293b2f 100644 --- a/shiny/ui/_chat.py +++ b/shiny/ui/_chat.py @@ -20,7 +20,7 @@ from .. import _utils, reactive from .._docstring import add_example -from .._namespaces import resolve_id +from .._namespaces import ResolvedId, resolve_id from ..session import require_active_session, session_context from ..types import MISSING, MISSING_TYPE, NotifyException from ..ui.css import CssUnit, as_css_unit @@ -147,9 +147,11 @@ def __init__( on_error: Literal["auto", "actual", "sanitize", "unhandled"] = "auto", tokenizer: TokenEncoding | MISSING_TYPE | None = MISSING, ): + if not isinstance(id, str): + raise TypeError("`id` must be a string.") - self.id = id - self.user_input_id = f"{id}_user_input" + self.id = resolve_id(id) + self.user_input_id = ResolvedId(f"{self.id}_user_input") self._transform_user: TransformUserInputAsync | None = None self._transform_assistant: TransformAssistantResponseChunkAsync | None = None if isinstance(tokenizer, MISSING_TYPE): diff --git a/tests/playwright/shiny/components/chat/module/app.py b/tests/playwright/shiny/components/chat/module/app.py new file mode 100644 index 000000000..79ebbd23c --- /dev/null +++ b/tests/playwright/shiny/components/chat/module/app.py @@ -0,0 +1,32 @@ +from htmltools import Tag + +from shiny import App, Inputs, Outputs, Session, module, ui + + +@module.ui +def chat_mod_ui() -> Tag: + return ui.chat_ui(id="chat") + + +@module.server +def chat_mod_server(input: Inputs, output: Outputs, session: Session): + chat = ui.Chat(id="chat") + + @chat.on_user_submit + async def _(): + user = chat.user_input() + await chat.append_message(f"You said: {user}") + + +app_ui = ui.page_fillable( + ui.panel_title("Hello Shiny Chat"), + chat_mod_ui("foo"), + fillable_mobile=True, +) + + +def server(input: Inputs, output: Outputs, session: Session): + chat_mod_server("foo") + + +app = App(app_ui, server) diff --git a/tests/playwright/shiny/components/chat/module/test_chat_module.py b/tests/playwright/shiny/components/chat/module/test_chat_module.py new file mode 100644 index 000000000..6e74c123f --- /dev/null +++ b/tests/playwright/shiny/components/chat/module/test_chat_module.py @@ -0,0 +1,18 @@ +from playwright.sync_api import Page, expect +from utils.deploy_utils import skip_on_webkit + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +@skip_on_webkit +def test_validate_chat_append_user_message(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + chat = controller.Chat(page, "foo-chat") + + # Verify starting state + expect(chat.loc).to_be_visible(timeout=30 * 1000) + chat.set_user_input("A user message") + chat.send_user_input() + chat.expect_latest_message("You said: A user message", timeout=30 * 1000)