fix: ai chat styling and some crashes

This commit is contained in:
2026-05-01 21:39:05 +02:00
parent b5ebea6ff2
commit f8b8ccabbd
11 changed files with 835 additions and 81 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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