diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 2ecd5d8e..52726374 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -117,7 +117,9 @@ Last.fm schemas (separate, not Ecto-persisted to main DB):
| `Assets.Image` / `Assets.Transform` | Image processing via Vix (libvips) |
| `Colors.ColorFrequencyExtractor` | Color extraction via pixel sampling/histogram |
| `Colors.EdgeWeightedExtractor` | Color extraction weighted by Sobel edge detection |
-| `RecordChat` | OpenAI-powered streaming chat about a record (web search enabled) |
+| `Chat` | Behaviour for streaming AI chat (`stream_response/3` callback) |
+| `RecordChat` | Chat implementation for records (OpenAI streaming, web search enabled) |
+| `ArtistChat` | Chat implementation for artists (OpenAI streaming, uses Wikipedia/artist context) |
| `FormatNumber` | Number formatting utility |
---
@@ -240,7 +242,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
| `StatsLive.TopAlbums` | StatsLive.Index | Top albums by period (assign_async) |
| `StatsLive.TopArtists` | StatsLive.Index | Top artists by period (assign_async) |
| `UniversalSearchLive.Index` | Layout (global) | Cmd+K search modal |
-| `RecordChat` | CollectionLive.Show, WishlistLive.Show | AI chat sheet for a record (OpenAI streaming) |
+| `Chat` | CollectionLive.Show, WishlistLive.Show, ArtistLive.Show | AI chat sheet (OpenAI streaming, configurable per entity) |
### Shared Component Modules (lib/music_library_web/components/)
@@ -257,7 +259,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
| `BarcodeScanner` | Barcode scanning UI (uses barcode-detector JS) |
| `Release` | MusicBrainz release display |
| `Notes` | Markdown note rendering |
-| `RecordChat` | AI record chat sheet with streaming responses |
+| `Chat` | AI chat sheet with streaming responses (generic, used for records and artists) |
| `Markdown` | Markdown-to-HTML conversion with `[[double bracket]]` link syntax |
| `Duration` | Milliseconds to human-readable duration formatting |
@@ -288,7 +290,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
| `UniversalSearchNavigation` | External | Keyboard navigation in search modal (via `create-navigation-hook` factory) |
| `RecordPickerNavigation` | External | Keyboard navigation in record picker (via `create-navigation-hook` factory) |
| `SortableList` | External (`assets/js/hooks/`) | Drag-and-drop reordering of record set items (uses sortablejs) |
-| Various `.ColocatedHooks` | Colocated (in .heex) | Inline hooks prefixed with `.` (includes `.ScrollBottom` for RecordChat) |
+| Various `.ColocatedHooks` | Colocated (in .heex) | Inline hooks prefixed with `.` (includes `.ScrollBottom` for Chat) |
### JS Event Listeners (app.js)
diff --git a/lib/music_library/artist_chat.ex b/lib/music_library/artist_chat.ex
new file mode 100644
index 00000000..d24d5efc
--- /dev/null
+++ b/lib/music_library/artist_chat.ex
@@ -0,0 +1,44 @@
+defmodule MusicLibrary.ArtistChat do
+ @behaviour MusicLibrary.Chat
+
+ alias MusicLibrary.Artists.ArtistInfo
+
+ @impl true
+ def stream_response(messages, {artist, artist_info}, callback) do
+ instructions = build_instructions(artist, artist_info)
+
+ OpenAI.chat_stream(messages, on_chunk: callback, instructions: instructions)
+ end
+
+ defp build_instructions(artist, artist_info) do
+ context = build_context(artist, artist_info)
+
+ """
+ You are a knowledgeable music assistant. Answer questions about the artist \
+ the user is currently viewing. Use the provided artist information as your \
+ primary reference, and use web search to find additional up-to-date \
+ information when helpful. Be concise and accurate. When unsure, say so.
+
+ Artist information:
+ #{context}
+ """
+ end
+
+ defp build_context(artist, artist_info) do
+ country = ArtistInfo.country(artist_info)
+ description = ArtistInfo.wikipedia_description(artist_info)
+ summary = ArtistInfo.wikipedia_summary(artist_info)
+
+ parts =
+ [
+ {"Name", artist.name},
+ {"Country", country.name},
+ {"Description", description},
+ {"Biography", summary}
+ ]
+ |> Enum.reject(fn {_label, value} -> value in [nil, ""] end)
+ |> Enum.map_join("\n", fn {label, value} -> "#{label}: #{value}" end)
+
+ parts
+ end
+end
diff --git a/lib/music_library/chat.ex b/lib/music_library/chat.ex
new file mode 100644
index 00000000..f0b3a2c1
--- /dev/null
+++ b/lib/music_library/chat.ex
@@ -0,0 +1,7 @@
+defmodule MusicLibrary.Chat do
+ @callback stream_response(
+ messages :: list(map()),
+ context :: term(),
+ callback :: (String.t() -> any())
+ ) :: :ok | {:error, term()}
+end
diff --git a/lib/music_library/record_chat.ex b/lib/music_library/record_chat.ex
index 97a01e21..e96ae60f 100644
--- a/lib/music_library/record_chat.ex
+++ b/lib/music_library/record_chat.ex
@@ -1,7 +1,10 @@
defmodule MusicLibrary.RecordChat do
+ @behaviour MusicLibrary.Chat
+
alias MusicLibrary.Records.Record
- def stream_response(messages, record, embedding_text, callback) do
+ @impl true
+ def stream_response(messages, {record, embedding_text}, callback) do
instructions = build_instructions(record, embedding_text)
OpenAI.chat_stream(messages, on_chunk: callback, instructions: instructions)
diff --git a/lib/music_library_web/components/record_chat.ex b/lib/music_library_web/components/chat.ex
similarity index 91%
rename from lib/music_library_web/components/record_chat.ex
rename to lib/music_library_web/components/chat.ex
index 7f8110ae..d8a47020 100644
--- a/lib/music_library_web/components/record_chat.ex
+++ b/lib/music_library_web/components/chat.ex
@@ -1,9 +1,8 @@
-defmodule MusicLibraryWeb.Components.RecordChat do
+defmodule MusicLibraryWeb.Components.Chat do
use MusicLibraryWeb, :live_component
require Logger
- alias MusicLibrary.RecordChat
alias MusicLibraryWeb.Markdown
def open(id), do: Fluxon.open_dialog(id)
@@ -55,7 +54,7 @@ defmodule MusicLibraryWeb.Components.RecordChat do
>
- {gettext("Chat about %{title}", title: @record.title)}
+ {gettext("Chat about %{title}", title: @title)}
<.button
:if={@messages != []}
@@ -82,10 +81,7 @@ defmodule MusicLibraryWeb.Components.RecordChat do
name="hero-chat-bubble-left-right"
class="size-12 mb-4 text-zinc-300 dark:text-zinc-600"
/>
-
{gettext("Ask anything about this album")}
-
- {gettext("e.g. \"What genre is this?\", \"Tell me about the artists\"")}
-
+
{@empty_prompt}
- case RecordChat.stream_response(messages, record, embedding_text, fn chunk ->
+ case chat_module.stream_response(messages, chat_context, fn chunk ->
Phoenix.LiveView.send_update(parent_pid, __MODULE__,
id: component_id,
chunk: chunk
@@ -232,7 +228,7 @@ defmodule MusicLibraryWeb.Components.RecordChat do
)
{:error, reason} ->
- Logger.error("RecordChat streaming error: #{reason}")
+ Logger.error("Chat streaming error: #{reason}")
Phoenix.LiveView.send_update(parent_pid, __MODULE__,
id: component_id,
diff --git a/lib/music_library_web/live/artist_live/show.html.heex b/lib/music_library_web/live/artist_live/show.html.heex
index 1d86793a..bddec309 100644
--- a/lib/music_library_web/live/artist_live/show.html.heex
+++ b/lib/music_library_web/live/artist_live/show.html.heex
@@ -21,6 +21,18 @@
data-slot="icon"
/>
+ <.button
+ variant="soft"
+ phx-click={MusicLibraryWeb.Components.Chat.open("artist-chat-sheet")}
+ >
+ {gettext("Chat about artist")}
+ <.icon
+ name="hero-chat-bubble-left-right"
+ class="h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
<.dropdown id={"actions-#{@artist.musicbrainz_id}"} placement="bottom-end">
<:toggle>
<.button variant="soft">
@@ -332,6 +344,17 @@
musicbrainz_id={@artist.musicbrainz_id}
/>
+ <.live_component
+ id="artist-chat"
+ sheet_id="artist-chat-sheet"
+ module={MusicLibraryWeb.Components.Chat}
+ title={@artist.name}
+ chat_module={MusicLibrary.ArtistChat}
+ chat_context={{@artist, @artist_info}}
+ placeholder={gettext("Ask about this artist...")}
+ empty_prompt={gettext("Ask anything about this artist")}
+ />
+
<.structured_modal
:if={@live_action == :edit}
id="artist-info-modal"
diff --git a/lib/music_library_web/live/collection_live/show.html.heex b/lib/music_library_web/live/collection_live/show.html.heex
index a8253400..4efccc54 100644
--- a/lib/music_library_web/live/collection_live/show.html.heex
+++ b/lib/music_library_web/live/collection_live/show.html.heex
@@ -28,7 +28,7 @@
<.button
variant="soft"
- phx-click={MusicLibraryWeb.Components.RecordChat.open("record-chat-sheet")}
+ phx-click={MusicLibraryWeb.Components.Chat.open("record-chat-sheet")}
>
{gettext("Chat about album")}
<.icon
@@ -342,9 +342,12 @@
<.live_component
id="record-chat"
sheet_id="record-chat-sheet"
- module={MusicLibraryWeb.Components.RecordChat}
- record={@record}
- embedding_text={@embedding_text}
+ module={MusicLibraryWeb.Components.Chat}
+ title={@record.title}
+ chat_module={MusicLibrary.RecordChat}
+ chat_context={{@record, @embedding_text}}
+ placeholder={gettext("Ask about this album...")}
+ empty_prompt={gettext("Ask anything about this album")}
/>
<.structured_modal
diff --git a/lib/music_library_web/live/wishlist_live/show.html.heex b/lib/music_library_web/live/wishlist_live/show.html.heex
index 900c39cb..bfed8631 100644
--- a/lib/music_library_web/live/wishlist_live/show.html.heex
+++ b/lib/music_library_web/live/wishlist_live/show.html.heex
@@ -16,7 +16,7 @@
<.button_group>
<.button
variant="soft"
- phx-click={MusicLibraryWeb.Components.RecordChat.open("record-chat-sheet")}
+ phx-click={MusicLibraryWeb.Components.Chat.open("record-chat-sheet")}
>
{gettext("Chat about album")}
<.icon
@@ -307,9 +307,12 @@
<.live_component
id="record-chat"
sheet_id="record-chat-sheet"
- module={MusicLibraryWeb.Components.RecordChat}
- record={@record}
- embedding_text={@embedding_text}
+ module={MusicLibraryWeb.Components.Chat}
+ title={@record.title}
+ chat_module={MusicLibrary.RecordChat}
+ chat_context={{@record, @embedding_text}}
+ placeholder={gettext("Ask about this album...")}
+ empty_prompt={gettext("Ask anything about this album")}
/>
<.structured_modal
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index f02c1bd8..1a81c357 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -1951,7 +1951,7 @@ msgstr ""
msgid "Record sets"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#: lib/music_library_web/components/release.ex
#, elixir-autogen, elixir-format
msgid "Retry"
@@ -1962,17 +1962,19 @@ msgstr ""
msgid "Based on genres, artists, and musical style"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/live/collection_live/show.html.heex
+#: lib/music_library_web/live/wishlist_live/show.html.heex
#, elixir-autogen, elixir-format
msgid "Ask about this album..."
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/live/collection_live/show.html.heex
+#: lib/music_library_web/live/wishlist_live/show.html.heex
#, elixir-autogen, elixir-format
msgid "Ask anything about this album"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Chat about %{title}"
msgstr ""
@@ -1983,27 +1985,37 @@ msgstr ""
msgid "Chat about album"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Clear chat"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Send message"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Thinking..."
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
-#, elixir-autogen, elixir-format
-msgid "e.g. \"What genre is this?\", \"Tell me about the artists\""
-msgstr ""
-
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Something went wrong. Please try again."
msgstr ""
+
+#: lib/music_library_web/live/artist_live/show.html.heex
+#, elixir-autogen, elixir-format
+msgid "Ask about this artist..."
+msgstr ""
+
+#: lib/music_library_web/live/artist_live/show.html.heex
+#, elixir-autogen, elixir-format
+msgid "Ask anything about this artist"
+msgstr ""
+
+#: lib/music_library_web/live/artist_live/show.html.heex
+#, elixir-autogen, elixir-format
+msgid "Chat about artist"
+msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 1770c3e4..898dcbb6 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -1951,7 +1951,7 @@ msgstr ""
msgid "Record sets"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#: lib/music_library_web/components/release.ex
#, elixir-autogen, elixir-format
msgid "Retry"
@@ -1962,17 +1962,19 @@ msgstr ""
msgid "Based on genres, artists, and musical style"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/live/collection_live/show.html.heex
+#: lib/music_library_web/live/wishlist_live/show.html.heex
#, elixir-autogen, elixir-format
msgid "Ask about this album..."
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/live/collection_live/show.html.heex
+#: lib/music_library_web/live/wishlist_live/show.html.heex
#, elixir-autogen, elixir-format
msgid "Ask anything about this album"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Chat about %{title}"
msgstr ""
@@ -1983,27 +1985,37 @@ msgstr ""
msgid "Chat about album"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Clear chat"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Send message"
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Thinking..."
msgstr ""
-#: lib/music_library_web/components/record_chat.ex
-#, elixir-autogen, elixir-format
-msgid "e.g. \"What genre is this?\", \"Tell me about the artists\""
-msgstr ""
-
-#: lib/music_library_web/components/record_chat.ex
+#: lib/music_library_web/components/chat.ex
#, elixir-autogen, elixir-format
msgid "Something went wrong. Please try again."
msgstr ""
+
+#: lib/music_library_web/live/artist_live/show.html.heex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Ask about this artist..."
+msgstr ""
+
+#: lib/music_library_web/live/artist_live/show.html.heex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Ask anything about this artist"
+msgstr ""
+
+#: lib/music_library_web/live/artist_live/show.html.heex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Chat about artist"
+msgstr ""