feat: add dynamic card/value box dashboard tool#266
Draft
gadenbuie wants to merge 39 commits into
Draft
Conversation
Add a "cards" tool category that lets the LLM promote query results and insights into a persistent, developer-placed dashboard area via a new `querychat_card` tool and `$ui_cards()` method. - New `tool_card()` (R/querychat_card.R) with add/update/remove actions and per-type validation: table (test_query), visualization (ggsql), value box (exactly 1x1), and markdown. - Card store + `manage_card` callback, `render_card()` renderer for all four card types, and `mod_ui_cards()` in the Shiny module; cards persist across bookmarks. - `$ui_cards()` method plus `card_placeholder`/`card_layout` configuration on `$server()`. - Conditional system prompt section and `tool-card.md` tool description. - 11-cards-app example and test-querychat_card.R coverage.
- isolate cards reactiveVal read in manage_card (tool runs outside a reactive context) - pass required id to input_code_editor in card code tabs - render table/visualization cards with navset_card_underline, a leading nav_spacer, and icon-only tab titles - validate value box icon in the card tool so bad bsicons names return to the model instead of crashing the render
Replace the card tool "update" action with two explicit actions:
"replace" (full overwrite, the former "update" behavior) and "patch"
(overlay only the supplied fields onto an existing card). Patch fetches
the existing card via a new manage_card("get") path, merges with
utils::modifyList(), then runs the same validation as replace.
Collapsed the two-axis type/display taxonomy into a single `display`
enum with four values: table, visualization, markdown, value_box. Merged
the `footer` (card) and `subtitle` (value_box) fields into a single
`caption` field. Schema is now uniform: every card is
{id, display, title, value, caption, theme, icon}.
Icon is now validated and honored by all display types: value_box uses
it as showcase, table/visualization/markdown render it in the card
header next to the title. Theme remains value_box-only.
Updated tool arguments, validator, renderer, prompt docs, and tests
(45 card tests, 780 total).
Replace the single flat `layout_columns()` grid with run-based layout: - Walk the card list and coalesce consecutive cards of the same kind (value_box vs content) into runs. Each run renders as its own `bslib::layout_column_wrap()` block; blocks are stacked vertically via `htmltools::tagList()`, preserving insertion order. - Value-box runs use `width = "200px"`; content runs (table, visualization, markdown) use `width = "400px"` and `heights_equal = "row"` so short markdown cards don't stretch to match tall tables or charts. - Extract `coalesce_card_runs()` as a named helper in `querychat_module.R` for clarity. BREAKING CHANGE: Remove `card_layout` parameter from `$server()` and `mod_server()`. Layout is now fully automatic. The parameter was previously forwarded to `layout_columns()` (e.g. `list(col_widths = c(6, 6))`); callers passing `card_layout` must remove it.
Extract each display branch of render_card() into its own happy-path helper (render_card_value_box/table/visualization/markdown) and reduce render_card() to a switch() dispatcher. Centralize error handling: move the per-branch tryCatch up into render_card(), which now returns a single danger-accented error card (render_card_error) carrying the condition message for any display type, including value boxes.
Vendor rlang's standalone-purrr shim and replace base vapply()/Filter() calls in querychat_module.R with map_chr()/map_lgl()/discard().
Set the chat tool-call title from the action: "Add Card", "Replace Card", "Update Card" (patch), "Remove Card" — via the result's extra$display$title, overriding the static "Update Cards" annotation. Rework tool-card.md and prompt.md to lead with action:"patch" as the preferred edit and clarify it only needs the fields being changed.
Add an action:"get" to querychat_card so the model can read existing
cards — all cards when "id" is omitted, or a single card by "id". This
also sets up future card-restore, where the model needs to query the
cards that already exist.
Drop cards_summary from the mutation responses. Add/replace/patch/remove
now return {id, status}; get returns the cards' full definitions as a
JSON array (all) or object (one), id-first with unset fields omitted.
Card ids were 16 hex chars (random_hex() default); shorten to 4 chars so the model can type them easily for patch/replace/remove/get. Add new_card_id() which regenerates on the rare collision with an existing card id.
When the "cards" tool is enabled, $app_obj()/querychat_app() now show the data table and an "Insights" tab (wiring up $ui_cards()) in a tabbed card. The default app (cards disabled) is unchanged.
The bundled app now has an Insights tab, so querychat_app() defaults to
tools = c("filter", "query", "visualize", "cards"). An explicit tools
argument overrides it. querychat() and QueryChat$new() keep the
c("filter", "query") default.
When the cards tool is enabled, guide the model to offer pinning noteworthy insights as clickable suggestions when they surface, rather than only on an explicit request. Capped to one or two per turn to avoid spamming offers for routine results.
$app_obj() now always uses bslib::page_navbar(). The Data tab holds the SQL query card and the data table; the Insights tab (cards UI) appears when the "cards" tool is enabled. The close button moves into a nav_item().
Overload `enable_bookmarking` on `$server()`/`mod_server()` to accept a
character subset of c("conversation", "cards") in addition to TRUE/FALSE.
This lets cards be bookmarked independently of conversation history, so an
app configured with `enable_bookmarking = "cards"` produces links that open
with the same insights but a fresh conversation.
- Add `normalize_bookmark_categories()` to coerce TRUE/FALSE/NULL/subset
into a validated character vector of categories.
- Fold the dashboard filter (sql/title), greeting, and inline viz widgets
into the "conversation" category; cards into the "cards" category.
- Drive the bookmark trigger ourselves for cards-only bookmarking, since
shinychat only triggers on conversation activity.
- Namespace all bookmark state keys with `session$ns()` so multiple
QueryChat instances in one app don't clobber each other's state.
Two bugs surfaced when bookmarking cards to the URL: - Drop the manual `session$ns()` on bookmark state keys. Shiny's module scope already namespaces `state$values` per module id, so pre-namespacing double-prefixed the keys (e.g. `id-id-querychat_cards`). Flat keys are correct and still collision-safe across multiple instances. - Shiny decodes URL state with `jsonlite::fromJSON(simplifyDataFrame = TRUE)`, turning the bookmarked card array into a data.frame and breaking restore with "$ operator is invalid for atomic vectors". Add `restore_record_list()` to rebuild the list-of-lists shape (dropping absent optional fields), and apply it to both cards and viz widgets on restore.
… to tool args Dedupe the card prompting across its three surfaces so each has one job: ellmer tool `arguments` carry the per-parameter contract, tool-card.md covers purpose/when-to-use/displays/failure behavior, and prompt.md covers conversational judgment only (dropped the mechanical patch/replace/get bullets). Remove the redundant Parameters/Returns sections from tool-card.md (now covered by the inline ellmer arg descriptions, matching #250) and reword the arg descriptions to match. Also: encourage one value_box per metric for small sets (~3-4); instruct the value_box SQL to format the value as a human-readable string; keep the prompts ASCII-only (no em-dashes); and generalize bslib/bsicons references to Bootstrap.
Cards are R-only today; Python registers no card tool yet. This stages the card tool prompt so it's ready when the Python port lands. Keeps the Parameters/Returns sections per the Python convention (chatlas extracts param docs from __doc__).
The proactive-offer guidance only described what was worth pinning. Add the signals that should trigger an offer: ah-ha results, the user showing interest (follow-up questions, circling back, reactions), and several related findings accumulating over a few exchanges. Guardrails unchanged (offer as a suggestion, one or two per turn, never routine lookups).
…gonalize bookmark axes Rename the $server() argument enable_bookmarking to bookmark_enable, now that it selects which categories of state to bookmark rather than a plain on/off. The old name remains as a soft-deprecated alias (lifecycle::deprecate_warn). Add bookmark_enable to $app(), $app_obj(), and querychat_app() (new, unreleased) and make the two bookmarking arguments orthogonal: - bookmark_enable controls whether + what is saved (TRUE/FALSE/categories). - bookmark_store controls where state is stored (url/server/disable). Nothing is bookmarked when either axis is off; neither argument mutates the other. $app_obj() resolves an effective store from both, so e.g. bookmark_store="url" + bookmark_enable=FALSE now passes "disable" to shinyApp rather than enabling an empty URL store. BREAKING CHANGE: $server(enable_bookmarking=) is deprecated in favor of bookmark_enable.
…e supplied Addresses review findings on the previous commit: - The deprecated enable_bookmarking alias no longer silently overrides an explicitly supplied bookmark_enable; supplying both now errors. - Add tests for the deprecation warning, the conflict error, and $app_obj()'s enableBookmarking resolution (bookmark_store + bookmark_enable -> effective store).
…r method Table cards called executor$test_query(value), but the QueryExecutor's test_query() requires a table_name (used to dispatch per-source and check require_all_columns), so an add with no named table raised "missing subscript". Card queries can JOIN across tables, so there is no single table to name. Add a dedicated QueryExecutor$validate_query(query) that just confirms a query runs (fetch 1 row, no table/column semantics) and point the table card branch at it. DuckDBExecutor sends + fetches one row; DataSourceExecutor delegates to the primary source's test_query(). Test fixtures passed a bare DataFrameSource (whose test_query() has no table_name arg), masking the production signature. Add local_query_executor() and run the card tests against a real executor so this drift is caught.
The QueryExecutor rebase renamed tool_card()'s first argument to `executor` and dropped the old `<DataSource>` guard, leaving a bad first argument to surface as a confusing downstream error. Add check_query_executor() (mirroring check_data_source()) and call it first in tool_card() so an invalid executor fails with a clear message. Update the input-check test to pass a real executor when exercising the manage_card guard, and re-record the snapshot.
The internal mod_server() argument was renamed enable_bookmarking -> bookmark_enable without an alias (only the public $server() kept a deprecation shim), but four direct testServer(mod_server, ...) calls still passed the old name and errored. The file is skip_on_cran, which hid the failures.
Change the `bookmark_store` default from "url" to NULL on `$app()`, `$app_obj()`, and `querychat_app()`. NULL means "auto-decide": defer to a store the app author already set via `shiny::enableBookmarking()`, otherwise pick a sensible default. Add `resolve_bookmark_store()` in querychat_module.R, resolving (first match): disable when no categories; an explicit store; defer (NULL) when enableBookmarking() is already set; "server" when conversation is bookmarked (transcript overflows URL limits); "server" on a hosting platform (via R_CONFIG_ACTIVE); "url" otherwise. `$server()` continues to set no store itself.
Pull the required-field checks, icon validation, and per-display query validation out of the LLM tool path into a pure helper, so the URL-restore and author-seed entry points can share one definition of a valid card. No behavior change to the tool path.
Add cards_to_payload()/payload_to_cards() (gzip + URL-safe base64), $cards_url()/$cards_set_url() methods on QueryChat, and a mod_server() reader that seeds the cards reactive from a namespaced querychat_cards query param at session start. URL-seeded cards take precedence over bookmark-restored cards.
Add a cards arg to QueryChat\$new() and querychat(), accepting a list of card field-lists, a JSON string, or a path to a .json file. normalize_seed_cards() does structural checks at construction; mod_server() runs full validate_and_build_card() validation (loud, indexed) once the executor is available and uses the result as the initial cards reactive value. A URL param or bookmark restore still overwrites the seed.
…ature C) Render an "open in new tab" link at the bottom of the Insights panel that points at \$cards_url() for the current cards. Only shown when cards are enabled and the dashboard is non-empty; rebuilds reactively as cards change.
Regenerate man pages for \$cards_url()/\$cards_set_url() and the cards seed arg, add a NEWS entry, and apply air formatting.
querychat_app()/$app_obj() now selects the Insights tab on startup whenever cards are present, regardless of how they were seeded (author cards= arg, ?querychat_cards= URL param, or bookmark restore). An onFlushed(once) check fires after the first reactive flush, by which point all three seed paths have applied.
Value boxes now support full_screen = TRUE. When expanded, a read-only SQL code editor appears below the value so users can inspect the query behind the stat. The editor is hidden in normal mode via a CSS rule on the bslib data-full-screen attribute.
Mechanical rename — no behavior change. The overloaded `value` field is replaced by `query` (SQL/ggsql data for table/visualization/value_box) and `text` (markdown body for markdown display). Per-display required-field validation now checks `query` or `text` as appropriate.
Value box queries can now return multiple columns in a single row. The displayed number comes from the `value` column (or the first column if no `value` column). Columns named `title`, `caption`, `theme`, or `icon` override the static card fields, enabling dynamic theming (e.g. CASE WHEN ... THEN 'danger' ELSE 'success' END AS theme). Add-time validation checks returned theme/icon values. Render-time column-mapping uses a col_or() helper that treats NA as absent, falling back to the static card fields.
Markdown cards can now optionally include a `query` field (SQL returning
exactly 1 row) whose columns become {{var}} placeholders in the `text`
body, rendered via whisker::whisker.render(). When a query is present,
the card shows a tabbed view (content + SQL code tab) matching the
table/visualization pattern. Static markdown (no query) is unchanged.
…fix card API gaps - Merge `caption` field into `text`: single field now serves as body (markdown), footer (table/viz), or subtitle (value_box) - Drop theme validation — bslib accepts any theme string as a CSS class - Rename `data_source` parameter to `executor` in render_card functions - Fix `querychat_app()` not passing `cards` to `QueryChat$new()` - Fix stale `@param cards` docstring referencing removed `value` field - Update tool descriptions and prompts (R and Python) for new schema
- Add `migrate_card_fields()` shim: copies `caption` -> `text` and `value` -> `query` for cards persisted under the old schema - Apply migration in both `validate_and_build_card()` (seed/URL cards) and `render_card()` (bookmark-restored cards) - Update Python tool prompt to describe theme values as examples, not an exhaustive set - Regenerate Rd documentation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new
querychat_cardLLM tool that lets the model promote query results and insights into a persistent, developer-placed dashboard area. Cards are opt-in: include"cards"in thetoolsparameter and place$ui_cards()in your layout.table,visualization,markdown,value_box— with a uniform schema ({id, display, title, query, text, theme, icon}) and five actions (add,patch,replace,remove,get)value,title,text,theme, oriconoverride the corresponding static card fieldsquery, columns become{{var}}mustache placeholders in thetextbody$cards_url()encodes cards as gzip + base64 into a URL query param, independent of Shiny bookmarks; author-seeded cards via thecardsargument onQueryChat$new()bookmark_enable/bookmark_storeparameters with smart auto-resolution;enable_bookmarkingdeprecated with a lifecycle warningquerychat_app()gains apage_navbarlayout with Data + Insights tabs; Insights tab appears only when cards are enabledVerification
Ask the assistant to create cards — e.g. "Show me average body mass by species as a value box" or "Summarize the dataset in a markdown card." Cards appear in the Insights tab and persist across the conversation.
For author-seeded cards: