Typewriting effect in liveview for streamed text

I’m new to Elixir and the Elixir forum. I hope my question is clear and I post it on the correct topic!

I’m working on a webchat implementation using Phoenix. Whenever a message comes in, my app sends a post request to another backend server, which will send a response containing text in a stream. Currently, I’m assigning new text to the socket at the moment the text comes in. This way, the text appears in my liveview at (close to) the same pace as my backend serves streams the text.

I want to implement some buffer so I can dictate, in my Elixir app, the maximum rate at which text appears in my liveview, effectively creating a typewriting effect. I’m not sure how to do this using Elixir/Phoenix.

Does anyone know how to do this?

For a typewriter effect I assume you want each character to appear after the other with some delay. So you’d first have to decide how you want to do that

  1. CSS (animation)
  2. JS
  3. on the server

For (3) you’d need to create a timer when you got new text in the buffer, handle the msg of the timer in a handle_info of the LV and there

  • remove the first char from the buffer
  • add it to the text to display
  • continue that until buffer is empty
  • restart if new text in the buffer.

Thanks for your response!

I’m looking for something like (3). Is this also possible with something like a push queue? Where I would broadcasting to a pubsub-topic, but then this topic has a configurable dispatch rate. This would then publish it to a listener, that assigns the text to a socket, but at a maximum rate. As far as I know, this is not possible with the phoenix pubsub system.

Maybe you can benefit from @seanmor5 implementation here: Streaming GPT-3 Responses with Elixir and LiveView – Sean Moriarity

It’s similar to what you’re looking for.

1 Like

Thank you for sharing the link. I’ve actually already implemented something similar to what was described. However, what I’m looking to incorporate additionally is the ability to control the rate at which the text is rendered or added to the socket. Essentially, rather than rendering the chunks at the pace dictated by the API, I aim to introduce the text into the socket at a constant rate that I can regulate, ensuring it’s either slower or equal to the rate at which the chunks are received.

1 Like

I am learning elixir, so not sure if this is the best way, but this would be my suggestion.

So basically; have a typed_text_input ‘buffer’ and a typed_text_output assigned on the socket.

Then handle a :tick message which transfers a character from input to output and resends a :tick message to the same process…

This way if you assign something (coming from pubsub or whatever) to your typed_text_input it will end up char by char in typed_text_output.

  def mount(_params, _session, socket) do
    # start ticker
    Process.send_after(self(), :tick, 100)
    {:ok, assign(socket, typed_text_input: "Type something...", typed_text_output: "")}

  def handle_info(:tick, socket) do
    # send a message to the client
    Process.send_after(self(), :tick, 100)

    case socket.assigns.typed_text_input do
      "" ->
        {:noreply, socket}

      input ->
        # remove the first character from the input and append it to the output
        output = input |> String.slice(0..0)
        # reassign input and output
           typed_text_input: String.slice(input, 1..-1),
           typed_text_output: socket.assigns.typed_text_output <> output

  def render(assigns) do
    <div class="container">
        <%= @typed_text_output %>
1 Like

Sounds like that might work! The only thing that I think needs to be added here is that you stop the “ticking” after the stream is done, so you don’t unnecessarily send tick events.

Thinking more about it, the client-side options seem more efficient. It makes sense to me that it’s better to send all the data as fast as possible to the client and animate it there. Do you know how you would approach such an implementation?

You can push events from the server to a client hook that can handle it using javascript.

This link has an example of how you can handle it (the chart example) and a bit above in the page is the documentations for hooks.

Thanks for providing the link! Sound promising, I’ll give that a try!