Introduction
By default, blockr.ui
exposes a dashboard module to
display blocks on a grid. In short, to spin up an app from an existing
board, you can do:
my_board <- new_dag_board(
...,
modules = new_dashboard_module()
)
serve(my_board)
new_dag_board()
produces a board object
with a dashboard module attached to it with
new_dashboard_module()
. The serve()
function
starts a Shiny app. ...
is a placeholder for
blocks, stacks and
links that you want to display when the app starts.
Develop custom extensions
Creating a new board module
To create a new board module, you can use the
new_board_module()
function. This function takes a
module
argument that defines the module’s UI and server
logic:
my_module <- function(id = "dashboard", title = "Dashboard") {
new_board_module(
module_ui,
module_server,
id = id,
title = title,
context_menu = list(
# PASS context menu entries here (see below)
),
class = "dashboard_module"
)
}
module_ui
and module_server
are functions
that define the module’s user interface and server logic, respectively.
The id
argument is used to identify the module, and the
title
argument is used to display the module’s title in the
app. The context menu is a list of context menu items that will be
available in the app when someone right-clicks on the network.
Context menu items
A context menu entry is defined as follows with
new_context_menu_entry()
:
menu_entry <- new_context_menu_entry(
name = "Remove from dashboard",
js = function(ns) {
sprintf(
"(value, target, current) => {
if (current.id === undefined) return;
Shiny.setInputValue('%s', current.id, {priority: 'event'});
}",
ns("remove_from_dashboard")
)
},
action = function(input, output, session, board, update, parent) {
observeEvent(
input$remove_from_dashboard,
{
parent$removed_from_dashboard <- input$remove_from_dashboard
parent$in_grid[[parent$removed_from_dashboard]] <- FALSE
}
)
},
condition = function(board, parent, target) {
target$type == "node" &&
target$id %in% names(parent$in_grid) &&
parent$in_grid[[target$id]]
}
)
where each parameters is defined as follows:
-
name
is the name of the context menu entry. -
js
is a JavaScript function or a string containing a JS function (() => { ... }
) that defines the behavior of the context menu entry when it is clicked. It can access thens
function to get the namespace of the module. This code is evaluated on the client side. -
action
is an R function that defines the server-side logic of the context menu entry. It can access theinput
,output
,session
,board
,update
, andparent
arguments. -
condition
is an R function that defines the condition under which the context menu entry should be displayed. It can access theboard
,parent
, andtarget
arguments.
In the above example, the context menu entry is named “Remove from
dashboard”. It only appears when the target is a node
and the node is currently in the dashboard grid. When
clicked, the element id is recovered from the
client and stored within
input$<MODULE_NAMESPACE>-remove_from_dashboard
. The
server logic then observes this input and updates the
parent$in_grid
list to remove the node from the grid.
Example: a custom AI chat
We want to create a custom AI powered chat that will allow us to
build blockr pipelines. We leverage shinychat
and define
the server and UI logic as follows:
chat_ui <- function(id, board, ...) {
id <- NS(id, "chat")
card(
card_header("blockr.ui assistant"),
shinychat::chat_mod_ui(
id = NS(id, "openai"),
messages = list(
"Hi! I'll help you to build blockr.ui pipeline with OpenAI's `gpt-4o`."
)
)
)
}
chat_srv <- function(board, update, session, parent, ...) {
moduleServer(
"chat",
function(input, output, session) {
openai <- ellmer::chat_openai(
system_prompt = "You are a helpful assistant for developers who want to work with blockr but do not
not very well data analysis. Packages are available at: https://github.com/BristolMyersSquibb/blockr.core,
https://github.com/BristolMyersSquibb/blockr.dplyr, https://github.com/BristolMyersSquibb/blockr.io, These packages provide
blocks such that people can import data with new_dataset_block, transform them with new_select_block and do other things.
We also have blockr.ai that basically exposes llm blocks such as new_llm_transform_block and new_llm_plot_block. They
are convenient to accomplish tasks for which no block exists yet ... A blockr.ui application allows people to build blockr pipeline
step by step by adding block one after each other.
When you are given an order, you will only return the corresponding block
constructor to the user without the `new`. You will return 'Adding a *_block', '*' being the
type of the block.
For instance 'Add me a block to select columns from a dataset' would return
'Adding a select_block' and not new_select_block. If you are asked a question like 'How to load data?'
You will answer to the question with more ellaborated answer from the blockr documentation.
If you are asked 'How to get started?' or similar, you will explain how a blockr pipeline works, from a data block to a plot block.",
model = "gpt-4o",
echo = "all"
)
res <- shinychat::chat_mod_server("openai", openai)
observeEvent(res(), {
blk <- regmatches(res()@text, regexpr("\\w+_block", res()@text))
if (!length(blk)) {
return(NULL)
}
if (blk %in% names(available_blocks())) {
parent$scoutbar$action <- "add_block"
parent$scoutbar$value <- blk
}
})
}
)
}
In a nutshell, the chat module uses shinychat
to create
a chat interface with OpenAI’s gpt-4o
model. The server
logic listens for messages and updates the parent$scoutbar
to add a block based on the response from OpenAI. Some context is
provided to the model to guide its responses, specifically about blockr
and how to build pipelines. Providing a better context isn’t the purpose
of this vignette but you could imagine asking the model to handle
specific block parameters so we can for instance start with a dataset
block from the palmerpenguins data (instead of the datasets
package).
We then create our new module:
new_chat_module <- function(id = "chat", title = "AI chat") {
new_board_module(
chat_ui,
chat_srv,
id = id,
title = title,
context_menu = list(
new_context_menu_entry(
name = "Open AI chat",
js = "() => {
console.log('Hello world')
}",
action = function(input, output, session, board, update, parent) {
# TBD
}
)
),
class = "chat_module"
)
}
serve(
new_dag_board(
modules = list(
dash = new_dashboard_module(),
chat = new_chat_module()
)
),
"main"
)
The context menu entry won’t do much. If you run the app and right click on the network panel, you’ll see the “Open AI chat” entry. This is then up to you to refine it.