241 lines
6.8 KiB
Elixir
241 lines
6.8 KiB
Elixir
defmodule BDS.Desktop.ShellLive.ChatSurface do
|
|
@moduledoc false
|
|
|
|
import Phoenix.Component, only: [assign: 3]
|
|
|
|
alias BDS.BoundedAtoms
|
|
alias BDS.Desktop.ShellData
|
|
alias BDS.Desktop.ShellLive.{ChatEditor, TabHelpers}
|
|
alias BDS.UI.Workbench
|
|
|
|
@doc """
|
|
Handle a chat-surface action from a chat message. Receives callbacks for
|
|
`reload_shell/2` and `open_sidebar_item/3` to remain decoupled from
|
|
`BDS.Desktop.ShellLive` private state.
|
|
"""
|
|
def handle_action(socket, params, callbacks) do
|
|
surface_id = Map.get(params, "surface-id", "")
|
|
|
|
payload =
|
|
params
|
|
|> Map.get("payload")
|
|
|> decode_payload()
|
|
|> maybe_put_form_data(socket, surface_id)
|
|
|
|
case normalize_action(Map.get(params, "action", "")) do
|
|
:open_post ->
|
|
case Map.get(payload, "postId") || Map.get(payload, "post_id") do
|
|
post_id when is_binary(post_id) and post_id != "" ->
|
|
socket
|
|
|> clear_action_error()
|
|
|> callbacks.open_sidebar.(
|
|
%{
|
|
"route" => "post",
|
|
"id" => post_id,
|
|
"title" => TabHelpers.post_title(post_id),
|
|
"subtitle" => TabHelpers.post_subtitle(post_id)
|
|
},
|
|
:pin
|
|
)
|
|
|
|
_other ->
|
|
ChatEditor.set_action_error(
|
|
socket,
|
|
socket.assigns.current_tab.id,
|
|
"Invalid payload for openPost action",
|
|
callbacks.reload
|
|
)
|
|
end
|
|
|
|
:open_media ->
|
|
case Map.get(payload, "mediaId") || Map.get(payload, "media_id") do
|
|
media_id when is_binary(media_id) and media_id != "" ->
|
|
socket
|
|
|> clear_action_error()
|
|
|> callbacks.open_sidebar.(
|
|
%{
|
|
"route" => "media",
|
|
"id" => media_id,
|
|
"title" => TabHelpers.media_title(media_id),
|
|
"subtitle" => TabHelpers.media_subtitle(media_id)
|
|
},
|
|
:pin
|
|
)
|
|
|
|
_other ->
|
|
ChatEditor.set_action_error(
|
|
socket,
|
|
socket.assigns.current_tab.id,
|
|
"Invalid payload for openMedia action",
|
|
callbacks.reload
|
|
)
|
|
end
|
|
|
|
:open_settings ->
|
|
socket
|
|
|> clear_action_error()
|
|
|> callbacks.open_sidebar.(
|
|
%{
|
|
"route" => "settings",
|
|
"id" => "settings-ai",
|
|
"title" => "Settings",
|
|
"subtitle" => "AI"
|
|
},
|
|
:pin
|
|
)
|
|
|
|
:open_chat ->
|
|
chat_id =
|
|
Map.get(payload, "conversationId") || Map.get(payload, "conversation_id") ||
|
|
socket.assigns.current_tab.id
|
|
|
|
socket
|
|
|> clear_action_error()
|
|
|> callbacks.open_sidebar.(
|
|
%{
|
|
"route" => "chat",
|
|
"id" => chat_id,
|
|
"title" => Map.get(payload, "title", "Chat"),
|
|
"subtitle" => Map.get(payload, "subtitle", "")
|
|
},
|
|
:pin
|
|
)
|
|
|
|
:switch_view ->
|
|
case BoundedAtoms.sidebar_view(Map.get(payload, "view")) do
|
|
nil ->
|
|
ChatEditor.set_action_error(
|
|
socket,
|
|
socket.assigns.current_tab.id,
|
|
"Invalid payload for switchView action",
|
|
callbacks.reload
|
|
)
|
|
|
|
view ->
|
|
socket
|
|
|> clear_action_error()
|
|
|> callbacks.reload.(Workbench.click_activity(socket.assigns.workbench, view))
|
|
end
|
|
|
|
:toggle_sidebar ->
|
|
socket
|
|
|> clear_action_error()
|
|
|> callbacks.reload.(Workbench.toggle_sidebar(socket.assigns.workbench))
|
|
|
|
:toggle_panel ->
|
|
socket
|
|
|> clear_action_error()
|
|
|> callbacks.reload.(Workbench.toggle_panel(socket.assigns.workbench))
|
|
|
|
:toggle_assistant_sidebar ->
|
|
socket
|
|
|> clear_action_error()
|
|
|> callbacks.reload.(Workbench.toggle_assistant_sidebar(socket.assigns.workbench))
|
|
|
|
:unknown ->
|
|
ChatEditor.set_action_error(
|
|
socket,
|
|
socket.assigns.current_tab.id,
|
|
"Unsupported assistant action",
|
|
callbacks.reload
|
|
)
|
|
end
|
|
end
|
|
|
|
def assistant_turn(prompt, socket) do
|
|
[
|
|
%{role: "user", content: prompt},
|
|
%{role: "assistant", content: assistant_reply(socket)}
|
|
]
|
|
end
|
|
|
|
def assistant_project_name(nil), do: translated("Projects")
|
|
def assistant_project_name(project), do: project.name
|
|
|
|
def assistant_message_label("assistant"), do: translated("Assistant")
|
|
def assistant_message_label("user"), do: translated("You")
|
|
def assistant_message_label(_role), do: translated("Assistant")
|
|
|
|
def assistant_message_testid(role), do: "assistant-message-#{role}"
|
|
|
|
def update_shell_overlay(socket, updater) do
|
|
case socket.assigns[:shell_overlay] do
|
|
nil -> socket
|
|
overlay -> assign(socket, :shell_overlay, updater.(overlay))
|
|
end
|
|
end
|
|
|
|
def clear_action_error(%{assigns: %{current_tab: %{type: :chat, id: conversation_id}}} = socket) do
|
|
assign(
|
|
socket,
|
|
:chat_editor_action_errors,
|
|
Map.delete(socket.assigns.chat_editor_action_errors, conversation_id)
|
|
)
|
|
end
|
|
|
|
def clear_action_error(socket), do: socket
|
|
|
|
defp decode_payload(nil), do: %{}
|
|
defp decode_payload(""), do: %{}
|
|
|
|
defp decode_payload(payload) when is_binary(payload) do
|
|
case Jason.decode(payload) do
|
|
{:ok, decoded} when is_map(decoded) -> decoded
|
|
_other -> %{}
|
|
end
|
|
end
|
|
|
|
defp decode_payload(_payload), do: %{}
|
|
|
|
defp maybe_put_form_data(payload, socket, surface_id)
|
|
when is_binary(surface_id) and surface_id != "" do
|
|
form_data = ChatEditor.current_surface_data(socket, surface_id)
|
|
|
|
if form_data == %{} do
|
|
payload
|
|
else
|
|
Map.put(payload, "formData", form_data)
|
|
end
|
|
end
|
|
|
|
defp maybe_put_form_data(payload, _socket, _surface_id), do: payload
|
|
|
|
defp normalize_action(action) do
|
|
action
|
|
|> to_string()
|
|
|> String.replace("_", "")
|
|
|> String.downcase()
|
|
|> case do
|
|
"openpost" -> :open_post
|
|
"openmedia" -> :open_media
|
|
"opensettings" -> :open_settings
|
|
"openchat" -> :open_chat
|
|
"switchview" -> :switch_view
|
|
"setactiveview" -> :switch_view
|
|
"togglesidebar" -> :toggle_sidebar
|
|
"togglepanel" -> :toggle_panel
|
|
"openpanel" -> :toggle_panel
|
|
"toggleassistantsidebar" -> :toggle_assistant_sidebar
|
|
_other -> :unknown
|
|
end
|
|
end
|
|
|
|
defp assistant_reply(socket) do
|
|
if socket.assigns.offline_mode do
|
|
ShellData.translate(
|
|
"Automatic AI actions stay gated by airplane mode.",
|
|
%{},
|
|
socket.assigns.page_language
|
|
)
|
|
else
|
|
ShellData.translate(
|
|
"The assistant sidebar chat surface is ready, but model execution is not connected yet.",
|
|
%{},
|
|
socket.assigns.page_language
|
|
)
|
|
end
|
|
end
|
|
|
|
defp translated(text), do: ShellData.translate(text, %{}, BDS.Desktop.UILocale.current())
|
|
end
|