fix: A2UI surfaces
This commit is contained in:
@@ -352,7 +352,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
@spec chat_surface(term()) :: term()
|
@spec chat_surface(term()) :: term()
|
||||||
def chat_surface(assigns) do
|
def chat_surface(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<details id={@surface.id} class={["chat-inline-surface", "chat-inline-surface-#{@surface.type}"]} data-testid="chat-inline-surface" open={Map.get(@surface, :expanded?, false)}>
|
<details id={@surface.id} class={["chat-inline-surface", "chat-inline-surface-#{@surface.type}"]} data-testid="chat-inline-surface" data-expanded={Map.get(@surface, :expanded?, false)} open={Map.get(@surface, :expanded?, false)}>
|
||||||
<summary class="chat-inline-surface-header">
|
<summary class="chat-inline-surface-header">
|
||||||
<span class="chat-inline-surface-icon"><%= surface_icon(@surface.type) %></span>
|
<span class="chat-inline-surface-icon"><%= surface_icon(@surface.type) %></span>
|
||||||
<span class="chat-inline-surface-title"><%= surface_title(@surface) %></span>
|
<span class="chat-inline-surface-title"><%= surface_title(@surface) %></span>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
|
|||||||
tool_markers: tool_markers,
|
tool_markers: tool_markers,
|
||||||
inline_surfaces:
|
inline_surfaces:
|
||||||
ToolSurfaces.build_render_surfaces(tool_markers, message.id, assigns)
|
ToolSurfaces.build_render_surfaces(tool_markers, message.id, assigns)
|
||||||
|> mark_latest_surface_expanded(assigns),
|
|> mark_surfaces_expanded(assigns),
|
||||||
tool_surfaces: []
|
tool_surfaces: []
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -125,15 +125,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp mark_latest_surface_expanded([], _assigns), do: []
|
defp mark_surfaces_expanded([], _assigns), do: []
|
||||||
|
|
||||||
defp mark_latest_surface_expanded(surfaces, assigns) do
|
defp mark_surfaces_expanded(surfaces, assigns) do
|
||||||
dismissed = Map.get(assigns, :chat_editor_dismissed_surfaces, MapSet.new())
|
dismissed = Map.get(assigns, :chat_editor_dismissed_surfaces, MapSet.new())
|
||||||
|
|
||||||
surfaces
|
surfaces
|
||||||
|> Enum.reject(&MapSet.member?(dismissed, &1.id))
|
|> Enum.reject(&MapSet.member?(dismissed, &1.id))
|
||||||
|> Enum.with_index()
|
|> Enum.map(&Map.put(&1, :expanded?, true))
|
||||||
|> Enum.map(fn {surface, index} -> Map.put(surface, :expanded?, index == length(surfaces) - 1) end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp pending_user_message(_messages, nil), do: nil
|
defp pending_user_message(_messages, nil), do: nil
|
||||||
@@ -157,7 +156,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
|
|||||||
request
|
request
|
||||||
|> ToolTracking.tool_markers_from_events()
|
|> ToolTracking.tool_markers_from_events()
|
||||||
|> ToolSurfaces.build_render_surfaces("streaming-#{conversation_id}", assigns)
|
|> ToolSurfaces.build_render_surfaces("streaming-#{conversation_id}", assigns)
|
||||||
|> mark_latest_surface_expanded(assigns)
|
|> mark_surfaces_expanded(assigns)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp translated(text, bindings \\ %{}),
|
defp translated(text, bindings \\ %{}),
|
||||||
|
|||||||
@@ -780,6 +780,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.syncExpandedSurfaces = () => {
|
||||||
|
this.el
|
||||||
|
.querySelectorAll(".chat-inline-surface[data-expanded='true']")
|
||||||
|
.forEach((surface) => {
|
||||||
|
surface.open = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
this.handleScroll = () => {
|
this.handleScroll = () => {
|
||||||
if (!this.scrollContainer) {
|
if (!this.scrollContainer) {
|
||||||
this.stickToBottom = true;
|
this.stickToBottom = true;
|
||||||
@@ -823,12 +831,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
this.el.addEventListener("keydown", this.handleKeyDown);
|
this.el.addEventListener("keydown", this.handleKeyDown);
|
||||||
|
|
||||||
this.syncScrollContainer();
|
this.syncScrollContainer();
|
||||||
|
this.syncExpandedSurfaces();
|
||||||
this.autoResize();
|
this.autoResize();
|
||||||
window.requestAnimationFrame(() => this.scrollToBottom(true));
|
window.requestAnimationFrame(() => this.scrollToBottom(true));
|
||||||
},
|
},
|
||||||
|
|
||||||
updated() {
|
updated() {
|
||||||
this.syncScrollContainer();
|
this.syncScrollContainer();
|
||||||
|
this.syncExpandedSurfaces();
|
||||||
this.autoResize();
|
this.autoResize();
|
||||||
window.requestAnimationFrame(() => this.scrollToBottom());
|
window.requestAnimationFrame(() => this.scrollToBottom());
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2280,6 +2280,77 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
refute dismissed_html =~ ~s(data-testid="chat-inline-surface")
|
refute dismissed_html =~ ~s(data-testid="chat-inline-surface")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "chat editor keeps every non-dismissed A2UI surface expanded" do
|
||||||
|
assert {:ok, conversation} = AI.start_chat(%{title: "Surface Chat", model: "gpt-4.1"})
|
||||||
|
|
||||||
|
now = Persistence.now_ms()
|
||||||
|
|
||||||
|
Repo.insert!(
|
||||||
|
BDS.AI.ChatMessage.changeset(%BDS.AI.ChatMessage{}, %{
|
||||||
|
conversation_id: conversation.id,
|
||||||
|
role: :user,
|
||||||
|
content: "Show two updates",
|
||||||
|
created_at: now
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.insert!(
|
||||||
|
BDS.AI.ChatMessage.changeset(%BDS.AI.ChatMessage{}, %{
|
||||||
|
conversation_id: conversation.id,
|
||||||
|
role: :assistant,
|
||||||
|
content: "Here are the updates.",
|
||||||
|
tool_calls:
|
||||||
|
Jason.encode!([
|
||||||
|
%{
|
||||||
|
"id" => "call-card-old",
|
||||||
|
"name" => "render_card",
|
||||||
|
"arguments" => %{
|
||||||
|
"title" => "Earlier Missing Data",
|
||||||
|
"body" => "The first data request needs review."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"id" => "call-card-new",
|
||||||
|
"name" => "render_card",
|
||||||
|
"arguments" => %{
|
||||||
|
"title" => "Latest Missing Data",
|
||||||
|
"body" => "The second data request needs review."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
created_at: now + 1
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
|
html =
|
||||||
|
render_click(view, "pin_sidebar_item", %{
|
||||||
|
"route" => "chat",
|
||||||
|
"id" => conversation.id,
|
||||||
|
"title" => conversation.title,
|
||||||
|
"subtitle" => conversation.model || "chat"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert length(:binary.matches(html, ~s(data-testid="chat-inline-surface"))) == 2
|
||||||
|
assert length(:binary.matches(html, "data-expanded")) == 2
|
||||||
|
assert html =~ "Earlier Missing Data"
|
||||||
|
assert html =~ "The first data request needs review."
|
||||||
|
assert html =~ "Latest Missing Data"
|
||||||
|
assert html =~ "The second data request needs review."
|
||||||
|
end
|
||||||
|
|
||||||
|
test "chat editor hook reopens server-expanded A2UI surfaces after patches" do
|
||||||
|
live_js = File.read!(Path.expand("../../../priv/ui/live.js", __DIR__))
|
||||||
|
chat_editor = File.read!(Path.expand("../../../lib/bds/desktop/shell_live/chat_editor.ex", __DIR__))
|
||||||
|
|
||||||
|
assert chat_editor =~ "data-expanded={Map.get(@surface, :expanded?, false)}"
|
||||||
|
assert live_js =~ "this.syncExpandedSurfaces = () =>"
|
||||||
|
assert live_js =~ "querySelectorAll(\".chat-inline-surface[data-expanded='true']\")"
|
||||||
|
assert live_js =~ "surface.open = true;"
|
||||||
|
assert live_js =~ "this.syncExpandedSurfaces();"
|
||||||
|
end
|
||||||
|
|
||||||
test "chat editor folds tool-only assistant steps into the final assistant answer" do
|
test "chat editor folds tool-only assistant steps into the final assistant answer" do
|
||||||
assert {:ok, conversation} = AI.start_chat(%{title: "Tool Chat", model: "gpt-4.1"})
|
assert {:ok, conversation} = AI.start_chat(%{title: "Tool Chat", model: "gpt-4.1"})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user