fix: ai chat styling and some crashes
This commit is contained in:
@@ -482,7 +482,7 @@ defmodule BDS.AITest do
|
||||
|
||||
test "chat persists user, tool, and assistant messages with usage and blog stats prompt augmentation" do
|
||||
{:ok, project} = create_project_fixture("AI Chat")
|
||||
:ok = seed_project_content(project.id)
|
||||
_fixtures = seed_project_content(project.id)
|
||||
|
||||
assert {:ok, _endpoint} =
|
||||
BDS.AI.put_endpoint(
|
||||
@@ -530,8 +530,15 @@ defmodule BDS.AITest do
|
||||
message["role"] == "system" and String.contains?(message["content"], "Posts: 1") and
|
||||
String.contains?(message["content"], "Media: 1") and
|
||||
String.contains?(message["content"], "Available blog data tools") and
|
||||
String.contains?(message["content"], "get_blog_stats") and
|
||||
String.contains?(message["content"], "list_posts") and
|
||||
String.contains?(message["content"], "list_media")
|
||||
String.contains?(message["content"], "get_media") and
|
||||
String.contains?(message["content"], "view_image") and
|
||||
String.contains?(message["content"], "update_post_metadata") and
|
||||
String.contains?(message["content"], "Available UI Render Tools") and
|
||||
String.contains?(message["content"], "render_chart") and
|
||||
String.contains?(message["content"], "heatmap") and
|
||||
String.contains?(message["content"], "render_tabs")
|
||||
end)
|
||||
|
||||
tool_descriptions =
|
||||
@@ -540,24 +547,66 @@ defmodule BDS.AITest do
|
||||
{get_in(tool, ["function", "name"]), get_in(tool, ["function", "description"])}
|
||||
end)
|
||||
|
||||
assert tool_descriptions["blog_stats"] =~ "aggregate"
|
||||
expected_old_app_tools = [
|
||||
"get_blog_stats",
|
||||
"search_posts",
|
||||
"read_post",
|
||||
"read_post_by_slug",
|
||||
"list_posts",
|
||||
"get_media",
|
||||
"list_media",
|
||||
"view_image",
|
||||
"update_post_metadata",
|
||||
"update_media_metadata",
|
||||
"list_tags",
|
||||
"list_categories",
|
||||
"get_post_backlinks",
|
||||
"get_post_outlinks",
|
||||
"get_post_media",
|
||||
"get_media_posts",
|
||||
"render_chart",
|
||||
"render_table",
|
||||
"render_form",
|
||||
"render_card",
|
||||
"render_metric",
|
||||
"render_list",
|
||||
"render_tabs",
|
||||
"render_mindmap"
|
||||
]
|
||||
|
||||
assert Enum.all?(expected_old_app_tools, &Map.has_key?(tool_descriptions, &1))
|
||||
assert tool_descriptions["get_blog_stats"] =~ "comprehensive blog statistics"
|
||||
assert tool_descriptions["list_posts"] =~ "titles"
|
||||
assert tool_descriptions["list_posts"] =~ "URLs"
|
||||
assert tool_descriptions["list_media"] =~ "filenames"
|
||||
assert tool_descriptions["render_chart"] =~ "interactive chart"
|
||||
assert tool_descriptions["render_chart"] =~ "heatmap"
|
||||
assert tool_descriptions["render_table"] =~ "tabular data"
|
||||
assert tool_descriptions["render_tabs"] =~ "multiple tabs"
|
||||
|
||||
render_chart_schema =
|
||||
first_request.tools
|
||||
|> Enum.find(&(get_in(&1, ["function", "name"]) == "render_chart"))
|
||||
|> get_in(["function", "parameters", "properties"])
|
||||
|
||||
assert get_in(render_chart_schema, ["chartType", "enum"]) == [
|
||||
"bar",
|
||||
"stacked-bar",
|
||||
"line",
|
||||
"area",
|
||||
"pie",
|
||||
"donut",
|
||||
"heatmap"
|
||||
]
|
||||
|
||||
assert get_in(render_chart_schema, ["series", "items", "properties", "segments"]) != nil
|
||||
|
||||
assert Enum.any?(second_request.messages, fn message -> message["role"] == "tool" end)
|
||||
end
|
||||
|
||||
test "non-stat chat tools expose concrete project data" do
|
||||
{:ok, project} = create_project_fixture("Concrete Tools")
|
||||
:ok = seed_project_content(project.id)
|
||||
|
||||
[post] =
|
||||
Repo.all(
|
||||
from post in Post,
|
||||
where: post.project_id == ^project.id,
|
||||
select: post
|
||||
)
|
||||
%{post: post, media: media} = seed_project_content(project.id)
|
||||
|
||||
assert %{posts: [listed_post], total: 1} =
|
||||
BDS.AI.ChatTools.execute("list_posts", %{"limit" => 5}, project.id)
|
||||
@@ -567,10 +616,68 @@ defmodule BDS.AITest do
|
||||
assert listed_post["url"] == "/posts/#{post.slug}"
|
||||
assert listed_post["updated_at"] == post.updated_at
|
||||
|
||||
assert %{post: read_post} =
|
||||
BDS.AI.ChatTools.execute("read_post", %{"postId" => post.id}, project.id)
|
||||
|
||||
assert read_post["title"] == post.title
|
||||
assert read_post["content"] == post.content
|
||||
|
||||
assert [listed_media] = BDS.AI.ChatTools.execute("list_media", %{"limit" => 5}, project.id)
|
||||
assert listed_media.filename == "image.png"
|
||||
assert listed_media.mime_type == "image/png"
|
||||
assert listed_media.updated_at
|
||||
|
||||
assert %{media: loaded_media} =
|
||||
BDS.AI.ChatTools.execute("get_media", %{"mediaId" => media.id}, project.id)
|
||||
|
||||
assert loaded_media.id == media.id
|
||||
assert loaded_media.title == "Hero"
|
||||
|
||||
assert %{linked_by: []} =
|
||||
BDS.AI.ChatTools.execute("get_post_backlinks", %{"postId" => post.id}, project.id)
|
||||
|
||||
assert %{links_to: []} =
|
||||
BDS.AI.ChatTools.execute("get_post_outlinks", %{"postId" => post.id}, project.id)
|
||||
|
||||
assert %{media: []} =
|
||||
BDS.AI.ChatTools.execute("get_post_media", %{"postId" => post.id}, project.id)
|
||||
|
||||
assert %{posts: []} =
|
||||
BDS.AI.ChatTools.execute("get_media_posts", %{"mediaId" => media.id}, project.id)
|
||||
|
||||
assert %{success: true, post: updated_post} =
|
||||
BDS.AI.ChatTools.execute(
|
||||
"update_post_metadata",
|
||||
%{"postId" => post.id, "title" => "Updated AI Post"},
|
||||
project.id
|
||||
)
|
||||
|
||||
assert updated_post["title"] == "Updated AI Post"
|
||||
|
||||
assert %{success: true, media: updated_media} =
|
||||
BDS.AI.ChatTools.execute(
|
||||
"update_media_metadata",
|
||||
%{"mediaId" => media.id, "alt" => "Updated alt"},
|
||||
project.id
|
||||
)
|
||||
|
||||
assert updated_media.alt == "Updated alt"
|
||||
|
||||
assert %{
|
||||
type: "chart",
|
||||
chart_type: "heatmap",
|
||||
series: [%{"label" => "2026", "segments" => [%{"label" => "Jan", "value" => 2}]}]
|
||||
} =
|
||||
BDS.AI.ChatTools.execute(
|
||||
"render_chart",
|
||||
%{
|
||||
"chartType" => "heatmap",
|
||||
"series" => [
|
||||
%{"label" => "2026", "segments" => [%{"label" => "Jan", "value" => 2}]}
|
||||
]
|
||||
},
|
||||
project.id
|
||||
)
|
||||
end
|
||||
|
||||
test "cancel_chat aborts an in-flight chat turn" do
|
||||
@@ -621,36 +728,39 @@ defmodule BDS.AITest do
|
||||
defp seed_project_content(project_id) do
|
||||
now = Persistence.now_ms()
|
||||
|
||||
Repo.insert!(
|
||||
Post.changeset(%Post{}, %{
|
||||
id: Ecto.UUID.generate(),
|
||||
project_id: project_id,
|
||||
title: "AI Post",
|
||||
slug: "ai-post",
|
||||
excerpt: "Summary",
|
||||
content: "Body",
|
||||
status: :draft,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
do_not_translate: false
|
||||
})
|
||||
)
|
||||
post =
|
||||
Repo.insert!(
|
||||
Post.changeset(%Post{}, %{
|
||||
id: Ecto.UUID.generate(),
|
||||
project_id: project_id,
|
||||
title: "AI Post",
|
||||
slug: "ai-post",
|
||||
excerpt: "Summary",
|
||||
content: "Body",
|
||||
status: :draft,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
do_not_translate: false
|
||||
})
|
||||
)
|
||||
|
||||
Repo.insert!(
|
||||
Media.changeset(%Media{}, %{
|
||||
id: Ecto.UUID.generate(),
|
||||
project_id: project_id,
|
||||
filename: "image.png",
|
||||
original_name: "image.png",
|
||||
mime_type: "image/png",
|
||||
size: 128,
|
||||
file_path: "media/image.png",
|
||||
sidecar_path: "media/image.png.meta",
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
})
|
||||
)
|
||||
media =
|
||||
Repo.insert!(
|
||||
Media.changeset(%Media{}, %{
|
||||
id: Ecto.UUID.generate(),
|
||||
project_id: project_id,
|
||||
filename: "image.png",
|
||||
original_name: "image.png",
|
||||
mime_type: "image/png",
|
||||
size: 128,
|
||||
title: "Hero",
|
||||
file_path: "media/image.png",
|
||||
sidecar_path: "media/image.png.meta",
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
})
|
||||
)
|
||||
|
||||
:ok
|
||||
%{post: post, media: media}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2247,6 +2247,58 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
assert css =~ "line-height: 1.35;"
|
||||
end
|
||||
|
||||
test "chat editor keeps empty input single-line until content grows" do
|
||||
assert {:ok, conversation} = AI.start_chat(%{title: "Input Sizing", model: "gpt-4.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 html =~ ~s(rows="1")
|
||||
assert html =~ ~s(class="chat-input chat-surface-input")
|
||||
|
||||
css = File.read!(Path.expand("../../../priv/ui/app.css", __DIR__))
|
||||
assert css =~ "--chat-input-line-height: 20px;"
|
||||
assert css =~ "--chat-input-min-height: 20px;"
|
||||
assert css =~ ".chat-panel .chat-input-container"
|
||||
assert css =~ "padding: 8px 16px;"
|
||||
assert css =~ "padding: 6px 8px;"
|
||||
assert css =~ ".chat-panel .chat-input-wrapper"
|
||||
assert css =~ "min-height: 30px;"
|
||||
assert css =~ "padding: 4px 6px;"
|
||||
assert css =~ ".chat-panel .chat-input"
|
||||
assert css =~ "box-sizing: border-box;"
|
||||
assert css =~ "margin: 0;"
|
||||
assert css =~ "height: var(--chat-input-min-height);"
|
||||
assert css =~ "min-height: var(--chat-input-min-height);"
|
||||
assert css =~ "overflow-y: hidden;"
|
||||
assert css =~ ".chat-panel .chat-send-button"
|
||||
assert css =~ "width: 22px;"
|
||||
assert css =~ "height: 22px;"
|
||||
assert css =~ "max-width: 22px;"
|
||||
assert css =~ "max-height: 22px;"
|
||||
assert css =~ "padding: 0;"
|
||||
|
||||
live_js = File.read!(Path.expand("../../../priv/ui/live.js", __DIR__))
|
||||
|
||||
assert live_js =~
|
||||
"minHeight = parseFloat(styles.getPropertyValue(\"--chat-input-min-height\"))"
|
||||
|
||||
assert live_js =~ "textarea.value.trim() === \"\""
|
||||
assert live_js =~ "textarea.rows = 1;"
|
||||
assert live_js =~ "textarea.style.minHeight = `${minHeight}px`;"
|
||||
assert live_js =~ "textarea.style.height = `${minHeight}px`;"
|
||||
assert live_js =~ "textarea.style.maxHeight = `${minHeight}px`;"
|
||||
assert live_js =~ "textarea.style.height = \"0px\";"
|
||||
assert live_js =~ "textarea.style.overflowY = nextHeight >= maxHeight ? \"auto\" : \"hidden\""
|
||||
end
|
||||
|
||||
test "chat editor groups selector models by provider and uses catalog labels" do
|
||||
updated_at = Persistence.now_ms()
|
||||
|
||||
|
||||
@@ -3,6 +3,13 @@ defmodule BDS.DesktopTest do
|
||||
|
||||
import Plug.Test
|
||||
|
||||
defmodule FakeShutdown do
|
||||
def request_quit do
|
||||
send(Application.fetch_env!(:bds, :desktop_shutdown_test_pid), :quit_requested)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
test "desktop configuration no longer uses a pending adapter" do
|
||||
assert Application.get_env(:bds, BDS.Application)[:desktop_adapter] == :desktop
|
||||
end
|
||||
@@ -99,6 +106,38 @@ defmodule BDS.DesktopTest do
|
||||
assert menu_item(groups, :metadata_diff).shortcut == nil
|
||||
end
|
||||
|
||||
test "native menu quit requests app-owned shutdown" do
|
||||
previous_module = Application.get_env(:bds, :desktop_shutdown_module)
|
||||
previous_pid = Application.get_env(:bds, :desktop_shutdown_test_pid)
|
||||
|
||||
Application.put_env(:bds, :desktop_shutdown_module, FakeShutdown)
|
||||
Application.put_env(:bds, :desktop_shutdown_test_pid, self())
|
||||
|
||||
on_exit(fn ->
|
||||
restore_env(:desktop_shutdown_module, previous_module)
|
||||
restore_env(:desktop_shutdown_test_pid, previous_pid)
|
||||
end)
|
||||
|
||||
assert {:noreply, %{}} = BDS.Desktop.MenuBar.handle_event("quit", %{})
|
||||
assert_receive :quit_requested
|
||||
end
|
||||
|
||||
test "icon menu quit requests app-owned shutdown" do
|
||||
previous_module = Application.get_env(:bds, :desktop_shutdown_module)
|
||||
previous_pid = Application.get_env(:bds, :desktop_shutdown_test_pid)
|
||||
|
||||
Application.put_env(:bds, :desktop_shutdown_module, FakeShutdown)
|
||||
Application.put_env(:bds, :desktop_shutdown_test_pid, self())
|
||||
|
||||
on_exit(fn ->
|
||||
restore_env(:desktop_shutdown_module, previous_module)
|
||||
restore_env(:desktop_shutdown_test_pid, previous_pid)
|
||||
end)
|
||||
|
||||
assert {:noreply, %{}} = BDS.Desktop.Menu.handle_event("quit", %{})
|
||||
assert_receive :quit_requested
|
||||
end
|
||||
|
||||
test "desktop root html is a LiveView shell and references only the live bootstrap assets" do
|
||||
conn = conn(:get, "/?k=#{Desktop.Auth.login_key()}")
|
||||
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))
|
||||
@@ -178,4 +217,7 @@ defmodule BDS.DesktopTest do
|
||||
Image.new!(3, 2, color: [255, 0, 0])
|
||||
|> Image.write!(:memory, suffix: ".jpg", quality: 85)
|
||||
end
|
||||
|
||||
defp restore_env(key, nil), do: Application.delete_env(:bds, key)
|
||||
defp restore_env(key, value), do: Application.put_env(:bds, key, value)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user