fix: ai preferences crashed and deleted stuff in preferences
This commit is contained in:
@@ -54,13 +54,21 @@ defmodule BDS.AI do
|
||||
model = MapUtils.attr(attrs, :model)
|
||||
api_key = MapUtils.attr(attrs, :api_key)
|
||||
|
||||
with :ok <- put_setting("ai.#{kind_key}.url", url),
|
||||
:ok <- put_setting("ai.#{kind_key}.model", model),
|
||||
:ok <- put_secret("ai.#{kind_key}.api_key", api_key, backend) do
|
||||
with :ok <- put_or_delete_setting("ai.#{kind_key}.url", url),
|
||||
:ok <- put_or_delete_setting("ai.#{kind_key}.model", model),
|
||||
:ok <- put_or_delete_secret("ai.#{kind_key}.api_key", api_key, backend) do
|
||||
{:ok, %{kind: kind, url: url, api_key: api_key, model: model}}
|
||||
end
|
||||
end
|
||||
|
||||
# A nil field means "clear it" — writing nil into put_setting/2 would raise a
|
||||
# FunctionClauseError and crash the caller (the preferences panel).
|
||||
defp put_or_delete_setting(key, nil), do: delete_setting(key)
|
||||
defp put_or_delete_setting(key, value) when is_binary(value), do: put_setting(key, value)
|
||||
|
||||
defp put_or_delete_secret(key, nil, _backend), do: delete_setting(encrypted_key(key))
|
||||
defp put_or_delete_secret(key, value, backend), do: put_secret(key, value, backend)
|
||||
|
||||
@spec get_endpoint(endpoint_kind(), keyword()) ::
|
||||
{:ok, endpoint() | nil} | {:error, term()}
|
||||
def get_endpoint(kind, opts \\ []) when is_atom(kind) and is_list(opts) do
|
||||
|
||||
@@ -719,6 +719,11 @@ defmodule BDS.Desktop.ShellLive do
|
||||
{:noreply, refresh_sidebar(socket, socket.assigns.workbench)}
|
||||
end
|
||||
|
||||
def handle_info({:shell_alert, title, message}, socket) do
|
||||
overlay = %{kind: :alert, title: title, message: message, tone: "error"}
|
||||
{:noreply, assign(socket, :shell_overlay, overlay)}
|
||||
end
|
||||
|
||||
def handle_info(message, socket) do
|
||||
Bridges.handle_info(message, socket, bridges_callbacks())
|
||||
end
|
||||
@@ -729,6 +734,20 @@ defmodule BDS.Desktop.ShellLive do
|
||||
index(assigns)
|
||||
end
|
||||
|
||||
# Dark-themed toast classes for LiveToast (its default is bg-white/text-black).
|
||||
# Mirrors the library's structural classes but uses the app's dark palette.
|
||||
@doc false
|
||||
@spec toast_class_fn(map()) :: [String.t() | false]
|
||||
def toast_class_fn(assigns) do
|
||||
[
|
||||
"group/toast z-100 pointer-events-auto relative w-full items-center justify-between origin-center overflow-hidden rounded-lg p-4 shadow-lg border col-start-1 col-end-1 row-start-1 row-end-2",
|
||||
"bg-[#2d2d2d] text-[#f0f0f0] border-[#3c3c3c]",
|
||||
"[@media(scripting:enabled)]:opacity-0 [@media(scripting:enabled){[data-phx-main]_&}]:opacity-100",
|
||||
if(assigns[:rest][:hidden] == true, do: "hidden", else: "flex"),
|
||||
assigns[:kind] == :error && "!bg-[#3a2020] !text-red-300 !border-[#5a2a2a]"
|
||||
]
|
||||
end
|
||||
|
||||
defp refresh_layout(socket, workbench), do: SocketState.refresh_layout(socket, workbench)
|
||||
|
||||
defp refresh_sidebar(socket, workbench), do: SocketState.refresh_sidebar(socket, workbench)
|
||||
|
||||
@@ -678,5 +678,6 @@
|
||||
</footer>
|
||||
|
||||
<ShellOverlayComponents.shell_overlay shell_overlay={@shell_overlay} />
|
||||
<LiveToast.toast_group flash={@flash} connected={assigns[:socket] != nil} toasts_sync={assigns[:toasts_sync]} corner={:bottom_right} toast_class_fn={&toast_class_fn/1} />
|
||||
<GitRun.git_run_modal git_run={@git_run} />
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,12 @@ defmodule BDS.Desktop.ShellLive.Notify do
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec alert(String.t(), String.t()) :: :ok
|
||||
def alert(title, message) do
|
||||
send(self(), {:shell_alert, title, message})
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec tab_meta(atom(), term(), String.t(), String.t()) :: :ok
|
||||
def tab_meta(type, id, title, subtitle) do
|
||||
send(self(), {:editor_tab_meta, type, id, %{title: title, subtitle: subtitle || ""}})
|
||||
|
||||
@@ -186,6 +186,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% :alert -> %>
|
||||
<div class="shell-overlay-backdrop confirm-delete-modal-backdrop" data-testid="shell-overlay-backdrop" phx-window-keydown="overlay_keydown">
|
||||
<button class="shell-overlay-dismiss" type="button" phx-click="close_overlay" aria-label={dgettext("ui", "Close")}></button>
|
||||
<div class={["confirm-delete-modal", "alert-modal", "alert-modal-#{@shell_overlay.tone}"]} role="alertdialog" aria-modal="true">
|
||||
<div class="confirm-delete-modal-header">
|
||||
<h2><%= @shell_overlay.title %></h2>
|
||||
<button class="confirm-delete-modal-close" type="button" phx-click="close_overlay">×</button>
|
||||
</div>
|
||||
<div class="confirm-delete-modal-body">
|
||||
<div class="confirm-delete-message"><%= @shell_overlay.message %></div>
|
||||
</div>
|
||||
<div class="confirm-delete-modal-footer">
|
||||
<button class="button-apply primary ui-button ui-button-primary" type="button" phx-click="close_overlay"><%= dgettext("ui", "OK") %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% :gallery -> %>
|
||||
<div class="shell-overlay-backdrop gallery-overlay-backdrop" data-testid="shell-overlay-backdrop" phx-window-keydown="overlay_keydown">
|
||||
<button class="shell-overlay-dismiss" type="button" phx-click="close_overlay" aria-label={dgettext("ui", "Cancel")}></button>
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
use Phoenix.Component
|
||||
|
||||
alias BDS.AI
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
alias BDS.Desktop.ShellLive.SettingsEditor.EditorSettings
|
||||
require Logger
|
||||
use Gettext, backend: BDS.Gettext
|
||||
@@ -71,6 +72,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
|
||||
with {:ok, endpoint} <- endpoint_refresh_attrs(endpoint_key, attrs),
|
||||
{:ok, models} <- AI.list_endpoint_models(endpoint) do
|
||||
notify_models_loaded(models)
|
||||
|
||||
socket
|
||||
|> assign(
|
||||
:settings_editor_endpoint_models,
|
||||
@@ -83,14 +86,25 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
|> reload.(socket.assigns.workbench)
|
||||
else
|
||||
{:error, reason} ->
|
||||
message = ai_error_message(reason)
|
||||
Notify.alert(dgettext("ui", "Could not load models"), message)
|
||||
|
||||
socket
|
||||
|> append_output.(dgettext("ui", "AI Settings"), inspect(reason), nil, "error")
|
||||
|> append_output.(dgettext("ui", "AI Settings"), message, nil, "error")
|
||||
|> reload.(socket.assigns.workbench)
|
||||
end
|
||||
end
|
||||
|
||||
@spec save_ai(term(), term(), term()) :: term()
|
||||
def save_ai(socket, reload, append_output) do
|
||||
do_save_ai(socket, reload, append_output)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("AI settings save crashed: #{Exception.format(:error, error, __STACKTRACE__)}")
|
||||
surface_ai_error(socket, reload, append_output, error)
|
||||
end
|
||||
|
||||
defp do_save_ai(socket, reload, append_output) do
|
||||
attrs = ai_attrs(socket.assigns)
|
||||
|
||||
with :ok <-
|
||||
@@ -142,18 +156,48 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
attrs.offline_chat_images
|
||||
),
|
||||
:ok <- EditorSettings.put_global_setting("ai.system_prompt", attrs.system_prompt) do
|
||||
LiveToast.send_toast(:info, dgettext("ui", "AI settings saved."))
|
||||
|
||||
socket
|
||||
|> assign(:settings_editor_ai_draft, %{})
|
||||
|> assign(:offline_mode, attrs.offline_mode)
|
||||
|> reload.(socket.assigns.workbench)
|
||||
else
|
||||
{:error, reason} ->
|
||||
socket
|
||||
|> append_output.(dgettext("ui", "AI Settings"), inspect(reason), nil, "error")
|
||||
|> reload.(socket.assigns.workbench)
|
||||
surface_ai_error(socket, reload, append_output, reason)
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_models_loaded([]) do
|
||||
LiveToast.send_toast(
|
||||
:info,
|
||||
dgettext(
|
||||
"ui",
|
||||
"The endpoint returned no models. You can still type a model name manually."
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp notify_models_loaded(models) do
|
||||
LiveToast.send_toast(
|
||||
:info,
|
||||
dgettext("ui", "Loaded %{count} models.", count: length(models))
|
||||
)
|
||||
end
|
||||
|
||||
defp surface_ai_error(socket, reload, append_output, reason) do
|
||||
message = ai_error_message(reason)
|
||||
Notify.alert(dgettext("ui", "Could not save AI settings"), message)
|
||||
|
||||
socket
|
||||
|> append_output.(dgettext("ui", "AI Settings"), message, nil, "error")
|
||||
|> reload.(socket.assigns.workbench)
|
||||
end
|
||||
|
||||
defp ai_error_message(%{__exception__: true} = error), do: Exception.message(error)
|
||||
defp ai_error_message(reason) when is_binary(reason), do: reason
|
||||
defp ai_error_message(reason), do: inspect(reason)
|
||||
|
||||
@spec reset_ai_prompt(term(), term(), term()) :: term()
|
||||
def reset_ai_prompt(socket, reload, append_output) do
|
||||
case EditorSettings.put_global_setting("ai.system_prompt", "") do
|
||||
@@ -179,7 +223,9 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
end
|
||||
|
||||
defp ai_attrs(assigns) do
|
||||
draft = Map.get(assigns, :settings_editor_ai_draft, %{})
|
||||
# Merge the stored form with the edit draft so untouched fields (URLs, API
|
||||
# keys) are preserved on save instead of being read as nil and wiped.
|
||||
draft = Map.merge(ai_form(assigns), Map.get(assigns, :settings_editor_ai_draft, %{}))
|
||||
|
||||
%{
|
||||
online_url: blank_to_nil(Map.get(draft, "online_url")),
|
||||
|
||||
Reference in New Issue
Block a user