defmodule MusicLibraryWeb.Components.Chat do
use MusicLibraryWeb, :live_component
require Logger
alias MusicLibraryWeb.Markdown
def open(id), do: Fluxon.open_dialog(id)
@impl true
def mount(socket) do
{:ok,
socket
|> assign(:messages, [])
|> assign(:current_response, "")
|> assign(:loading, false)
|> assign(:error, nil)}
end
@impl true
def update(%{chunk: chunk}, socket) do
{:ok, update(socket, :current_response, &(&1 <> chunk))}
end
def update(%{done: true}, socket) do
completed_message = %{role: "assistant", content: socket.assigns.current_response}
{:ok,
socket
|> update(:messages, &(&1 ++ [completed_message]))
|> assign(:current_response, "")
|> assign(:loading, false)}
end
def update(%{error: error}, socket) do
{:ok,
socket
|> assign(:error, error)
|> assign(:loading, false)}
end
def update(assigns, socket) do
{:ok, assign(socket, assigns)}
end
@impl true
def render(assigns) do
~H"""
<.sheet
id={@sheet_id}
placement="right"
class="w-md sm:min-w-lg lg:min-w-2xl flex flex-col h-full"
>
{gettext("Chat about %{title}", title: @title)}
<.button
:if={@messages != []}
size="icon-sm"
variant="ghost"
phx-click="clear_chat"
phx-target={@myself}
aria-label={gettext("Clear chat")}
>
<.icon name="hero-trash" class="icon" />
<.icon
name="hero-chat-bubble-left-right"
class="size-12 mb-4 text-zinc-300 dark:text-zinc-600"
/>
{@empty_prompt}
{message.content}
{raw(Markdown.to_html(message.content))}
{raw(Markdown.to_html(@current_response))}
<.loading class="size-4" />
{gettext("Thinking...")}
{@error}
<.button
size="xs"
variant="ghost"
color="danger"
phx-click="retry"
phx-target={@myself}
class="mt-2"
>
{gettext("Retry")}
"""
end
@impl true
def handle_event("send_message", %{"message" => ""}, socket), do: {:noreply, socket}
def handle_event("send_message", %{"message" => text}, socket),
do: do_send_message(socket, text)
def handle_event("clear_chat", _params, socket) do
{:noreply,
socket
|> assign(:messages, [])
|> assign(:current_response, "")
|> assign(:error, nil)}
end
def handle_event("retry", _params, socket) do
case List.last(socket.assigns.messages) do
%{role: "user"} = last_message ->
socket
|> assign(:messages, Enum.drop(socket.assigns.messages, -1))
|> do_send_message(last_message.content)
_ ->
{:noreply, assign(socket, :error, nil)}
end
end
defp do_send_message(socket, text) do
parent_pid = self()
component_id = socket.assigns.id
user_message = %{role: "user", content: String.trim(text)}
messages = socket.assigns.messages ++ [user_message]
chat_module = socket.assigns.chat_module
chat_context = socket.assigns.chat_context
Task.Supervisor.start_child(MusicLibrary.TaskSupervisor, fn ->
case chat_module.stream_response(messages, chat_context, fn chunk ->
Phoenix.LiveView.send_update(parent_pid, __MODULE__,
id: component_id,
chunk: chunk
)
end) do
:ok ->
Phoenix.LiveView.send_update(parent_pid, __MODULE__,
id: component_id,
done: true
)
{:error, reason} ->
Logger.error("Chat streaming error: #{reason}")
Phoenix.LiveView.send_update(parent_pid, __MODULE__,
id: component_id,
error: gettext("Something went wrong. Please try again.")
)
end
end)
{:noreply,
socket
|> assign(:messages, messages)
|> assign(:loading, true)
|> assign(:current_response, "")
|> assign(:error, nil)}
end
defp message_classes("user") do
"ml-auto bg-blue-500 dark:bg-blue-600 text-white"
end
defp message_classes("assistant") do
"bg-zinc-100 dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100"
end
defp message_classes(_), do: ""
end