jcheng5 / shinychat

Chat UI component for Shiny for R
Other
25 stars 1 forks source link

Rendering maths #6

Closed emitanaka closed 1 week ago

emitanaka commented 2 weeks ago

I'm probably wrong, but I was expecting withMathJax to work here for rending maths in chat output. Am I wrong?

library(shiny)
library(shinychat)

ui <- fluidPage(
  withMathJax(chat_ui("chat"))
)

server <- function(input, output, session) {
  chat <- elmer::chat_openai(system_prompt = "You're a helpful statistics tutor. Show maths with $ or $$ with no spaces in between.")

  observeEvent(input$chat_user_input, {
    stream <- chat$stream_async(input$chat_user_input)
    chat_append("chat", stream)
  })
}

shinyApp(ui, server)
Screenshot 2024-10-11 at 6 16 08 PM
jcheng5 commented 2 weeks ago

Thanks, @emitanaka!

@cpsievert, any thoughts?

cpsievert commented 1 week ago

withMathJax() returns <script>MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);</script>, which does the actual rendering. Since that JS executes before the content gets appended to the page, it has no effect. Also, unfortunately shinychat doesn't currently have a way to determine when a response is done streaming (btw, there a couple ways to do this in Python), so I don't have a great workaround other than to have a button to render the math:


library(shiny)
library(shinychat)

ui <- bslib::page_fillable(
  chat_ui("chat"),
  actionButton("render_math", "Render Math")
)

server <- function(input, output, session) {

  chat <- elmer::chat_openai(system_prompt = "You're a helpful statistics tutor. Show maths with $ or $$ with no spaces in between.")
  stream <- chat$stream_async("What is the mean?")
  chat_append("chat", stream)

  observeEvent(input$render_math, {
    insertUI("body", "beforeEnd", withMathJax())
  })

}

shinyApp(ui, server)

PS. @emitanaka the chat component will look better if you use bslib::page_fluid() instead of fluidPage() :)

jcheng5 commented 1 week ago

I don't remember if I did this on purpose, but chat_append actually returns a promise, which you can chain on.

library(shiny)
library(shinychat)
library(promises)

ui <- bslib::page_fillable(
  chat_ui("chat"),
  actionButton("render_math", "Render Math")
)

server <- function(input, output, session) {

  chat <- elmer::chat_openai(system_prompt = "You're a helpful statistics tutor. Show maths with $ or $$ with no spaces in between.")
  stream <- chat$stream_async("What is the mean?")
  chat_append("chat", stream) %...>% {
    insertUI("body", "beforeEnd", withMathJax(), immediate = TRUE)
  }
}

shinyApp(ui, server)

This seems to work just OK for me, like block equations work but inline expressions don't.

jcheng5 commented 1 week ago

From https://docs.mathjax.org/en/stable/start.html:

The default math delimiters are $$...$$ and [...] for displayed mathematics, and (...) for in-line mathematics. Note in particular that the $...$ in-line delimiters are not used by default. That is because dollar signs appear too often in non-mathematical settings, which could cause some text to be treated as mathematics unexpectedly. For example, with single-dollar delimiters, “… the cost is $2.50 for the first one, and $2.00 for each additional one …” would cause the phrase “2.50 for the first one, and” to be treated as mathematics since it falls between dollar signs. For this reason, if you want to use single-dollars for in-line math mode, you must enable that explicitly in your configuration:

I tried this system prompt: "You're a helpful statistics tutor. Show maths with \(...\) for in-line math and $$...$$ for displayed with no spaces in between." It worked in that the model generated good output:

The mean, often referred to as the average, is a measure of central tendency in statistics. 
It is calculated by summing all the values in a dataset and then dividing that sum by the 
number of values.

Mathematically, the mean \( \mu \) of a dataset with \( n \) values \( x_1, x_2, \ldots, x_n 
\) is given by the formula:

$$
\mu = \frac{x_1 + x_2 + \ldots + x_n}{n}
$$

For example, if you have the dataset \( \{2, 3, 5, 7, 11\} \), the mean would be calculated 
as follows:

1. Sum the values: \( 2 + 3 + 5 + 7 + 11 = 28 \)
2. Count the number of values: \( n = 5 \)
3. Divide the sum by the number of values: 

$$
\mu = \frac{28}{5} = 5.6
$$

So, the mean of this dataset is \( 5.6 \).

but the Markdown renderer in shinychat changes the \(...\) to (...), since \ is an escape character in Markdown.

Oh, this works, by intercepting tokens and replacing every \ with \\. I'm not sure what bad side-effects there might be. @emitanaka maybe this is worth the risk for you.

library(shiny)
library(shinychat)
library(promises)

ui <- bslib::page_fillable(
  chat_ui("chat"),
  actionButton("render_math", "Render Math")
)

server <- function(input, output, session) {

  chat <- elmer::chat_openai(system_prompt = "You're a helpful statistics tutor. Show maths with \\(...\\) for in-line math and $$...$$ for displayed with no spaces in between.")
  stream <- chat$stream_async("What is the mean?")
  stream2 <- coro::async_generator(function() {
    for (chunk in coro::await_each(stream)) {
      yield(gsub("\\", "\\\\", chunk, fixed = TRUE))
    }
  })()
  chat_append("chat", stream2) %...>% {
    insertUI("body", "beforeEnd", withMathJax(), immediate = TRUE)
  }
}

shinyApp(ui, server)
emitanaka commented 1 week ago

Thanks, @jcheng5 and @cpsievert for looking into it! @jcheng5 that's an acceptable hack to me, thanks!

PS: @cpsievert noted about bslib::page_fluid()!