Use MDEx streaming to render chat content

This commit is contained in:
Claudio Ortolina
2026-03-20 23:42:10 +00:00
parent de8a0a044a
commit 56d9d6bfd7
4 changed files with 84 additions and 13 deletions
+16 -5
View File
@@ -19,12 +19,18 @@ defmodule MusicLibraryWeb.Components.Chat do
|> assign(:view, :active)
|> assign(:chat, nil)
|> assign(:chats, [])
|> assign(:streaming_doc, nil)
|> assign(:has_history, false)}
end
@impl true
def update(%{chunk: chunk}, socket) do
{:ok, update(socket, :current_response, &(&1 <> chunk))}
doc = socket.assigns.streaming_doc || Markdown.new_streaming_doc()
{:ok,
socket
|> update(:current_response, &(&1 <> chunk))
|> assign(:streaming_doc, MDEx.Document.put_markdown(doc, chunk))}
end
def update(%{done: true}, socket) do
@@ -36,6 +42,7 @@ defmodule MusicLibraryWeb.Components.Chat do
socket
|> update(:messages, &(&1 ++ [completed_message]))
|> assign(:current_response, "")
|> assign(:streaming_doc, nil)
|> assign(:loading, false)}
end
@@ -207,7 +214,7 @@ defmodule MusicLibraryWeb.Components.Chat do
phx-hook=".ScrollBottom"
>
<div
:if={@messages == [] and @current_response == ""}
:if={@messages == [] and @streaming_doc == nil}
class="flex h-full flex-col items-center justify-center text-center text-zinc-500 dark:text-zinc-400"
>
<.icon
@@ -231,16 +238,16 @@ defmodule MusicLibraryWeb.Components.Chat do
</div>
<div
:if={@current_response != ""}
:if={@streaming_doc != nil}
class="max-w-[85%] rounded-lg bg-zinc-100 px-4 py-2 text-sm text-zinc-900 dark:bg-zinc-700 dark:text-zinc-100"
>
<div class="dark:prose-invert prose prose-sm">
{raw(Markdown.to_html(@current_response))}
{raw(Markdown.streaming_to_html(@streaming_doc))}
</div>
</div>
<div
:if={@loading and @current_response == ""}
:if={@loading and @streaming_doc == nil}
class="flex items-center gap-2 text-zinc-500 dark:text-zinc-400"
>
<.loading class="size-4" />
@@ -306,6 +313,7 @@ defmodule MusicLibraryWeb.Components.Chat do
socket
|> assign(:messages, [])
|> assign(:current_response, "")
|> assign(:streaming_doc, nil)
|> assign(:error, nil)
|> assign(:chat, nil)
|> assign(:view, :active)}
@@ -329,6 +337,7 @@ defmodule MusicLibraryWeb.Components.Chat do
|> assign(:chat, chat)
|> assign(:messages, messages)
|> assign(:current_response, "")
|> assign(:streaming_doc, nil)
|> assign(:error, nil)
|> assign(:view, :active)}
end
@@ -346,6 +355,7 @@ defmodule MusicLibraryWeb.Components.Chat do
|> assign(:chat, nil)
|> assign(:messages, [])
|> assign(:current_response, "")
|> assign(:streaming_doc, nil)
|> assign(:error, nil)
else
socket
@@ -407,6 +417,7 @@ defmodule MusicLibraryWeb.Components.Chat do
|> assign(:has_history, true)
|> assign(:loading, true)
|> assign(:current_response, "")
|> assign(:streaming_doc, nil)
|> assign(:error, nil)}
end
+33 -7
View File
@@ -1,12 +1,19 @@
defmodule MusicLibraryWeb.Markdown do
@moduledoc """
Custom markdown processor that handles double square bracket links.
Custom markdown processor that handles double square bracket links
and streaming markdown rendering for chat.
Text wrapped in double square brackets like `[[Foo]]` will be rendered as
Text wrapped in double square brackets like `[[Foo]]` will be rendered as
search links to the collection page.
"""
alias MusicLibrary.Records
@mdex_options [
extension: [autolink: true, strikethrough: true, table: true],
render: [unsafe: true],
sanitize: MDEx.Document.default_sanitize_options()
]
@doc """
Converts markdown with custom `[[link]]` syntax to HTML.
@@ -21,11 +28,7 @@ defmodule MusicLibraryWeb.Markdown do
result =
markdown_text
|> process_double_bracket_links()
|> MDEx.to_html!(
extension: [autolink: true, strikethrough: true, table: true],
render: [unsafe: true],
sanitize: MDEx.Document.default_sanitize_options()
)
|> MDEx.to_html!(@mdex_options)
{result, %{}}
end
@@ -34,6 +37,29 @@ defmodule MusicLibraryWeb.Markdown do
def to_html(nil), do: ""
@doc """
Creates a new streaming MDEx document for incremental markdown rendering.
"""
@spec new_streaming_doc() :: MDEx.Document.t()
def new_streaming_doc do
MDEx.new([streaming: true] ++ @mdex_options)
end
@doc """
Renders a streaming MDEx document to HTML.
"""
@spec streaming_to_html(MDEx.Document.t()) :: String.t()
def streaming_to_html(%MDEx.Document{} = doc) do
:telemetry.span(
[:markdown, :streaming_to_html],
%{},
fn ->
result = MDEx.to_html!(doc)
{result, %{}}
end
)
end
@doc """
Processes text to convert [[text]] patterns into markdown links.