feat: ccloseout of changes for audit
This commit is contained in:
@@ -35,9 +35,11 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp generate_blocking(url, headers, payload, request) do
|
defp generate_blocking(url, headers, payload, request) do
|
||||||
payload_json = encode_payload(payload)
|
payload_json = Jason.encode!(payload)
|
||||||
|
|
||||||
log_request(:blocking, url, request, payload, payload_json)
|
Logger.debug(
|
||||||
|
"AI OpenAI-compatible request operation=#{inspect(Map.get(request, :operation))} model=#{inspect(request.model)} url=#{url} tools=#{payload |> Map.get("tools", []) |> length()} payload_size=#{byte_size(payload_json)}"
|
||||||
|
)
|
||||||
|
|
||||||
case HttpClient.post(url, headers, payload_json) do
|
case HttpClient.post(url, headers, payload_json) do
|
||||||
{:ok, %{status: 200, body: body}} ->
|
{:ok, %{status: 200, body: body}} ->
|
||||||
@@ -75,9 +77,11 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
|
|||||||
# snapshots to `on_stream` as they arrive. The assembled message goes
|
# snapshots to `on_stream` as they arrive. The assembled message goes
|
||||||
# through the same normalization as the blocking path.
|
# through the same normalization as the blocking path.
|
||||||
defp generate_streaming(url, headers, payload, request, on_stream) do
|
defp generate_streaming(url, headers, payload, request, on_stream) do
|
||||||
payload_json = payload |> streaming_payload() |> encode_payload()
|
payload_json = payload |> streaming_payload() |> Jason.encode!()
|
||||||
|
|
||||||
log_request(:streaming, url, request, payload, payload_json)
|
Logger.debug(
|
||||||
|
"AI OpenAI-compatible streaming request operation=#{inspect(Map.get(request, :operation))} model=#{inspect(request.model)} url=#{url} payload_size=#{byte_size(payload_json)}"
|
||||||
|
)
|
||||||
|
|
||||||
sse = SSE.new(on_stream, emit_interval_ms: stream_emit_interval_ms())
|
sse = SSE.new(on_stream, emit_interval_ms: stream_emit_interval_ms())
|
||||||
|
|
||||||
@@ -234,20 +238,6 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
|
|||||||
|> Map.put("stream_options", %{"include_usage" => true})
|
|> Map.put("stream_options", %{"include_usage" => true})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp encode_payload(payload), do: Jason.encode!(payload)
|
|
||||||
|
|
||||||
defp log_request(mode, url, request, payload, payload_json) do
|
|
||||||
tools_segment =
|
|
||||||
case mode do
|
|
||||||
:blocking -> " tools=#{payload |> Map.get("tools", []) |> length()}"
|
|
||||||
:streaming -> ""
|
|
||||||
end
|
|
||||||
|
|
||||||
Logger.debug(
|
|
||||||
"AI OpenAI-compatible#{if mode == :streaming, do: " streaming", else: ""} request operation=#{inspect(Map.get(request, :operation))} model=#{inspect(request.model)} url=#{url}#{tools_segment} payload_size=#{byte_size(payload_json)}"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp log_http_status_error(prefix, status, body) do
|
defp log_http_status_error(prefix, status, body) do
|
||||||
Logger.error("#{prefix} HTTP error status=#{status} body=#{String.slice(body, 0, 2000)}")
|
Logger.error("#{prefix} HTTP error status=#{status} body=#{String.slice(body, 0, 2000)}")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -545,8 +545,13 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|
|
||||||
@spec markdown_html(binary()) :: Phoenix.HTML.Safe.t()
|
@spec markdown_html(binary()) :: Phoenix.HTML.Safe.t()
|
||||||
def markdown_html(content) when is_binary(content) do
|
def markdown_html(content) when is_binary(content) do
|
||||||
|
# Match Earmark's defaults (GFM tables, strikethrough, autolinks); escape
|
||||||
|
# raw HTML to text rather than rendering it, as the old escape: true did.
|
||||||
html =
|
html =
|
||||||
case MDEx.to_html(content) do
|
case MDEx.to_html(content,
|
||||||
|
extension: [table: true, strikethrough: true, autolink: true],
|
||||||
|
render: [escape: true]
|
||||||
|
) do
|
||||||
{:ok, rendered} -> rendered
|
{:ok, rendered} -> rendered
|
||||||
{:error, _reason} -> ""
|
{:error, _reason} -> ""
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -278,8 +278,13 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
|
|
||||||
@spec markdown_html(String.t()) :: Phoenix.HTML.safe()
|
@spec markdown_html(String.t()) :: Phoenix.HTML.safe()
|
||||||
def markdown_html(content) do
|
def markdown_html(content) do
|
||||||
|
# Match Earmark's defaults (GFM tables, strikethrough, autolinks); escape
|
||||||
|
# raw HTML to text rather than rendering it, as the old escape: true did.
|
||||||
html =
|
html =
|
||||||
case MDEx.to_html(content || "") do
|
case MDEx.to_html(content || "",
|
||||||
|
extension: [table: true, strikethrough: true, autolink: true],
|
||||||
|
render: [escape: true]
|
||||||
|
) do
|
||||||
{:ok, rendered} -> rendered
|
{:ok, rendered} -> rendered
|
||||||
{:error, _reason} -> ""
|
{:error, _reason} -> ""
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,24 +3,6 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|
|
||||||
alias BDS.Desktop.ShellLive.SidebarState, as: ShellSidebarState
|
alias BDS.Desktop.ShellLive.SidebarState, as: ShellSidebarState
|
||||||
|
|
||||||
@event_handlers %{
|
|
||||||
"toggle_sidebar_filters" => :toggle_sidebar_filters,
|
|
||||||
"toggle_sidebar_archive" => :toggle_sidebar_archive,
|
|
||||||
"toggle_sidebar_tags" => :toggle_sidebar_tags,
|
|
||||||
"toggle_sidebar_categories" => :toggle_sidebar_categories,
|
|
||||||
"update_sidebar_search" => :update_sidebar_search,
|
|
||||||
"clear_sidebar_search" => :clear_sidebar_search,
|
|
||||||
"clear_sidebar_tags" => :clear_sidebar_tags,
|
|
||||||
"clear_sidebar_categories" => :clear_sidebar_categories,
|
|
||||||
"toggle_sidebar_tag" => :toggle_sidebar_tag,
|
|
||||||
"toggle_sidebar_category" => :toggle_sidebar_category,
|
|
||||||
"select_sidebar_year" => :select_sidebar_year,
|
|
||||||
"select_sidebar_month" => :select_sidebar_month,
|
|
||||||
"clear_sidebar_month" => :clear_sidebar_month,
|
|
||||||
"clear_sidebar_filters" => :clear_sidebar_filters,
|
|
||||||
"load_more_sidebar" => :load_more_sidebar
|
|
||||||
}
|
|
||||||
|
|
||||||
@spec handle(Phoenix.LiveView.Socket.t(), String.t(), map(), (Phoenix.LiveView.Socket.t(),
|
@spec handle(Phoenix.LiveView.Socket.t(), String.t(), map(), (Phoenix.LiveView.Socket.t(),
|
||||||
term() ->
|
term() ->
|
||||||
Phoenix.LiveView.Socket.t())) ::
|
Phoenix.LiveView.Socket.t())) ::
|
||||||
@@ -29,14 +11,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
{:noreply, dispatch(event, socket, params, reload)}
|
{:noreply, dispatch(event, socket, params, reload)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch(event, socket, params, reload) do
|
defp dispatch("toggle_sidebar_filters", socket, _params, reload) do
|
||||||
case Map.fetch(@event_handlers, event) do
|
|
||||||
{:ok, handler} -> handle_event(handler, socket, params, reload)
|
|
||||||
:error -> socket
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp handle_event(:toggle_sidebar_filters, socket, _params, reload) do
|
|
||||||
socket =
|
socket =
|
||||||
ShellSidebarState.put_filter_panel_state(socket, fn state ->
|
ShellSidebarState.put_filter_panel_state(socket, fn state ->
|
||||||
if state.visible do
|
if state.visible do
|
||||||
@@ -55,7 +30,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
reload.(socket, socket.assigns.workbench)
|
reload.(socket, socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:toggle_sidebar_archive, socket, _params, reload) do
|
defp dispatch("toggle_sidebar_archive", socket, _params, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
||||||
%{state | archive_collapsed: not state.archive_collapsed}
|
%{state | archive_collapsed: not state.archive_collapsed}
|
||||||
@@ -63,7 +38,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:toggle_sidebar_tags, socket, _params, reload) do
|
defp dispatch("toggle_sidebar_tags", socket, _params, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
||||||
%{state | tags_collapsed: not state.tags_collapsed}
|
%{state | tags_collapsed: not state.tags_collapsed}
|
||||||
@@ -71,7 +46,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:toggle_sidebar_categories, socket, _params, reload) do
|
defp dispatch("toggle_sidebar_categories", socket, _params, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
||||||
%{state | categories_collapsed: not state.categories_collapsed}
|
%{state | categories_collapsed: not state.categories_collapsed}
|
||||||
@@ -79,7 +54,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:update_sidebar_search, socket, %{"sidebar_filters" => params}, reload) do
|
defp dispatch("update_sidebar_search", socket, %{"sidebar_filters" => params}, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filters(fn filters ->
|
|> ShellSidebarState.put_filters(fn filters ->
|
||||||
Map.put(
|
Map.put(
|
||||||
@@ -91,25 +66,25 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:clear_sidebar_search, socket, _params, reload) do
|
defp dispatch("clear_sidebar_search", socket, _params, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filters(fn filters -> Map.put(filters, :search, nil) end)
|
|> ShellSidebarState.put_filters(fn filters -> Map.put(filters, :search, nil) end)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:clear_sidebar_tags, socket, _params, reload) do
|
defp dispatch("clear_sidebar_tags", socket, _params, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filters(fn filters -> Map.put(filters, :tags, []) end)
|
|> ShellSidebarState.put_filters(fn filters -> Map.put(filters, :tags, []) end)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:clear_sidebar_categories, socket, _params, reload) do
|
defp dispatch("clear_sidebar_categories", socket, _params, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filters(fn filters -> Map.put(filters, :categories, []) end)
|
|> ShellSidebarState.put_filters(fn filters -> Map.put(filters, :categories, []) end)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:toggle_sidebar_tag, socket, %{"tag" => tag}, reload) do
|
defp dispatch("toggle_sidebar_tag", socket, %{"tag" => tag}, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filters(fn filters ->
|
|> ShellSidebarState.put_filters(fn filters ->
|
||||||
ShellSidebarState.toggle_filter_value(filters, :tags, tag)
|
ShellSidebarState.toggle_filter_value(filters, :tags, tag)
|
||||||
@@ -117,7 +92,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:toggle_sidebar_category, socket, %{"category" => category}, reload) do
|
defp dispatch("toggle_sidebar_category", socket, %{"category" => category}, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filters(fn filters ->
|
|> ShellSidebarState.put_filters(fn filters ->
|
||||||
ShellSidebarState.toggle_filter_value(filters, :categories, category)
|
ShellSidebarState.toggle_filter_value(filters, :categories, category)
|
||||||
@@ -125,7 +100,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:select_sidebar_year, socket, %{"year" => year}, reload) do
|
defp dispatch("select_sidebar_year", socket, %{"year" => year}, reload) do
|
||||||
parsed_year = ShellSidebarState.parse_optional_integer(year)
|
parsed_year = ShellSidebarState.parse_optional_integer(year)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
@@ -144,7 +119,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:select_sidebar_month, socket, %{"year" => year, "month" => month}, reload) do
|
defp dispatch("select_sidebar_month", socket, %{"year" => year, "month" => month}, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
||||||
%{
|
%{
|
||||||
@@ -161,7 +136,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:clear_sidebar_month, socket, _params, reload) do
|
defp dispatch("clear_sidebar_month", socket, _params, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
||||||
%{state | archive_collapsed: false}
|
%{state | archive_collapsed: false}
|
||||||
@@ -172,7 +147,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:clear_sidebar_filters, socket, _params, reload) do
|
defp dispatch("clear_sidebar_filters", socket, _params, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filters(fn filters ->
|
|> ShellSidebarState.put_filters(fn filters ->
|
||||||
filters
|
filters
|
||||||
@@ -189,7 +164,7 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:load_more_sidebar, socket, _params, reload) do
|
defp dispatch("load_more_sidebar", socket, _params, reload) do
|
||||||
socket
|
socket
|
||||||
|> ShellSidebarState.put_filters(fn filters ->
|
|> ShellSidebarState.put_filters(fn filters ->
|
||||||
Map.update(
|
Map.update(
|
||||||
|
|||||||
@@ -180,10 +180,13 @@ defmodule BDS.Rendering.Filters do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp render_markdown_html(markdown) do
|
defp render_markdown_html(markdown) do
|
||||||
# Macros above inject raw HTML (gallery, video embeds), so the render path
|
# Match Earmark's defaults: GFM tables, strikethrough and bare-URL autolinks
|
||||||
# must pass HTML through untouched. unsafe: true matches the prior Earmark
|
# were all on. Macros above inject raw HTML (gallery, video embeds), so
|
||||||
# no-escape behavior.
|
# unsafe: true keeps that HTML untouched as Earmark's no-escape path did.
|
||||||
case MDEx.to_html(markdown, render: [unsafe: true]) do
|
case MDEx.to_html(markdown,
|
||||||
|
extension: [table: true, strikethrough: true, autolink: true],
|
||||||
|
render: [unsafe: true]
|
||||||
|
) do
|
||||||
{:ok, html} -> html
|
{:ok, html} -> html
|
||||||
{:error, _reason} -> markdown
|
{:error, _reason} -> markdown
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5465,6 +5465,18 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
refute html =~ ~r/<img\b/
|
refute html =~ ~r/<img\b/
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "chat editor renders GFM markdown and escapes raw HTML" do
|
||||||
|
{:safe, html} =
|
||||||
|
BDS.Desktop.ShellLive.ChatEditor.markdown_html(
|
||||||
|
"~~old~~ value\n\n| A | B |\n|---|---|\n| 1 | 2 |\n\n<script>alert(1)</script>"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert html =~ "<del>old</del>"
|
||||||
|
assert html =~ "<table>"
|
||||||
|
refute html =~ "<script>"
|
||||||
|
assert html =~ "<script>"
|
||||||
|
end
|
||||||
|
|
||||||
test "chat editor renders chart inline surfaces from render_chart tool calls" do
|
test "chat editor renders chart inline surfaces from render_chart tool calls" do
|
||||||
assert {:ok, conversation} = AI.start_chat(%{title: "Chart Chat", model: "gpt-4.1"})
|
assert {:ok, conversation} = AI.start_chat(%{title: "Chart Chat", model: "gpt-4.1"})
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,37 @@ defmodule BDS.GalleryPipelineTest do
|
|||||||
assert result =~ "tag-cloud-empty"
|
assert result =~ "tag-cloud-empty"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "renders GFM tables, strikethrough and autolinks like Earmark did", %{
|
||||||
|
project: project,
|
||||||
|
post: post
|
||||||
|
} do
|
||||||
|
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
language = metadata.main_language || "en"
|
||||||
|
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||||
|
|
||||||
|
markdown = """
|
||||||
|
| A | B |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 2 |
|
||||||
|
|
||||||
|
~~gone~~ and visit https://example.com now
|
||||||
|
"""
|
||||||
|
|
||||||
|
result =
|
||||||
|
BDS.Rendering.Filters.render_markdown(
|
||||||
|
markdown,
|
||||||
|
%{},
|
||||||
|
%{},
|
||||||
|
language,
|
||||||
|
template_context,
|
||||||
|
post.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result =~ "<table>"
|
||||||
|
assert result =~ "<del>gone</del>"
|
||||||
|
assert result =~ ~s(<a href="https://example.com">)
|
||||||
|
end
|
||||||
|
|
||||||
test "[[photo_archive]] renders without media", %{project: project, post: post} do
|
test "[[photo_archive]] renders without media", %{project: project, post: post} do
|
||||||
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
language = metadata.main_language || "en"
|
language = metadata.main_language || "en"
|
||||||
|
|||||||
Reference in New Issue
Block a user