From 8ab19bd4f46ec9f03e60a3133e6aa1d4f2a4d576 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 19 Aug 2025 12:05:04 -0400 Subject: [PATCH 1/2] feat: simplify styles, give querychat components classes --- pkg-py/examples/app-database-sqlite.py | 3 +- pkg-py/examples/app-dataframe-pandas.py | 3 +- pkg-py/examples/app.py | 12 ++- pkg-py/src/querychat/querychat.py | 5 +- pkg-py/src/querychat/static/css/styles.css | 96 ++-------------------- pkg-r/R/querychat.R | 46 +++++++---- pkg-r/R/querychat_app.R | 3 +- pkg-r/inst/htmldep/styles.css | 36 ++------ pkg-r/man/querychat_init.Rd | 22 +++-- pkg-r/man/querychat_ui.Rd | 14 ++-- 10 files changed, 82 insertions(+), 158 deletions(-) diff --git a/pkg-py/examples/app-database-sqlite.py b/pkg-py/examples/app-database-sqlite.py index c5107fb78..953c39272 100644 --- a/pkg-py/examples/app-database-sqlite.py +++ b/pkg-py/examples/app-database-sqlite.py @@ -34,7 +34,7 @@ def use_github_models(system_prompt: str) -> chatlas.Chat: "titanic", greeting=greeting, data_description=data_desc, - create_chat_callback=use_github_models, + client=use_github_models, ) # Create UI @@ -48,6 +48,7 @@ def use_github_models(system_prompt: str) -> chatlas.Chat: ), title="querychat with Python (SQLite)", fillable=True, + class_="bslib-page-dashboard", ) diff --git a/pkg-py/examples/app-dataframe-pandas.py b/pkg-py/examples/app-dataframe-pandas.py index dac93c6ee..71c23c131 100644 --- a/pkg-py/examples/app-dataframe-pandas.py +++ b/pkg-py/examples/app-dataframe-pandas.py @@ -25,7 +25,7 @@ def use_github_models(system_prompt: str) -> chatlas.Chat: "titanic", greeting=greeting, data_description=data_desc, - create_chat_callback=use_github_models, + client=use_github_models, ) # Create UI @@ -39,6 +39,7 @@ def use_github_models(system_prompt: str) -> chatlas.Chat: ), title="querychat with Python", fillable=True, + class_="bslib-page-dashboard", ) diff --git a/pkg-py/examples/app.py b/pkg-py/examples/app.py index 5870d21cc..3503ba2a0 100644 --- a/pkg-py/examples/app.py +++ b/pkg-py/examples/app.py @@ -23,7 +23,7 @@ def use_github_models(system_prompt: str) -> chatlas.Chat: querychat_config = qc.init( data_source=titanic, table_name="titanic", - create_chat_callback=use_github_models, + client=use_github_models, ) # Create UI @@ -32,7 +32,13 @@ def use_github_models(system_prompt: str) -> chatlas.Chat: # Alternatively, use qc.ui(id) elsewhere if you don't want your # chat interface to live in a sidebar. qc.sidebar("chat"), - ui.output_data_frame("data_table"), + ui.card( + ui.card_header("Titanic Data"), + ui.output_data_frame("data_table"), + fill=True, + ), + fillable=True, + class_="bslib-page-dashboard" ) @@ -49,4 +55,4 @@ def data_table(): # Create Shiny app -app = App(app_ui, server) \ No newline at end of file +app = App(app_ui, server) diff --git a/pkg-py/src/querychat/querychat.py b/pkg-py/src/querychat/querychat.py index c9e704822..15c637063 100644 --- a/pkg-py/src/querychat/querychat.py +++ b/pkg-py/src/querychat/querychat.py @@ -458,9 +458,7 @@ def mod_ui() -> ui.TagList: return ui.TagList( ui.include_css(css_path), - # Chat interface goes here - placeholder for now - # This would need to be replaced with actual chat UI components - ui.chat_ui("chat"), + ui.chat_ui("chat", class_="querychat"), ) @@ -494,6 +492,7 @@ def sidebar( mod_ui(id), width=width, height=height, + class_="querychat-sidebar", **kwargs, ) diff --git a/pkg-py/src/querychat/static/css/styles.css b/pkg-py/src/querychat/static/css/styles.css index 033ce722c..bd227030a 100644 --- a/pkg-py/src/querychat/static/css/styles.css +++ b/pkg-py/src/querychat/static/css/styles.css @@ -1,93 +1,15 @@ -:root { - --bslib-sidebar-main-bg: #f8f8f8; -} - -.querychat-container { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; -} - -.querychat-messages { - flex: 1; - overflow-y: auto; - padding: 10px; - display: flex; - flex-direction: column; -} - -.querychat-input-container { - display: flex; - padding: 10px; - border-top: 1px solid #ccc; -} - -.querychat-input { - flex: 1; - padding: 8px; - border: 1px solid #ccc; - border-radius: 4px; - resize: none; - min-height: 40px; - margin-right: 8px; -} - -.querychat-send-button { - padding: 8px 16px; - background-color: #0d6efd; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.querychat-send-button:hover { - background-color: #0a58ca; -} - -.querychat-message { - max-width: 85%; - padding: 8px 12px; - margin-bottom: 8px; - border-radius: 8px; - word-wrap: break-word; -} - -.querychat-user-message { - align-self: flex-end; - background-color: #0d6efd; - color: white; -} - -.querychat-assistant-message { - align-self: flex-start; - background-color: #e9ecef; -} - -.querychat-message table { - width: 100%; - border-collapse: collapse; - margin: 10px 0; -} - -.querychat-message table td, -.querychat-message table th { - border: 1px solid #ccc; +.querychat shiny-chat-message table td, +.querychat shiny-chat-message table th { + border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color); padding: 3px; } -.querychat-message table th { - background-color: #f0f0f0; +.querychat shiny-chat-message table td { + font-family: var(--bs-font-monospace); } -.querychat-message pre { - background-color: #f4f4f4; - padding: 8px; - border-radius: 4px; - overflow-x: auto; +/* querychat takes up the full sidebar, so move the collapse toggle out of the way */ +.bslib-sidebar-layout:has(.querychat-sidebar):not(.sidebar-collapsed)>.collapse-toggle { + right: 4px; + top: 4px; } - -.querychat-message code { - font-family: monospace; -} \ No newline at end of file diff --git a/pkg-r/R/querychat.R b/pkg-r/R/querychat.R index 0bad86830..f35e5b9e1 100644 --- a/pkg-r/R/querychat.R +++ b/pkg-r/R/querychat.R @@ -3,16 +3,20 @@ #' This will perform one-time initialization that can then be shared by all #' Shiny sessions in the R process. #' -#' @param data_source A querychat_data_source object created by `querychat_data_source()`. +#' @param data_source A querychat_data_source object created by +#' `querychat_data_source()`. +#' #' To create a data source: #' - For data frame: `querychat_data_source(df, tbl_name = "my_table")` #' - For database: `querychat_data_source(conn, "table_name")` #' @param greeting A string in Markdown format, containing the initial message #' to display to the user upon first loading the chatbot. If not provided, the #' LLM will be invoked at the start of the conversation to generate one. -#' @param data_description A string containing a data description for the chat model. We have found -#' that formatting the data description as a markdown bulleted list works best. -#' @param extra_instructions A string containing extra instructions for the chat model. +#' @param data_description A string containing a data description for the chat +#' model. We have found that formatting the data description as a markdown +#' bulleted list works best. +#' @param extra_instructions A string containing extra instructions for the +#' chat model. #' @param client An `ellmer::Chat` object, a string to be passed to #' [ellmer::chat()] describing the model to use (e.g. `"openai/gpt-4o"`), or a #' function that creates a chat client. When using a function, the function @@ -26,11 +30,11 @@ #' using [ellmer::chat_openai()]. #' @param create_chat_func `r lifecycle::badge('deprecated')`. Use the `client` #' argument instead. -#' @param system_prompt A string containing the system prompt for the chat model. -#' The default generates a generic prompt, which you can enhance via the `data_description` and -#' `extra_instructions` arguments. -#' @param auto_close_data_source Should the data source connection be automatically -#' closed when the shiny app stops? Defaults to TRUE. +#' @param system_prompt A string containing the system prompt for the chat +#' model. The default generates a generic prompt, which you can enhance via +#' the `data_description` and `extra_instructions` arguments. +#' @param auto_close_data_source Should the data source connection be +#' automatically closed when the shiny app stops? Defaults to TRUE. #' #' @returns An object that can be passed to `querychat_server()` as the #' `querychat_config` argument. By convention, this object should be named @@ -124,14 +128,15 @@ querychat_init <- function( #' UI components for querychat #' #' These functions create UI components for the querychat interface. -#' `querychat_ui` creates a basic chat interface, while `querychat_sidebar` -#' wraps the chat interface in a `bslib::sidebar` component designed to be used -#' as the `sidebar` argument to `bslib::page_sidebar`. +#' `querychat_ui()` creates a basic chat interface, while `querychat_sidebar()` +#' wraps the chat interface in a [bslib::sidebar()] component designed to be +#' used as the `sidebar` argument to [bslib::page_sidebar()]. #' #' @param id The ID of the module instance. -#' @param width The width of the sidebar (when using `querychat_sidebar`). -#' @param height The height of the sidebar (when using `querychat_sidebar`). -#' @param ... Additional arguments passed to `bslib::sidebar` (when using `querychat_sidebar`). +#' @param width,height In `querychat_sidebar()`: the width and height of the +#' sidebar. +#' @param ... In `querychat_sidebar()`: additional arguments passed to +#' [bslib::sidebar()]. #' #' @return A UI object that can be embedded in a Shiny app. #' @@ -141,8 +146,10 @@ querychat_sidebar <- function(id, width = 400, height = "100%", ...) { bslib::sidebar( width = width, height = height, + class = "querychat-sidebar", ..., - querychat_ui(id) # purposely NOT using ns() here, we're just a passthrough + # purposely NOT using ns() for `id`, we're just a passthrough + querychat_ui(id) ) } @@ -159,7 +166,12 @@ querychat_ui <- function(id) { script = "querychat.js", stylesheet = "styles.css" ), - shinychat::chat_ui(ns("chat"), height = "100%", fill = TRUE) + shinychat::chat_ui( + ns("chat"), + height = "100%", + fill = TRUE, + class = "querychat" + ) ) } diff --git a/pkg-r/R/querychat_app.R b/pkg-r/R/querychat_app.R index 92201fd4b..36b50ae40 100644 --- a/pkg-r/R/querychat_app.R +++ b/pkg-r/R/querychat_app.R @@ -44,6 +44,7 @@ querychat_app <- function(config, ..., bookmark_store = "url") { config$data_source$table_name, "" )), + class = "bslib-page-dashboard", sidebar = querychat_sidebar("chat"), bslib::card( fill = FALSE, @@ -130,7 +131,7 @@ querychat_app <- function(config, ..., bookmark_store = "url") { }) } - app <- shiny::shinyApp(ui, server, ..., enableBookmarking = bookmark_store) + app <- shiny::shinyApp(ui, server, enableBookmarking = bookmark_store) tryCatch(shiny::runGadget(app), interrupt = function(cnd) NULL) invisible(chat) } diff --git a/pkg-r/inst/htmldep/styles.css b/pkg-r/inst/htmldep/styles.css index 87e075bb1..bd227030a 100644 --- a/pkg-r/inst/htmldep/styles.css +++ b/pkg-r/inst/htmldep/styles.css @@ -1,37 +1,15 @@ -:root { - --bslib-sidebar-main-bg: #f8f8f8; -} - -.popover { - --bs-popover-header-bg: #222; - --bs-popover-header-color: #fff; -} - -.popover .btn-close { - filter: var(--bs-btn-close-white-filter); -} -shiny-chat-message table td, -shiny-chat-message table th { +.querychat shiny-chat-message table td, +.querychat shiny-chat-message table th { border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color); padding: 3px; } -shiny-chat-message table td { +.querychat shiny-chat-message table td { font-family: var(--bs-font-monospace); } -#show_title:empty, -#show_query:empty { - /* Prevent empty title/query blocks from taking any space */ - border: 0; - padding: 0; - margin-bottom: 0; -} - -#show_title:empty, -#show_query { - /* We can't affect the flex parent's gap, so instead we use a negative margin - to counteract it. In the case of #show_query, we don't even want the gap - even when it's showing, as it results in too much space between the two. */ - margin-top: calc(-1 * var(--bslib-mb-spacer)); +/* querychat takes up the full sidebar, so move the collapse toggle out of the way */ +.bslib-sidebar-layout:has(.querychat-sidebar):not(.sidebar-collapsed)>.collapse-toggle { + right: 4px; + top: 4px; } diff --git a/pkg-r/man/querychat_init.Rd b/pkg-r/man/querychat_init.Rd index bd1a43b9d..c79714d9d 100644 --- a/pkg-r/man/querychat_init.Rd +++ b/pkg-r/man/querychat_init.Rd @@ -16,7 +16,9 @@ querychat_init( ) } \arguments{ -\item{data_source}{A querychat_data_source object created by \code{querychat_data_source()}. +\item{data_source}{A querychat_data_source object created by +\code{querychat_data_source()}. + To create a data source: \itemize{ \item For data frame: \code{querychat_data_source(df, tbl_name = "my_table")} @@ -27,20 +29,22 @@ To create a data source: to display to the user upon first loading the chatbot. If not provided, the LLM will be invoked at the start of the conversation to generate one.} -\item{data_description}{A string containing a data description for the chat model. We have found -that formatting the data description as a markdown bulleted list works best.} +\item{data_description}{A string containing a data description for the chat +model. We have found that formatting the data description as a markdown +bulleted list works best.} -\item{extra_instructions}{A string containing extra instructions for the chat model.} +\item{extra_instructions}{A string containing extra instructions for the +chat model.} \item{create_chat_func}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}. Use the \code{client} argument instead.} -\item{system_prompt}{A string containing the system prompt for the chat model. -The default generates a generic prompt, which you can enhance via the \code{data_description} and -\code{extra_instructions} arguments.} +\item{system_prompt}{A string containing the system prompt for the chat +model. The default generates a generic prompt, which you can enhance via +the \code{data_description} and \code{extra_instructions} arguments.} -\item{auto_close_data_source}{Should the data source connection be automatically -closed when the shiny app stops? Defaults to TRUE.} +\item{auto_close_data_source}{Should the data source connection be +automatically closed when the shiny app stops? Defaults to TRUE.} \item{client}{An \code{ellmer::Chat} object, a string to be passed to \code{\link[ellmer:chat-any]{ellmer::chat()}} describing the model to use (e.g. \code{"openai/gpt-4o"}), or a diff --git a/pkg-r/man/querychat_ui.Rd b/pkg-r/man/querychat_ui.Rd index 40eea7a9c..664e3b6a7 100644 --- a/pkg-r/man/querychat_ui.Rd +++ b/pkg-r/man/querychat_ui.Rd @@ -12,18 +12,18 @@ querychat_ui(id) \arguments{ \item{id}{The ID of the module instance.} -\item{width}{The width of the sidebar (when using \code{querychat_sidebar}).} +\item{width, height}{In \code{querychat_sidebar()}: the width and height of the +sidebar.} -\item{height}{The height of the sidebar (when using \code{querychat_sidebar}).} - -\item{...}{Additional arguments passed to \code{bslib::sidebar} (when using \code{querychat_sidebar}).} +\item{...}{In \code{querychat_sidebar()}: additional arguments passed to +\code{\link[bslib:sidebar]{bslib::sidebar()}}.} } \value{ A UI object that can be embedded in a Shiny app. } \description{ These functions create UI components for the querychat interface. -\code{querychat_ui} creates a basic chat interface, while \code{querychat_sidebar} -wraps the chat interface in a \code{bslib::sidebar} component designed to be used -as the \code{sidebar} argument to \code{bslib::page_sidebar}. +\code{querychat_ui()} creates a basic chat interface, while \code{querychat_sidebar()} +wraps the chat interface in a \code{\link[bslib:sidebar]{bslib::sidebar()}} component designed to be +used as the \code{sidebar} argument to \code{\link[bslib:page_sidebar]{bslib::page_sidebar()}}. } From 9423f3eda337390e635be942da0f0d41acb93fe1 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 19 Aug 2025 12:15:19 -0400 Subject: [PATCH 2/2] docs: Update changelog/news --- pkg-py/CHANGELOG.md | 1 + pkg-r/NEWS.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pkg-py/CHANGELOG.md b/pkg-py/CHANGELOG.md index d3b45dc01..ee64bed0a 100644 --- a/pkg-py/CHANGELOG.md +++ b/pkg-py/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 If `client` is not provided, querychat will use the `QUERYCHAT_CLIENT` environment variable, which should be a provider-model string. If the envvar is not set, querychat uses OpenAI with the default model from `chatlas.ChatOpenAI()`. +* `querychat.ui()` now adds a `.querychat` class to the chat container and `querychat.sidebar()` adds a `.querychat-sidebar` class to the sidebar, allowing for easier customization via CSS. (#68) ## [0.1.0] - 2025-05-24 diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index df0d6fa79..c88f7f41f 100644 --- a/pkg-r/NEWS.md +++ b/pkg-r/NEWS.md @@ -23,3 +23,5 @@ * querychat now requires `ellmer` version 0.3.0 or later and uses rich tool cards for dashboard updates and database queries. (#65) * New `querychat_app()` function lets you quickly launch a Shiny app with a querychat chat interface. (#66) + +* `querychat_ui()` now adds a `.querychat` class to the chat container and `querychat_sidebar()` adds a `.querychat-sidebar` class to the sidebar, allowing for easier customization via CSS. (#68)