fix: ai preferences crashed and deleted stuff in preferences

This commit is contained in:
2026-07-07 22:43:05 +02:00
parent 4a03fc0ba2
commit dc35518bad
21 changed files with 1883 additions and 261 deletions

View File

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

View File

@@ -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 || ""}})

View File

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

View File

@@ -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")),