feat: add footer argument to chat_ui()#224
Conversation
Allows users to place arbitrary HTML content below the chat input, rendered as an HTML island via the RawHTML component (with Shiny binding support). Footer text is styled slightly smaller and lighter by default, customizable via `--shiny-chat-footer-font-size` and `--shiny-chat-footer-color` CSS properties.
| footer_tag = None | ||
| footer_deps = None | ||
| if footer is not None: | ||
| if isinstance(footer, str): | ||
| footer_tag = Tag("shiny-chat-footer", HTML(footer)) | ||
| elif isinstance(footer, (Tag, TagList)): | ||
| footer_tag = Tag("shiny-chat-footer", HTML(str(footer))) | ||
| footer_deps = footer.get_dependencies() | ||
| else: | ||
| footer_tag = Tag("shiny-chat-footer", HTML(str(footer))) |
There was a problem hiding this comment.
What's the reasoning for needing to "pre-render"? Also, I think you could drop the first if condition here.
There was a problem hiding this comment.
Good call on both points.
-
Simplified the Python branching — collapsed the three-way
if/elif/elsedown to two branches (one to extract deps fromTag/TagList, then a singleTag("shiny-chat-footer", HTML(str(footer)))for all cases). -
Moved the rendering logic to JS — instead of extracting
innerHTMLas a string and re-rendering it viaRawHTMLin React, the JS now preserves the server-rendered<shiny-chat-footer>DOM element and moves its child nodes directly into the React tree via a ref. This means:- No serialize→deserialize roundtrip for the footer HTML
- Shiny bindings on footer elements aren't destroyed and rebound
- R and Python only need to emit the
<shiny-chat-footer>wrapper (minimal, identical contract)
…eservation Instead of extracting innerHTML from the server-rendered <shiny-chat-footer> element and re-rendering it via RawHTML in React, preserve the DOM element and move its child nodes directly into the React tree via a ref. This avoids a serialize/deserialize roundtrip and keeps Shiny bindings intact. Also simplifies the Python footer code from three branches to two.
|
One thing I noticed while reading through the JS: the If we captured {footerHtml && (
<RawHTML html={footerHtml} displayContents={false} className="shiny-chat-footer" />
)}That would let us drop the Mostly a consistency argument — the island pattern already handles this exact scenario well, and reusing it keeps one fewer way of doing the same thing. |
This was the pattern I was using until your comment about pre-rendering #224 (comment) |
One thing that I liked about moving away from this approach was that if we're capturing |
|
Good points — I see now that the I'll also admit that when I left the earlier comment about pre-rendering, I hadn't fully read through the TypeScript changes yet, so I didn't have the full picture of what you were optimizing for. That said, I think I'd still err toward reusing If we do want the cleaner lifecycle, I think it'd be worth extracting the pattern into a proper primitive (something like Either way, I don't feel strongly — happy to leave it to your judgment. |
Summary
Adds a new
footerparameter tochat_ui()(Python and R) that renders arbitrary HTML content below the chat input. Common use cases include disclaimers, attribution text, or interactive toolbars (e.g. model-selection dropdowns, action buttons).--shiny-chat-footer-font-sizeand--shiny-chat-footer-colorCSS custom properties.1fr autoto1fr auto autowhen a footer is present.Examples
Simple text disclaimer:
Toolbar with model selector and action buttons:
Verification
Full example app (Python)
Full example app (R)