fix: persist a2ui surfaces in the database for chats to re-hydrate on
opening an old chat, unless manually dismissed
This commit is contained in:
@@ -186,4 +186,12 @@ defmodule BDS.AI do
|
||||
|
||||
@spec cancel_chat(String.t()) :: :ok
|
||||
defdelegate cancel_chat(conversation_id), to: Chat
|
||||
|
||||
@spec get_surface_state(String.t()) :: map()
|
||||
defdelegate get_surface_state(conversation_id), to: Chat
|
||||
|
||||
@spec put_surface_state(String.t(), map(), map(), MapSet.t()) ::
|
||||
{:ok, map()} | {:error, term()}
|
||||
defdelegate put_surface_state(conversation_id, surface_data, surface_tabs, dismissed_surfaces),
|
||||
to: Chat
|
||||
end
|
||||
|
||||
@@ -62,6 +62,42 @@ defmodule BDS.AI.Chat do
|
||||
Repo.get(ChatConversation, conversation_id)
|
||||
end
|
||||
|
||||
@spec get_surface_state(String.t()) :: map()
|
||||
def get_surface_state(conversation_id) when is_binary(conversation_id) do
|
||||
case Repo.get(ChatConversation, conversation_id) do
|
||||
%ChatConversation{surface_state: state} when is_map(state) -> state
|
||||
_other -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
@spec put_surface_state(String.t(), map(), map(), MapSet.t()) ::
|
||||
{:ok, map()} | {:error, term()}
|
||||
def put_surface_state(conversation_id, surface_data, surface_tabs, dismissed_surfaces)
|
||||
when is_binary(conversation_id) do
|
||||
case Repo.get(ChatConversation, conversation_id) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
|
||||
%ChatConversation{} = conversation ->
|
||||
state = %{
|
||||
"surface_data" => surface_data,
|
||||
"surface_tabs" => surface_tabs,
|
||||
"dismissed_surfaces" => MapSet.to_list(dismissed_surfaces)
|
||||
}
|
||||
|
||||
conversation
|
||||
|> ChatConversation.changeset(%{
|
||||
surface_state: state,
|
||||
updated_at: Persistence.now_ms()
|
||||
})
|
||||
|> Repo.update()
|
||||
|> case do
|
||||
{:ok, _updated} -> {:ok, state}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete_chat_conversation(String.t()) :: {:ok, :deleted} | {:error, :not_found | term()}
|
||||
def delete_chat_conversation(conversation_id) when is_binary(conversation_id) do
|
||||
case Repo.get(ChatConversation, conversation_id) do
|
||||
|
||||
@@ -11,6 +11,7 @@ defmodule BDS.AI.ChatConversation do
|
||||
title: String.t() | nil,
|
||||
model: String.t() | nil,
|
||||
copilot_session_id: String.t() | nil,
|
||||
surface_state: map() | nil,
|
||||
created_at: integer() | nil,
|
||||
updated_at: integer() | nil
|
||||
}
|
||||
@@ -19,13 +20,14 @@ defmodule BDS.AI.ChatConversation do
|
||||
field :title, :string
|
||||
field :model, :string
|
||||
field :copilot_session_id, :string
|
||||
field :surface_state, :map
|
||||
field :created_at, :integer
|
||||
field :updated_at, :integer
|
||||
end
|
||||
|
||||
def changeset(conversation, attrs) do
|
||||
conversation
|
||||
|> cast(attrs, [:id, :title, :model, :copilot_session_id, :created_at, :updated_at],
|
||||
|> cast(attrs, [:id, :title, :model, :copilot_session_id, :surface_state, :created_at, :updated_at],
|
||||
empty_values: [nil]
|
||||
)
|
||||
|> validate_required([:id, :title, :created_at, :updated_at])
|
||||
|
||||
@@ -116,6 +116,15 @@ defmodule BDS.Desktop.ShellLive.Bridges do
|
||||
{:noreply, assign(socket, :chat_editor_request_refs, refs)}
|
||||
end
|
||||
|
||||
def handle_info({:persist_surface_state, conversation_id}, socket, _callbacks) do
|
||||
send_update(ChatEditor,
|
||||
id: "chat-editor-#{conversation_id}",
|
||||
action: :persist_surface_state
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_info({:chat_editor_toggle_sidebar}, socket, callbacks) do
|
||||
{:noreply,
|
||||
callbacks.refresh_layout.(socket, Workbench.toggle_sidebar(socket.assigns.workbench))}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
@moduledoc false
|
||||
|
||||
require Logger
|
||||
|
||||
use Phoenix.LiveComponent
|
||||
|
||||
import Phoenix.HTML, only: [raw: 1]
|
||||
@@ -37,6 +39,10 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
{:ok, do_note_streaming_content(socket, content)}
|
||||
end
|
||||
|
||||
def update(%{action: :persist_surface_state}, socket) do
|
||||
{:ok, persist_surface_state(socket)}
|
||||
end
|
||||
|
||||
def update(assigns, socket) do
|
||||
socket =
|
||||
socket
|
||||
@@ -97,7 +103,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
socket
|
||||
) do
|
||||
next_data = Map.put(socket.assigns.surface_data, surface_id, fields)
|
||||
{:noreply, assign(socket, :surface_data, next_data) |> build_data()}
|
||||
{:noreply, assign(socket, :surface_data, next_data) |> schedule_surface_state_persist() |> build_data()}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
@@ -111,6 +117,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
:surface_tabs,
|
||||
Map.put(socket.assigns.surface_tabs, surface_id, parse_integer(index))
|
||||
)
|
||||
|> persist_surface_state()
|
||||
|> build_data()
|
||||
|
||||
{:noreply, socket}
|
||||
@@ -120,6 +127,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:dismissed_surfaces, MapSet.put(socket.assigns.dismissed_surfaces, surface_id))
|
||||
|> persist_surface_state()
|
||||
|> build_data()
|
||||
|
||||
{:noreply, socket}
|
||||
@@ -148,14 +156,29 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
defp ensure_state(socket) do
|
||||
conversation_id = socket.assigns.current_tab.id
|
||||
|
||||
persisted = AI.get_surface_state(conversation_id)
|
||||
|
||||
{surface_data, surface_tabs, dismissed_surfaces} =
|
||||
case persisted do
|
||||
state when is_map(state) and map_size(state) > 0 ->
|
||||
{
|
||||
state["surface_data"] || %{},
|
||||
state["surface_tabs"] || %{},
|
||||
MapSet.new(state["dismissed_surfaces"] || [])
|
||||
}
|
||||
|
||||
_other ->
|
||||
{%{}, %{}, MapSet.new()}
|
||||
end
|
||||
|
||||
defaults = %{
|
||||
conversation_id: conversation_id,
|
||||
input: "",
|
||||
model_selector_open?: false,
|
||||
request: nil,
|
||||
surface_data: %{},
|
||||
surface_tabs: %{},
|
||||
dismissed_surfaces: MapSet.new(),
|
||||
surface_data: surface_data,
|
||||
surface_tabs: surface_tabs,
|
||||
dismissed_surfaces: dismissed_surfaces,
|
||||
action_error: nil
|
||||
}
|
||||
|
||||
@@ -819,6 +842,41 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
|
||||
# ── Private helpers ───────────────────────────────────────────────────────
|
||||
|
||||
@surface_state_debounce_ms 500
|
||||
|
||||
defp persist_surface_state(socket) do
|
||||
conversation_id = socket.assigns.conversation_id
|
||||
surface_data = socket.assigns.surface_data
|
||||
surface_tabs = socket.assigns.surface_tabs
|
||||
dismissed_surfaces = socket.assigns.dismissed_surfaces
|
||||
|
||||
case AI.put_surface_state(conversation_id, surface_data, surface_tabs, dismissed_surfaces) do
|
||||
{:ok, _state} ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Failed to persist surface state for conversation #{conversation_id}",
|
||||
reason: inspect(reason)
|
||||
)
|
||||
end
|
||||
|
||||
socket
|
||||
end
|
||||
|
||||
defp schedule_surface_state_persist(socket) do
|
||||
if socket.assigns[:surface_state_timer] do
|
||||
Process.cancel_timer(socket.assigns[:surface_state_timer])
|
||||
end
|
||||
|
||||
timer =
|
||||
Process.send_after(
|
||||
self(),
|
||||
{:persist_surface_state, socket.assigns.conversation_id},
|
||||
@surface_state_debounce_ms
|
||||
)
|
||||
|
||||
assign(socket, :surface_state_timer, timer)
|
||||
end
|
||||
|
||||
defp active_project_id(socket) do
|
||||
socket.assigns[:project_id]
|
||||
|
||||
Reference in New Issue
Block a user