diff --git a/assets/css/app.css b/assets/css/app.css
index cec90d7..478bc0b 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -3,6 +3,7 @@
@source "../css";
@source "../js";
@source "../../lib/bds/desktop";
+@source "../../deps/live_toast/lib";
@import "./tokens.css";
@import "./shell.css";
diff --git a/assets/css/overlays.css b/assets/css/overlays.css
index 5d27594..f70239b 100644
--- a/assets/css/overlays.css
+++ b/assets/css/overlays.css
@@ -377,3 +377,8 @@
font: inherit;
}
+
+/* Alert modal (errors) — reuses .confirm-delete-modal with a tone accent. */
+.alert-modal-error {
+ border-top: 3px solid #ff6b6b;
+}
diff --git a/assets/js/hooks/index.js b/assets/js/hooks/index.js
index 58f52ba..3ebf318 100644
--- a/assets/js/hooks/index.js
+++ b/assets/js/hooks/index.js
@@ -6,8 +6,10 @@ import { ColourPicker } from "./colour_picker.js";
import { MenuEditorTree } from "./menu_editor_tree.js";
import { MonacoEditor } from "./monaco_editor.js";
import { MonacoDiffEditor } from "./monaco_diff_editor.js";
+import { createLiveToastHook } from "live_toast";
export const Hooks = {
+ LiveToast: createLiveToastHook(),
AppShell,
SidebarInteractions,
SettingsSectionScroll,
diff --git a/lib/bds/ai.ex b/lib/bds/ai.ex
index 63a798c..4bf484c 100644
--- a/lib/bds/ai.ex
+++ b/lib/bds/ai.ex
@@ -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
diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex
index 474d551..f5ba436 100644
--- a/lib/bds/desktop/shell_live.ex
+++ b/lib/bds/desktop/shell_live.ex
@@ -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)
diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex
index f40df65..a470c64 100644
--- a/lib/bds/desktop/shell_live/index.html.heex
+++ b/lib/bds/desktop/shell_live/index.html.heex
@@ -678,5 +678,6 @@
+
diff --git a/lib/bds/desktop/shell_live/notify.ex b/lib/bds/desktop/shell_live/notify.ex
index 7fb6797..aef233d 100644
--- a/lib/bds/desktop/shell_live/notify.ex
+++ b/lib/bds/desktop/shell_live/notify.ex
@@ -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 || ""}})
diff --git a/lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex b/lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex
index 83cbc1a..f013e0a 100644
--- a/lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex
+++ b/lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex
@@ -186,6 +186,23 @@
+ <% :alert -> %>
+
diff --git a/lib/bds/desktop/shell_live/settings_editor/ai_settings.ex b/lib/bds/desktop/shell_live/settings_editor/ai_settings.ex
index 9d26bc8..4693a2c 100644
--- a/lib/bds/desktop/shell_live/settings_editor/ai_settings.ex
+++ b/lib/bds/desktop/shell_live/settings_editor/ai_settings.ex
@@ -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")),
diff --git a/mix.exs b/mix.exs
index 6ae8bad..a7c5020 100644
--- a/mix.exs
+++ b/mix.exs
@@ -37,6 +37,7 @@ defmodule BDS.MixProject do
{:plug, "~> 1.18"},
{:bandit, "~> 1.5"},
{:req, "~> 0.5"},
+ {:live_toast, "~> 0.8.0"},
{:saxy, "~> 1.4"},
{:desktop, "~> 1.5"},
{:image, "~> 0.67"},
diff --git a/mix.lock b/mix.lock
index def39d4..5eb3510 100644
--- a/mix.lock
+++ b/mix.lock
@@ -43,6 +43,7 @@
"kday": {:hex, :kday, "1.1.0", "64efac85279a12283eaaf3ad6f13001ca2dff943eda8c53288179775a8c057a0", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "69703055d63b8d5b260479266c78b0b3e66f7aecdd2022906cd9bf09892a266d"},
"lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"},
"liquex": {:hex, :liquex, "0.13.1", "49f90d0b85fb2908f2558f35cd49d78497fe77a895eb55b360889940e1d7afb9", [:mix], [{:date_time_parser, "~> 1.2", [hex: :date_time_parser, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.5.2", [hex: :html_entities, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.4.3", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fbea5b9db264c1758a69bfafdcc8aaebcd56e168365bb9575392cd55d800108f"},
+ "live_toast": {:hex, :live_toast, "0.8.0", "d7e24801a9a966d8e48872767ac33c50fc14640ac5272128becfc534fabd99b7", [:mix], [{:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:gettext, ">= 0.26.2", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.19", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 1.0.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "7e64435131b7731698908d59f7ba9f5092df8a3720814f26edf57e99d97a803c"},
"luerl": {:hex, :luerl, "1.5.1", "f6700420950fc6889137e7a0c11c4a8467dea04a8c23f707a40d83566d14e786", [:rebar3], [], "hexpm", "abf88d849baa0d5dca93b245a8688d4de2ee3d588159bb2faf51e15946509390"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
diff --git a/priv/gettext/de/LC_MESSAGES/ui.po b/priv/gettext/de/LC_MESSAGES/ui.po
index 3062338..f55ca14 100644
--- a/priv/gettext/de/LC_MESSAGES/ui.po
+++ b/priv/gettext/de/LC_MESSAGES/ui.po
@@ -70,9 +70,9 @@ msgstr "KI"
msgid "AI Assistant"
msgstr "KI-Assistent"
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:87
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:152
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:167
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208
#, elixir-autogen, elixir-format
msgid "AI Settings"
msgstr "KI-Einstellungen"
@@ -336,7 +336,7 @@ msgstr "Autor"
msgid "Auto"
msgstr "Automatisch"
-#: lib/bds/desktop/shell_live.ex:442
+#: lib/bds/desktop/shell_live.ex:448
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,7 +418,7 @@ msgstr "Blogmark-Kategorie"
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr "Die Bookmarklet-Kopierfunktion ist über die Desktop-Laufzeit und die öffentliche Projekt-URL verdrahtet."
-#: lib/bds/desktop/shell_live/git_run.ex:126
+#: lib/bds/desktop/shell_live/git_run.ex:128
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -431,8 +431,8 @@ msgstr "Die Bookmarklet-Kopierfunktion ist über die Desktop-Laufzeit und die ö
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:165
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:173
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:183
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:208
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:225
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr "Abbrechen"
@@ -486,7 +486,7 @@ msgstr "Kategorie-Standards, Render-Flags und Template-Zuordnung"
msgid "Category name is required"
msgstr "Kategoriename ist erforderlich"
-#: lib/bds/desktop/shell_live.ex:748
+#: lib/bds/desktop/shell_live.ex:771
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1052,7 +1052,7 @@ msgstr "Duplikate finden"
msgid "Force Reload"
msgstr "Erzwungenes Neuladen"
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:194
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:211
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:432
#, elixir-autogen, elixir-format
msgid "Gallery"
@@ -1063,7 +1063,7 @@ msgstr "Galerie"
msgid "Generate Site"
msgstr "Website generieren"
-#: lib/bds/desktop/shell_live.ex:749
+#: lib/bds/desktop/shell_live.ex:772
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1077,7 +1077,7 @@ msgid "Git Diff"
msgstr "Git-Diff"
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:745
+#: lib/bds/desktop/shell_live.ex:768
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1200,9 +1200,9 @@ msgstr "Importdefinitionen"
msgid "Import failed: %{error}"
msgstr "Import fehlgeschlagen: %{error}"
-#: lib/bds/desktop/shell_live.ex:592
-#: lib/bds/desktop/shell_live.ex:861
-#: lib/bds/desktop/shell_live.ex:867
+#: lib/bds/desktop/shell_live.ex:598
+#: lib/bds/desktop/shell_live.ex:884
+#: lib/bds/desktop/shell_live.ex:890
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1784,7 +1784,7 @@ msgstr "Sonstige"
msgid "Other (%{count})"
msgstr "Andere (%{count})"
-#: lib/bds/desktop/shell_live.ex:744
+#: lib/bds/desktop/shell_live.ex:767
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2538,7 +2538,7 @@ msgstr "Schlagwortname"
msgid "Tags"
msgstr "Tags"
-#: lib/bds/desktop/shell_live.ex:743
+#: lib/bds/desktop/shell_live.ex:766
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3218,12 +3218,12 @@ msgstr "Willkommen beim KI-Assistenten"
msgid "Comparing database and filesystem metadata"
msgstr "Vergleicht Datenbank- und Dateisystem-Metadaten"
-#: lib/bds/desktop/shell_live.ex:668
+#: lib/bds/desktop/shell_live.ex:674
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} Bilder zum Beitrag hinzugefügt"
-#: lib/bds/desktop/shell_live.ex:636
+#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} hinzugefügt"
@@ -3243,18 +3243,18 @@ msgstr "Endbenutzer-Anleitung für redaktionelle Arbeitsabläufe, Medien, Vorlag
msgid "Image Import Concurrency"
msgstr "Gleichzeitige Bildimporte"
-#: lib/bds/desktop/shell_live.ex:441
-#: lib/bds/desktop/shell_live.ex:454
-#: lib/bds/desktop/shell_live.ex:635
-#: lib/bds/desktop/shell_live.ex:667
-#: lib/bds/desktop/shell_live.ex:678
-#: lib/bds/desktop/shell_live.ex:689
+#: lib/bds/desktop/shell_live.ex:447
+#: lib/bds/desktop/shell_live.ex:460
+#: lib/bds/desktop/shell_live.ex:641
+#: lib/bds/desktop/shell_live.ex:673
+#: lib/bds/desktop/shell_live.ex:684
+#: lib/bds/desktop/shell_live.ex:695
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Galerie-Bilder hinzufügen"
-#: lib/bds/desktop/shell_live.ex:690
+#: lib/bds/desktop/shell_live.ex:696
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "%{path} konnte nicht verarbeitet werden: %{reason}"
@@ -3353,7 +3353,7 @@ msgid "Local only"
msgstr "Nur lokal"
#: lib/bds/desktop/shell_live/git_handler.ex:113
-#: lib/bds/desktop/shell_live/git_run.ex:82
+#: lib/bds/desktop/shell_live/git_run.ex:83
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Kein aktives Projekt"
@@ -3445,12 +3445,12 @@ msgstr "umbenannt"
msgid "untracked"
msgstr "nicht verfolgt"
-#: lib/bds/desktop/shell_live.ex:769
+#: lib/bds/desktop/shell_live.ex:792
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:812
+#: lib/bds/desktop/shell_live.ex:835
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr "Öffnen Sie ein Projekt, bevor Sie ein Blogmark importieren."
@@ -3491,7 +3491,7 @@ msgstr "Loeschen"
msgid "Failed to copy bookmarklet to clipboard"
msgstr "Bookmarklet konnte nicht in die Zwischenablage kopiert werden"
-#: lib/bds/desktop/shell_live.ex:781
+#: lib/bds/desktop/shell_live.ex:804
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr "Das Projekt, auf das dieses Blogmark verweist, existiert hier nicht."
@@ -3537,12 +3537,13 @@ msgid "Zoom"
msgstr "Zoomen"
#: lib/bds/desktop/shell_live/git_run.ex:91
-#: lib/bds/desktop/shell_live/git_run.ex:127
+#: lib/bds/desktop/shell_live/git_run.ex:129
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#, elixir-autogen, elixir-format
msgid "Close"
msgstr "Schließen"
-#: lib/bds/desktop/shell_live/git_run.ex:124
+#: lib/bds/desktop/shell_live/git_run.ex:126
#, elixir-autogen, elixir-format
msgid "Failed (exit %{status})"
msgstr "Fehlgeschlagen (Exit %{status})"
@@ -3566,3 +3567,33 @@ msgstr "git pull"
#, elixir-autogen, elixir-format
msgid "git push"
msgstr "git push"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:159
+#, elixir-autogen, elixir-format
+msgid "AI settings saved."
+msgstr "KI-Einstellungen gespeichert."
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:90
+#, elixir-autogen, elixir-format
+msgid "Could not load models"
+msgstr "Modelle konnten nicht geladen werden"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187
+#, elixir-autogen, elixir-format
+msgid "Could not save AI settings"
+msgstr "KI-Einstellungen konnten nicht gespeichert werden"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182
+#, elixir-autogen, elixir-format
+msgid "Loaded %{count} models."
+msgstr "%{count} Modelle geladen."
+
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:201
+#, elixir-autogen, elixir-format
+msgid "OK"
+msgstr "OK"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173
+#, elixir-autogen, elixir-format
+msgid "The endpoint returned no models. You can still type a model name manually."
+msgstr "Der Endpunkt hat keine Modelle zurückgegeben. Sie können einen Modellnamen weiterhin manuell eingeben."
diff --git a/priv/gettext/en/LC_MESSAGES/ui.po b/priv/gettext/en/LC_MESSAGES/ui.po
index 1e04028..dcc57d6 100644
--- a/priv/gettext/en/LC_MESSAGES/ui.po
+++ b/priv/gettext/en/LC_MESSAGES/ui.po
@@ -70,9 +70,9 @@ msgstr ""
msgid "AI Assistant"
msgstr ""
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:87
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:152
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:167
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208
#, elixir-autogen, elixir-format
msgid "AI Settings"
msgstr ""
@@ -336,7 +336,7 @@ msgstr ""
msgid "Auto"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:442
+#: lib/bds/desktop/shell_live.ex:448
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,7 +418,7 @@ msgstr ""
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr ""
-#: lib/bds/desktop/shell_live/git_run.ex:126
+#: lib/bds/desktop/shell_live/git_run.ex:128
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -431,8 +431,8 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:165
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:173
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:183
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:208
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:225
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
@@ -486,7 +486,7 @@ msgstr ""
msgid "Category name is required"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:748
+#: lib/bds/desktop/shell_live.ex:771
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1052,7 +1052,7 @@ msgstr ""
msgid "Force Reload"
msgstr ""
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:194
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:211
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:432
#, elixir-autogen, elixir-format
msgid "Gallery"
@@ -1063,7 +1063,7 @@ msgstr ""
msgid "Generate Site"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:749
+#: lib/bds/desktop/shell_live.ex:772
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1077,7 +1077,7 @@ msgid "Git Diff"
msgstr ""
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:745
+#: lib/bds/desktop/shell_live.ex:768
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1200,9 +1200,9 @@ msgstr ""
msgid "Import failed: %{error}"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:592
-#: lib/bds/desktop/shell_live.ex:861
-#: lib/bds/desktop/shell_live.ex:867
+#: lib/bds/desktop/shell_live.ex:598
+#: lib/bds/desktop/shell_live.ex:884
+#: lib/bds/desktop/shell_live.ex:890
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1784,7 +1784,7 @@ msgstr ""
msgid "Other (%{count})"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:744
+#: lib/bds/desktop/shell_live.ex:767
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2538,7 +2538,7 @@ msgstr ""
msgid "Tags"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:743
+#: lib/bds/desktop/shell_live.ex:766
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3218,12 +3218,12 @@ msgstr ""
msgid "Comparing database and filesystem metadata"
msgstr "Comparing database and filesystem metadata"
-#: lib/bds/desktop/shell_live.ex:668
+#: lib/bds/desktop/shell_live.ex:674
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "Added %{count} images to post"
-#: lib/bds/desktop/shell_live.ex:636
+#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "Added %{title}"
@@ -3243,18 +3243,18 @@ msgstr ""
msgid "Image Import Concurrency"
msgstr "Image Import Concurrency"
-#: lib/bds/desktop/shell_live.ex:441
-#: lib/bds/desktop/shell_live.ex:454
-#: lib/bds/desktop/shell_live.ex:635
-#: lib/bds/desktop/shell_live.ex:667
-#: lib/bds/desktop/shell_live.ex:678
-#: lib/bds/desktop/shell_live.ex:689
+#: lib/bds/desktop/shell_live.ex:447
+#: lib/bds/desktop/shell_live.ex:460
+#: lib/bds/desktop/shell_live.ex:641
+#: lib/bds/desktop/shell_live.ex:673
+#: lib/bds/desktop/shell_live.ex:684
+#: lib/bds/desktop/shell_live.ex:695
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Add Gallery Images"
-#: lib/bds/desktop/shell_live.ex:690
+#: lib/bds/desktop/shell_live.ex:696
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "Failed to process %{path}: %{reason}"
@@ -3353,7 +3353,7 @@ msgid "Local only"
msgstr ""
#: lib/bds/desktop/shell_live/git_handler.ex:113
-#: lib/bds/desktop/shell_live/git_run.ex:82
+#: lib/bds/desktop/shell_live/git_run.ex:83
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr ""
@@ -3445,12 +3445,12 @@ msgstr ""
msgid "untracked"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:769
+#: lib/bds/desktop/shell_live.ex:792
#, elixir-autogen, elixir-format, fuzzy
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:812
+#: lib/bds/desktop/shell_live.ex:835
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr ""
@@ -3491,7 +3491,7 @@ msgstr ""
msgid "Failed to copy bookmarklet to clipboard"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:781
+#: lib/bds/desktop/shell_live.ex:804
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr ""
@@ -3537,12 +3537,13 @@ msgid "Zoom"
msgstr ""
#: lib/bds/desktop/shell_live/git_run.ex:91
-#: lib/bds/desktop/shell_live/git_run.ex:127
+#: lib/bds/desktop/shell_live/git_run.ex:129
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#, elixir-autogen, elixir-format, fuzzy
msgid "Close"
msgstr ""
-#: lib/bds/desktop/shell_live/git_run.ex:124
+#: lib/bds/desktop/shell_live/git_run.ex:126
#, elixir-autogen, elixir-format
msgid "Failed (exit %{status})"
msgstr ""
@@ -3566,3 +3567,33 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "git push"
msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:159
+#, elixir-autogen, elixir-format
+msgid "AI settings saved."
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:90
+#, elixir-autogen, elixir-format
+msgid "Could not load models"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187
+#, elixir-autogen, elixir-format
+msgid "Could not save AI settings"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182
+#, elixir-autogen, elixir-format
+msgid "Loaded %{count} models."
+msgstr ""
+
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:201
+#, elixir-autogen, elixir-format
+msgid "OK"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173
+#, elixir-autogen, elixir-format
+msgid "The endpoint returned no models. You can still type a model name manually."
+msgstr ""
diff --git a/priv/gettext/es/LC_MESSAGES/ui.po b/priv/gettext/es/LC_MESSAGES/ui.po
index 6dc2591..1b86d55 100644
--- a/priv/gettext/es/LC_MESSAGES/ui.po
+++ b/priv/gettext/es/LC_MESSAGES/ui.po
@@ -70,9 +70,9 @@ msgstr "IA"
msgid "AI Assistant"
msgstr "Asistente de IA"
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:87
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:152
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:167
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208
#, elixir-autogen, elixir-format
msgid "AI Settings"
msgstr "Configuración de IA"
@@ -336,7 +336,7 @@ msgstr "Autor"
msgid "Auto"
msgstr "Automático"
-#: lib/bds/desktop/shell_live.ex:442
+#: lib/bds/desktop/shell_live.ex:448
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,7 +418,7 @@ msgstr "Categoría de blogmark"
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr "La copia del bookmarklet está conectada mediante el entorno de escritorio y la URL pública del proyecto."
-#: lib/bds/desktop/shell_live/git_run.ex:126
+#: lib/bds/desktop/shell_live/git_run.ex:128
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -431,8 +431,8 @@ msgstr "La copia del bookmarklet está conectada mediante el entorno de escritor
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:165
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:173
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:183
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:208
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:225
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr "Cancelar"
@@ -486,7 +486,7 @@ msgstr "Valores predeterminados de categoría, opciones de renderizado y conexi
msgid "Category name is required"
msgstr "El nombre de la categoría es obligatorio"
-#: lib/bds/desktop/shell_live.ex:748
+#: lib/bds/desktop/shell_live.ex:771
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1052,7 +1052,7 @@ msgstr "Buscar duplicados"
msgid "Force Reload"
msgstr "Recarga forzada"
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:194
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:211
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:432
#, elixir-autogen, elixir-format
msgid "Gallery"
@@ -1063,7 +1063,7 @@ msgstr "Galeria"
msgid "Generate Site"
msgstr "Generar sitio"
-#: lib/bds/desktop/shell_live.ex:749
+#: lib/bds/desktop/shell_live.ex:772
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1077,7 +1077,7 @@ msgid "Git Diff"
msgstr "Diff de Git"
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:745
+#: lib/bds/desktop/shell_live.ex:768
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1200,9 +1200,9 @@ msgstr "Definiciones de importación"
msgid "Import failed: %{error}"
msgstr "La importación falló: %{error}"
-#: lib/bds/desktop/shell_live.ex:592
-#: lib/bds/desktop/shell_live.ex:861
-#: lib/bds/desktop/shell_live.ex:867
+#: lib/bds/desktop/shell_live.ex:598
+#: lib/bds/desktop/shell_live.ex:884
+#: lib/bds/desktop/shell_live.ex:890
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1784,7 +1784,7 @@ msgstr "Otros"
msgid "Other (%{count})"
msgstr "Otros (%{count})"
-#: lib/bds/desktop/shell_live.ex:744
+#: lib/bds/desktop/shell_live.ex:767
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2538,7 +2538,7 @@ msgstr "Nombre de la etiqueta"
msgid "Tags"
msgstr "Etiquetas"
-#: lib/bds/desktop/shell_live.ex:743
+#: lib/bds/desktop/shell_live.ex:766
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3218,12 +3218,12 @@ msgstr "Bienvenido al asistente de IA"
msgid "Comparing database and filesystem metadata"
msgstr "Comparando metadatos de la base de datos y del sistema de archivos"
-#: lib/bds/desktop/shell_live.ex:668
+#: lib/bds/desktop/shell_live.ex:674
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} imágenes añadidas a la publicación"
-#: lib/bds/desktop/shell_live.ex:636
+#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} añadido"
@@ -3243,18 +3243,18 @@ msgstr "Guía del usuario para flujos editoriales, medios, plantillas, traducci
msgid "Image Import Concurrency"
msgstr "Importación simultánea de imágenes"
-#: lib/bds/desktop/shell_live.ex:441
-#: lib/bds/desktop/shell_live.ex:454
-#: lib/bds/desktop/shell_live.ex:635
-#: lib/bds/desktop/shell_live.ex:667
-#: lib/bds/desktop/shell_live.ex:678
-#: lib/bds/desktop/shell_live.ex:689
+#: lib/bds/desktop/shell_live.ex:447
+#: lib/bds/desktop/shell_live.ex:460
+#: lib/bds/desktop/shell_live.ex:641
+#: lib/bds/desktop/shell_live.ex:673
+#: lib/bds/desktop/shell_live.ex:684
+#: lib/bds/desktop/shell_live.ex:695
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Añadir imágenes a la galería"
-#: lib/bds/desktop/shell_live.ex:690
+#: lib/bds/desktop/shell_live.ex:696
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "No se pudo procesar %{path}: %{reason}"
@@ -3353,7 +3353,7 @@ msgid "Local only"
msgstr "Solo local"
#: lib/bds/desktop/shell_live/git_handler.ex:113
-#: lib/bds/desktop/shell_live/git_run.ex:82
+#: lib/bds/desktop/shell_live/git_run.ex:83
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Sin proyecto activo"
@@ -3445,12 +3445,12 @@ msgstr "renombrado"
msgid "untracked"
msgstr "sin seguimiento"
-#: lib/bds/desktop/shell_live.ex:769
+#: lib/bds/desktop/shell_live.ex:792
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:812
+#: lib/bds/desktop/shell_live.ex:835
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr "Abre un proyecto antes de importar un blogmark."
@@ -3491,7 +3491,7 @@ msgstr "Eliminar"
msgid "Failed to copy bookmarklet to clipboard"
msgstr "Error al copiar el bookmarklet al portapapeles"
-#: lib/bds/desktop/shell_live.ex:781
+#: lib/bds/desktop/shell_live.ex:804
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr "El proyecto al que apunta este blogmark no existe aquí."
@@ -3537,12 +3537,13 @@ msgid "Zoom"
msgstr "Ampliar/reducir"
#: lib/bds/desktop/shell_live/git_run.ex:91
-#: lib/bds/desktop/shell_live/git_run.ex:127
+#: lib/bds/desktop/shell_live/git_run.ex:129
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#, elixir-autogen, elixir-format
msgid "Close"
msgstr "Cerrar"
-#: lib/bds/desktop/shell_live/git_run.ex:124
+#: lib/bds/desktop/shell_live/git_run.ex:126
#, elixir-autogen, elixir-format
msgid "Failed (exit %{status})"
msgstr "Error (salida %{status})"
@@ -3566,3 +3567,33 @@ msgstr "git pull"
#, elixir-autogen, elixir-format
msgid "git push"
msgstr "git push"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:159
+#, elixir-autogen, elixir-format
+msgid "AI settings saved."
+msgstr "Configuración de IA guardada."
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:90
+#, elixir-autogen, elixir-format
+msgid "Could not load models"
+msgstr "No se pudieron cargar los modelos"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187
+#, elixir-autogen, elixir-format
+msgid "Could not save AI settings"
+msgstr "No se pudo guardar la configuración de IA"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182
+#, elixir-autogen, elixir-format
+msgid "Loaded %{count} models."
+msgstr "%{count} modelos cargados."
+
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:201
+#, elixir-autogen, elixir-format
+msgid "OK"
+msgstr "OK"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173
+#, elixir-autogen, elixir-format
+msgid "The endpoint returned no models. You can still type a model name manually."
+msgstr "El endpoint no devolvió ningún modelo. Aún puedes escribir el nombre de un modelo manualmente."
diff --git a/priv/gettext/fr/LC_MESSAGES/ui.po b/priv/gettext/fr/LC_MESSAGES/ui.po
index 91a2f7e..9be6c82 100644
--- a/priv/gettext/fr/LC_MESSAGES/ui.po
+++ b/priv/gettext/fr/LC_MESSAGES/ui.po
@@ -70,9 +70,9 @@ msgstr "IA"
msgid "AI Assistant"
msgstr "Assistant IA"
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:87
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:152
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:167
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208
#, elixir-autogen, elixir-format
msgid "AI Settings"
msgstr "Paramètres IA"
@@ -336,7 +336,7 @@ msgstr "Auteur"
msgid "Auto"
msgstr "Automatique"
-#: lib/bds/desktop/shell_live.ex:442
+#: lib/bds/desktop/shell_live.ex:448
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,7 +418,7 @@ msgstr "Catégorie de blogmark"
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr "La copie du bookmarklet est reliée via l’environnement desktop et l’URL publique du projet."
-#: lib/bds/desktop/shell_live/git_run.ex:126
+#: lib/bds/desktop/shell_live/git_run.ex:128
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -431,8 +431,8 @@ msgstr "La copie du bookmarklet est reliée via l’environnement desktop et l
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:165
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:173
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:183
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:208
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:225
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr "Annuler"
@@ -486,7 +486,7 @@ msgstr "Valeurs par défaut des catégories, options de rendu et liaison des mod
msgid "Category name is required"
msgstr "Le nom de la catégorie est requis"
-#: lib/bds/desktop/shell_live.ex:748
+#: lib/bds/desktop/shell_live.ex:771
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1052,7 +1052,7 @@ msgstr "Trouver les doublons"
msgid "Force Reload"
msgstr "Recharger de force"
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:194
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:211
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:432
#, elixir-autogen, elixir-format
msgid "Gallery"
@@ -1063,7 +1063,7 @@ msgstr "Galerie"
msgid "Generate Site"
msgstr "Générer le site"
-#: lib/bds/desktop/shell_live.ex:749
+#: lib/bds/desktop/shell_live.ex:772
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1077,7 +1077,7 @@ msgid "Git Diff"
msgstr "Diff Git"
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:745
+#: lib/bds/desktop/shell_live.ex:768
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1200,9 +1200,9 @@ msgstr "Définitions d’import"
msgid "Import failed: %{error}"
msgstr "Échec de l’import : %{error}"
-#: lib/bds/desktop/shell_live.ex:592
-#: lib/bds/desktop/shell_live.ex:861
-#: lib/bds/desktop/shell_live.ex:867
+#: lib/bds/desktop/shell_live.ex:598
+#: lib/bds/desktop/shell_live.ex:884
+#: lib/bds/desktop/shell_live.ex:890
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1784,7 +1784,7 @@ msgstr "Autre"
msgid "Other (%{count})"
msgstr "Autres (%{count})"
-#: lib/bds/desktop/shell_live.ex:744
+#: lib/bds/desktop/shell_live.ex:767
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2538,7 +2538,7 @@ msgstr "Nom du mot-clé"
msgid "Tags"
msgstr "Tags"
-#: lib/bds/desktop/shell_live.ex:743
+#: lib/bds/desktop/shell_live.ex:766
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3218,12 +3218,12 @@ msgstr "Bienvenue dans l’assistant IA"
msgid "Comparing database and filesystem metadata"
msgstr "Comparaison des métadonnées entre la base et le système de fichiers"
-#: lib/bds/desktop/shell_live.ex:668
+#: lib/bds/desktop/shell_live.ex:674
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} images ajoutées à l'article"
-#: lib/bds/desktop/shell_live.ex:636
+#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} ajouté"
@@ -3243,18 +3243,18 @@ msgstr "Guide utilisateur pour les flux éditoriaux, médias, modèles, traducti
msgid "Image Import Concurrency"
msgstr "Importation simultanée d'images"
-#: lib/bds/desktop/shell_live.ex:441
-#: lib/bds/desktop/shell_live.ex:454
-#: lib/bds/desktop/shell_live.ex:635
-#: lib/bds/desktop/shell_live.ex:667
-#: lib/bds/desktop/shell_live.ex:678
-#: lib/bds/desktop/shell_live.ex:689
+#: lib/bds/desktop/shell_live.ex:447
+#: lib/bds/desktop/shell_live.ex:460
+#: lib/bds/desktop/shell_live.ex:641
+#: lib/bds/desktop/shell_live.ex:673
+#: lib/bds/desktop/shell_live.ex:684
+#: lib/bds/desktop/shell_live.ex:695
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Ajouter des images à la galerie"
-#: lib/bds/desktop/shell_live.ex:690
+#: lib/bds/desktop/shell_live.ex:696
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "Impossible de traiter %{path} : %{reason}"
@@ -3353,7 +3353,7 @@ msgid "Local only"
msgstr "Local uniquement"
#: lib/bds/desktop/shell_live/git_handler.ex:113
-#: lib/bds/desktop/shell_live/git_run.ex:82
+#: lib/bds/desktop/shell_live/git_run.ex:83
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Aucun projet actif"
@@ -3445,12 +3445,12 @@ msgstr "renommé"
msgid "untracked"
msgstr "non suivi"
-#: lib/bds/desktop/shell_live.ex:769
+#: lib/bds/desktop/shell_live.ex:792
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:812
+#: lib/bds/desktop/shell_live.ex:835
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr "Ouvrez un projet avant d’importer un blogmark."
@@ -3491,7 +3491,7 @@ msgstr "Supprimer"
msgid "Failed to copy bookmarklet to clipboard"
msgstr "Échec de la copie du bookmarklet dans le presse-papiers"
-#: lib/bds/desktop/shell_live.ex:781
+#: lib/bds/desktop/shell_live.ex:804
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr "Le projet ciblé par ce blogmark n'existe pas ici."
@@ -3537,12 +3537,13 @@ msgid "Zoom"
msgstr "Réduire/agrandir"
#: lib/bds/desktop/shell_live/git_run.ex:91
-#: lib/bds/desktop/shell_live/git_run.ex:127
+#: lib/bds/desktop/shell_live/git_run.ex:129
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#, elixir-autogen, elixir-format
msgid "Close"
msgstr "Fermer"
-#: lib/bds/desktop/shell_live/git_run.ex:124
+#: lib/bds/desktop/shell_live/git_run.ex:126
#, elixir-autogen, elixir-format
msgid "Failed (exit %{status})"
msgstr "Échec (code %{status})"
@@ -3566,3 +3567,33 @@ msgstr "git pull"
#, elixir-autogen, elixir-format
msgid "git push"
msgstr "git push"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:159
+#, elixir-autogen, elixir-format
+msgid "AI settings saved."
+msgstr "Paramètres IA enregistrés."
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:90
+#, elixir-autogen, elixir-format
+msgid "Could not load models"
+msgstr "Impossible de charger les modèles"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187
+#, elixir-autogen, elixir-format
+msgid "Could not save AI settings"
+msgstr "Impossible d'enregistrer les paramètres IA"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182
+#, elixir-autogen, elixir-format
+msgid "Loaded %{count} models."
+msgstr "%{count} modèles chargés."
+
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:201
+#, elixir-autogen, elixir-format
+msgid "OK"
+msgstr "OK"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173
+#, elixir-autogen, elixir-format
+msgid "The endpoint returned no models. You can still type a model name manually."
+msgstr "Le point de terminaison n'a renvoyé aucun modèle. Vous pouvez toujours saisir un nom de modèle manuellement."
diff --git a/priv/gettext/it/LC_MESSAGES/ui.po b/priv/gettext/it/LC_MESSAGES/ui.po
index 908bcbb..5c25575 100644
--- a/priv/gettext/it/LC_MESSAGES/ui.po
+++ b/priv/gettext/it/LC_MESSAGES/ui.po
@@ -70,9 +70,9 @@ msgstr "IA"
msgid "AI Assistant"
msgstr "Assistente IA"
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:87
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:152
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:167
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208
#, elixir-autogen, elixir-format
msgid "AI Settings"
msgstr "Impostazioni IA"
@@ -336,7 +336,7 @@ msgstr "Autore"
msgid "Auto"
msgstr "Automatico"
-#: lib/bds/desktop/shell_live.ex:442
+#: lib/bds/desktop/shell_live.ex:448
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,7 +418,7 @@ msgstr "Categoria blogmark"
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr "La copia del bookmarklet è collegata tramite il runtime desktop e l’URL pubblica del progetto."
-#: lib/bds/desktop/shell_live/git_run.ex:126
+#: lib/bds/desktop/shell_live/git_run.ex:128
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -431,8 +431,8 @@ msgstr "La copia del bookmarklet è collegata tramite il runtime desktop e l’U
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:165
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:173
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:183
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:208
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:225
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr "Annulla"
@@ -486,7 +486,7 @@ msgstr "Valori predefiniti delle categorie, opzioni di rendering e collegamento
msgid "Category name is required"
msgstr "Il nome della categoria è obbligatorio"
-#: lib/bds/desktop/shell_live.ex:748
+#: lib/bds/desktop/shell_live.ex:771
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1052,7 +1052,7 @@ msgstr "Trova duplicati"
msgid "Force Reload"
msgstr "Ricarica forzata"
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:194
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:211
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:432
#, elixir-autogen, elixir-format
msgid "Gallery"
@@ -1063,7 +1063,7 @@ msgstr "Galleria"
msgid "Generate Site"
msgstr "Genera sito"
-#: lib/bds/desktop/shell_live.ex:749
+#: lib/bds/desktop/shell_live.ex:772
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1077,7 +1077,7 @@ msgid "Git Diff"
msgstr "Diff Git"
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:745
+#: lib/bds/desktop/shell_live.ex:768
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1200,9 +1200,9 @@ msgstr "Definizioni di importazione"
msgid "Import failed: %{error}"
msgstr "Importazione non riuscita: %{error}"
-#: lib/bds/desktop/shell_live.ex:592
-#: lib/bds/desktop/shell_live.ex:861
-#: lib/bds/desktop/shell_live.ex:867
+#: lib/bds/desktop/shell_live.ex:598
+#: lib/bds/desktop/shell_live.ex:884
+#: lib/bds/desktop/shell_live.ex:890
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1784,7 +1784,7 @@ msgstr "Altro"
msgid "Other (%{count})"
msgstr "Altro (%{count})"
-#: lib/bds/desktop/shell_live.ex:744
+#: lib/bds/desktop/shell_live.ex:767
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2538,7 +2538,7 @@ msgstr "Nome del tag"
msgid "Tags"
msgstr "Tag"
-#: lib/bds/desktop/shell_live.ex:743
+#: lib/bds/desktop/shell_live.ex:766
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3218,12 +3218,12 @@ msgstr "Benvenuto nell’assistente IA"
msgid "Comparing database and filesystem metadata"
msgstr "Confronto tra i metadati del database e del filesystem"
-#: lib/bds/desktop/shell_live.ex:668
+#: lib/bds/desktop/shell_live.ex:674
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} immagini aggiunte al post"
-#: lib/bds/desktop/shell_live.ex:636
+#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} aggiunto"
@@ -3243,18 +3243,18 @@ msgstr "Guida per l'utente finale per flussi editoriali, media, modelli, traduzi
msgid "Image Import Concurrency"
msgstr "Importazione simultanea immagini"
-#: lib/bds/desktop/shell_live.ex:441
-#: lib/bds/desktop/shell_live.ex:454
-#: lib/bds/desktop/shell_live.ex:635
-#: lib/bds/desktop/shell_live.ex:667
-#: lib/bds/desktop/shell_live.ex:678
-#: lib/bds/desktop/shell_live.ex:689
+#: lib/bds/desktop/shell_live.ex:447
+#: lib/bds/desktop/shell_live.ex:460
+#: lib/bds/desktop/shell_live.ex:641
+#: lib/bds/desktop/shell_live.ex:673
+#: lib/bds/desktop/shell_live.ex:684
+#: lib/bds/desktop/shell_live.ex:695
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Aggiungi immagini alla galleria"
-#: lib/bds/desktop/shell_live.ex:690
+#: lib/bds/desktop/shell_live.ex:696
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "Impossibile elaborare %{path}: %{reason}"
@@ -3353,7 +3353,7 @@ msgid "Local only"
msgstr "Solo locale"
#: lib/bds/desktop/shell_live/git_handler.ex:113
-#: lib/bds/desktop/shell_live/git_run.ex:82
+#: lib/bds/desktop/shell_live/git_run.ex:83
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Nessun progetto attivo"
@@ -3445,12 +3445,12 @@ msgstr "rinominato"
msgid "untracked"
msgstr "non tracciato"
-#: lib/bds/desktop/shell_live.ex:769
+#: lib/bds/desktop/shell_live.ex:792
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:812
+#: lib/bds/desktop/shell_live.ex:835
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr "Apri un progetto prima di importare un blogmark."
@@ -3491,7 +3491,7 @@ msgstr "Elimina"
msgid "Failed to copy bookmarklet to clipboard"
msgstr "Impossibile copiare il bookmarklet negli appunti"
-#: lib/bds/desktop/shell_live.ex:781
+#: lib/bds/desktop/shell_live.ex:804
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr "Il progetto a cui punta questo blogmark non esiste qui."
@@ -3537,12 +3537,13 @@ msgid "Zoom"
msgstr "Riduci/ingrandisci"
#: lib/bds/desktop/shell_live/git_run.ex:91
-#: lib/bds/desktop/shell_live/git_run.ex:127
+#: lib/bds/desktop/shell_live/git_run.ex:129
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#, elixir-autogen, elixir-format
msgid "Close"
msgstr "Chiudi"
-#: lib/bds/desktop/shell_live/git_run.ex:124
+#: lib/bds/desktop/shell_live/git_run.ex:126
#, elixir-autogen, elixir-format
msgid "Failed (exit %{status})"
msgstr "Non riuscito (uscita %{status})"
@@ -3566,3 +3567,33 @@ msgstr "git pull"
#, elixir-autogen, elixir-format
msgid "git push"
msgstr "git push"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:159
+#, elixir-autogen, elixir-format
+msgid "AI settings saved."
+msgstr "Impostazioni IA salvate."
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:90
+#, elixir-autogen, elixir-format
+msgid "Could not load models"
+msgstr "Impossibile caricare i modelli"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187
+#, elixir-autogen, elixir-format
+msgid "Could not save AI settings"
+msgstr "Impossibile salvare le impostazioni IA"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182
+#, elixir-autogen, elixir-format
+msgid "Loaded %{count} models."
+msgstr "Caricati %{count} modelli."
+
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:201
+#, elixir-autogen, elixir-format
+msgid "OK"
+msgstr "OK"
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173
+#, elixir-autogen, elixir-format
+msgid "The endpoint returned no models. You can still type a model name manually."
+msgstr "L'endpoint non ha restituito alcun modello. Puoi comunque digitare manualmente il nome di un modello."
diff --git a/priv/gettext/ui.pot b/priv/gettext/ui.pot
index 32a63c2..694053f 100644
--- a/priv/gettext/ui.pot
+++ b/priv/gettext/ui.pot
@@ -83,9 +83,9 @@ msgstr ""
msgid "AI Assistant"
msgstr ""
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:87
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:152
-#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:167
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208
#, elixir-autogen, elixir-format
msgid "AI Settings"
msgstr ""
@@ -349,7 +349,7 @@ msgstr ""
msgid "Auto"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:442
+#: lib/bds/desktop/shell_live.ex:448
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -431,7 +431,7 @@ msgstr ""
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr ""
-#: lib/bds/desktop/shell_live/git_run.ex:126
+#: lib/bds/desktop/shell_live/git_run.ex:128
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -444,8 +444,8 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:165
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:173
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:183
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:208
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:225
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
@@ -499,7 +499,7 @@ msgstr ""
msgid "Category name is required"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:748
+#: lib/bds/desktop/shell_live.ex:771
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1065,7 +1065,7 @@ msgstr ""
msgid "Force Reload"
msgstr ""
-#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:194
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:211
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:432
#, elixir-autogen, elixir-format
msgid "Gallery"
@@ -1076,7 +1076,7 @@ msgstr ""
msgid "Generate Site"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:749
+#: lib/bds/desktop/shell_live.ex:772
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1090,7 +1090,7 @@ msgid "Git Diff"
msgstr ""
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:745
+#: lib/bds/desktop/shell_live.ex:768
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1213,9 +1213,9 @@ msgstr ""
msgid "Import failed: %{error}"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:592
-#: lib/bds/desktop/shell_live.ex:861
-#: lib/bds/desktop/shell_live.ex:867
+#: lib/bds/desktop/shell_live.ex:598
+#: lib/bds/desktop/shell_live.ex:884
+#: lib/bds/desktop/shell_live.ex:890
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1797,7 +1797,7 @@ msgstr ""
msgid "Other (%{count})"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:744
+#: lib/bds/desktop/shell_live.ex:767
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2551,7 +2551,7 @@ msgstr ""
msgid "Tags"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:743
+#: lib/bds/desktop/shell_live.ex:766
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3231,12 +3231,12 @@ msgstr ""
msgid "Comparing database and filesystem metadata"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:668
+#: lib/bds/desktop/shell_live.ex:674
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:636
+#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr ""
@@ -3256,18 +3256,18 @@ msgstr ""
msgid "Image Import Concurrency"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:441
-#: lib/bds/desktop/shell_live.ex:454
-#: lib/bds/desktop/shell_live.ex:635
-#: lib/bds/desktop/shell_live.ex:667
-#: lib/bds/desktop/shell_live.ex:678
-#: lib/bds/desktop/shell_live.ex:689
+#: lib/bds/desktop/shell_live.ex:447
+#: lib/bds/desktop/shell_live.ex:460
+#: lib/bds/desktop/shell_live.ex:641
+#: lib/bds/desktop/shell_live.ex:673
+#: lib/bds/desktop/shell_live.ex:684
+#: lib/bds/desktop/shell_live.ex:695
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:690
+#: lib/bds/desktop/shell_live.ex:696
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr ""
@@ -3366,7 +3366,7 @@ msgid "Local only"
msgstr ""
#: lib/bds/desktop/shell_live/git_handler.ex:113
-#: lib/bds/desktop/shell_live/git_run.ex:82
+#: lib/bds/desktop/shell_live/git_run.ex:83
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr ""
@@ -3458,12 +3458,12 @@ msgstr ""
msgid "untracked"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:769
+#: lib/bds/desktop/shell_live.ex:792
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:812
+#: lib/bds/desktop/shell_live.ex:835
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr ""
@@ -3504,7 +3504,7 @@ msgstr ""
msgid "Failed to copy bookmarklet to clipboard"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:781
+#: lib/bds/desktop/shell_live.ex:804
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr ""
@@ -3550,12 +3550,13 @@ msgid "Zoom"
msgstr ""
#: lib/bds/desktop/shell_live/git_run.ex:91
-#: lib/bds/desktop/shell_live/git_run.ex:127
+#: lib/bds/desktop/shell_live/git_run.ex:129
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:191
#, elixir-autogen, elixir-format
msgid "Close"
msgstr ""
-#: lib/bds/desktop/shell_live/git_run.ex:124
+#: lib/bds/desktop/shell_live/git_run.ex:126
#, elixir-autogen, elixir-format
msgid "Failed (exit %{status})"
msgstr ""
@@ -3579,3 +3580,33 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "git push"
msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:159
+#, elixir-autogen, elixir-format
+msgid "AI settings saved."
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:90
+#, elixir-autogen, elixir-format
+msgid "Could not load models"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187
+#, elixir-autogen, elixir-format
+msgid "Could not save AI settings"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182
+#, elixir-autogen, elixir-format
+msgid "Loaded %{count} models."
+msgstr ""
+
+#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:201
+#, elixir-autogen, elixir-format
+msgid "OK"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173
+#, elixir-autogen, elixir-format
+msgid "The endpoint returned no models. You can still type a model name manually."
+msgstr ""
diff --git a/priv/static/assets/app.css b/priv/static/assets/app.css
index 9356357..3642365 100644
--- a/priv/static/assets/app.css
+++ b/priv/static/assets/app.css
@@ -6,6 +6,12 @@
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
+ --color-red-100: oklch(93.6% 0.032 17.717);
+ --color-red-200: oklch(88.5% 0.062 18.334);
+ --color-red-300: oklch(80.8% 0.114 19.571);
+ --color-red-700: oklch(50.5% 0.213 27.518);
+ --color-black: #000;
+ --color-white: #fff;
--spacing: 0.25rem;
--container-xs: 20rem;
--container-2xl: 42rem;
@@ -13,7 +19,15 @@
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
+ --font-weight-semibold: 600;
--tracking-wide: 0.025em;
+ --radius-md: 0.375rem;
+ --radius-lg: 0.5rem;
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
+ --animate-spin: spin 1s linear infinite;
+ --default-transition-duration: 150ms;
+ --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono);
}
@@ -167,33 +181,105 @@
}
}
@layer utilities {
+ .pointer-events-auto {
+ pointer-events: auto;
+ }
+ .pointer-events-none {
+ pointer-events: none;
+ }
.visible {
visibility: visible;
}
.absolute {
position: absolute;
}
+ .fixed {
+ position: fixed;
+ }
.relative {
position: relative;
}
.static {
position: static;
}
+ .top-0 {
+ top: calc(var(--spacing) * 0);
+ }
+ .top-\[5px\] {
+ top: 5px;
+ }
.top-full {
top: 100%;
}
.right-0 {
right: calc(var(--spacing) * 0);
}
+ .right-\[5px\] {
+ right: 5px;
+ }
+ .bottom-0 {
+ bottom: calc(var(--spacing) * 0);
+ }
+ .left-0 {
+ left: calc(var(--spacing) * 0);
+ }
+ .left-1\/2 {
+ left: calc(1/2 * 100%);
+ }
.z-10 {
z-index: 10;
}
+ .z-50 {
+ z-index: 50;
+ }
+ .z-100 {
+ z-index: 100;
+ }
+ .col-start-1 {
+ grid-column-start: 1;
+ }
+ .col-end-1 {
+ grid-column-end: 1;
+ }
+ .row-start-1 {
+ grid-row-start: 1;
+ }
+ .row-end-2 {
+ grid-row-end: 2;
+ }
+ .container {
+ width: 100%;
+ @media (width >= 40rem) {
+ max-width: 40rem;
+ }
+ @media (width >= 48rem) {
+ max-width: 48rem;
+ }
+ @media (width >= 64rem) {
+ max-width: 64rem;
+ }
+ @media (width >= 80rem) {
+ max-width: 80rem;
+ }
+ @media (width >= 96rem) {
+ max-width: 96rem;
+ }
+ }
.mt-2 {
margin-top: calc(var(--spacing) * 2);
}
+ .mb-2 {
+ margin-bottom: calc(var(--spacing) * 2);
+ }
+ .ml-1 {
+ margin-left: calc(var(--spacing) * 1);
+ }
.block {
display: block;
}
+ .contents {
+ display: contents;
+ }
.flex {
display: flex;
}
@@ -206,12 +292,18 @@
.inline {
display: inline;
}
+ .inline-block {
+ display: inline-block;
+ }
.inline-flex {
display: inline-flex;
}
.table {
display: table;
}
+ .h-3 {
+ height: calc(var(--spacing) * 3);
+ }
.h-6 {
height: calc(var(--spacing) * 6);
}
@@ -224,6 +316,9 @@
.h-12 {
height: calc(var(--spacing) * 12);
}
+ .h-\[14px\] {
+ height: 14px;
+ }
.h-\[22px\] {
height: 22px;
}
@@ -236,6 +331,9 @@
.max-h-\[80vh\] {
max-height: 80vh;
}
+ .max-h-screen {
+ max-height: 100vh;
+ }
.min-h-0 {
min-height: calc(var(--spacing) * 0);
}
@@ -245,6 +343,9 @@
.min-h-\[16rem\] {
min-height: 16rem;
}
+ .w-3 {
+ width: calc(var(--spacing) * 3);
+ }
.w-6 {
width: calc(var(--spacing) * 6);
}
@@ -254,6 +355,9 @@
.w-12 {
width: calc(var(--spacing) * 12);
}
+ .w-\[14px\] {
+ width: 14px;
+ }
.w-full {
width: 100%;
}
@@ -287,9 +391,30 @@
.shrink-0 {
flex-shrink: 0;
}
+ .grow {
+ flex-grow: 1;
+ }
+ .origin-center {
+ transform-origin: center;
+ }
+ .-translate-x-1\/2 {
+ --tw-translate-x: calc(calc(1/2 * 100%) * -1);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+ .translate-y-0 {
+ --tw-translate-y: calc(var(--spacing) * 0);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+ .translate-y-4 {
+ --tw-translate-y: calc(var(--spacing) * 4);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
+ .animate-spin {
+ animation: var(--animate-spin);
+ }
.resize {
resize: both;
}
@@ -299,6 +424,9 @@
.flex-col {
flex-direction: column;
}
+ .flex-col-reverse {
+ flex-direction: column-reverse;
+ }
.flex-wrap {
flex-wrap: wrap;
}
@@ -358,13 +486,43 @@
.rounded {
border-radius: 0.25rem;
}
+ .rounded-lg {
+ border-radius: var(--radius-lg);
+ }
+ .rounded-md {
+ border-radius: var(--radius-md);
+ }
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
+ .\!border-\[\#5a2a2a\] {
+ border-color: #5a2a2a !important;
+ }
+ .border-\[\#3c3c3c\] {
+ border-color: #3c3c3c;
+ }
+ .border-red-200 {
+ border-color: var(--color-red-200);
+ }
+ .\!bg-\[\#3a2020\] {
+ background-color: #3a2020 !important;
+ }
+ .\!bg-red-100 {
+ background-color: var(--color-red-100) !important;
+ }
+ .bg-\[\#2d2d2d\] {
+ background-color: #2d2d2d;
+ }
+ .bg-white {
+ background-color: var(--color-white);
+ }
.p-4 {
padding: calc(var(--spacing) * 4);
}
+ .p-\[5px\] {
+ padding: 5px;
+ }
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
@@ -397,6 +555,18 @@
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
+ .leading-5 {
+ --tw-leading: calc(var(--spacing) * 5);
+ line-height: calc(var(--spacing) * 5);
+ }
+ .leading-6 {
+ --tw-leading: calc(var(--spacing) * 6);
+ line-height: calc(var(--spacing) * 6);
+ }
+ .font-semibold {
+ --tw-font-weight: var(--font-weight-semibold);
+ font-weight: var(--font-weight-semibold);
+ }
.tracking-wide {
--tw-tracking: var(--tracking-wide);
letter-spacing: var(--tracking-wide);
@@ -404,9 +574,40 @@
.whitespace-nowrap {
white-space: nowrap;
}
+ .\!text-red-300 {
+ color: var(--color-red-300) !important;
+ }
+ .\!text-red-700 {
+ color: var(--color-red-700) !important;
+ }
+ .text-\[\#f0f0f0\] {
+ color: #f0f0f0;
+ }
+ .text-black {
+ color: var(--color-black);
+ }
+ .text-black\/50 {
+ color: color-mix(in srgb, #000 50%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ color: color-mix(in oklab, var(--color-black) 50%, transparent);
+ }
+ }
.uppercase {
text-transform: uppercase;
}
+ .opacity-0 {
+ opacity: 0%;
+ }
+ .opacity-40 {
+ opacity: 40%;
+ }
+ .opacity-100 {
+ opacity: 100%;
+ }
+ .shadow-lg {
+ --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
@@ -419,6 +620,117 @@
--tw-invert: invert(100%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
+ .transition {
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .transition-all {
+ transition-property: all;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .transition-opacity {
+ transition-property: opacity;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .duration-200 {
+ --tw-duration: 200ms;
+ transition-duration: 200ms;
+ }
+ .duration-300 {
+ --tw-duration: 300ms;
+ transition-duration: 300ms;
+ }
+ .ease-in {
+ --tw-ease: var(--ease-in);
+ transition-timing-function: var(--ease-in);
+ }
+ .ease-out {
+ --tw-ease: var(--ease-out);
+ transition-timing-function: var(--ease-out);
+ }
+ .group-hover\:opacity-70 {
+ &:is(:where(.group):hover *) {
+ @media (hover: hover) {
+ opacity: 70%;
+ }
+ }
+ }
+ .group-hover\:opacity-100 {
+ &:is(:where(.group):hover *) {
+ @media (hover: hover) {
+ opacity: 100%;
+ }
+ }
+ }
+ .group-has-\[\[data-part\=\'title\'\]\]\/toast\:absolute {
+ &:is(:where(.group\/toast):has(*:is([data-part='title'])) *) {
+ position: absolute;
+ }
+ }
+ .hover\:text-black {
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-black);
+ }
+ }
+ }
+ .focus\:opacity-100 {
+ &:focus {
+ opacity: 100%;
+ }
+ }
+ .focus\:ring-1 {
+ &:focus {
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+ }
+ .focus\:outline-none {
+ &:focus {
+ --tw-outline-style: none;
+ outline-style: none;
+ }
+ }
+ .sm\:top-auto {
+ @media (width >= 40rem) {
+ top: auto;
+ }
+ }
+ .sm\:bottom-auto {
+ @media (width >= 40rem) {
+ bottom: auto;
+ }
+ }
+ .sm\:translate-y-0 {
+ @media (width >= 40rem) {
+ --tw-translate-y: calc(var(--spacing) * 0);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+ }
+ .sm\:scale-95 {
+ @media (width >= 40rem) {
+ --tw-scale-x: 95%;
+ --tw-scale-y: 95%;
+ --tw-scale-z: 95%;
+ scale: var(--tw-scale-x) var(--tw-scale-y);
+ }
+ }
+ .sm\:scale-100 {
+ @media (width >= 40rem) {
+ --tw-scale-x: 100%;
+ --tw-scale-y: 100%;
+ --tw-scale-z: 100%;
+ scale: var(--tw-scale-x) var(--tw-scale-y);
+ }
+ }
+ .md\:max-w-\[420px\] {
+ @media (width >= 48rem) {
+ max-width: 420px;
+ }
+ }
.md\:grid-cols-2 {
@media (width >= 48rem) {
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -444,6 +756,11 @@
grid-template-columns: minmax(320px,1fr) minmax(0,1.2fr);
}
}
+ .\[\@media\(scripting\:enabled\)\]\:opacity-0 {
+ @media (scripting:enabled) {
+ opacity: 0%;
+ }
+ }
}
:root {
--accent-color: #007acc;
@@ -4744,6 +5061,9 @@ button svg, button svg * {
padding: 14px 20px;
font: inherit;
}
+.alert-modal-error {
+ border-top: 3px solid #ff6b6b;
+}
.menu-editor-header h2 {
margin: 0;
}
@@ -6492,6 +6812,21 @@ button.import-taxonomy-pill {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
}
}
+@property --tw-translate-x {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-translate-y {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-translate-z {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
@@ -6517,10 +6852,83 @@ button.import-taxonomy-pill {
inherits: false;
initial-value: solid;
}
+@property --tw-leading {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-font-weight {
+ syntax: "*";
+ inherits: false;
+}
@property --tw-tracking {
syntax: "*";
inherits: false;
}
+@property --tw-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-shadow-alpha {
+ syntax: "
";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-inset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-inset-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-inset-shadow-alpha {
+ syntax: "";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-ring-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-inset-ring-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-inset-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-ring-inset {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ring-offset-width {
+ syntax: "";
+ inherits: false;
+ initial-value: 0px;
+}
+@property --tw-ring-offset-color {
+ syntax: "*";
+ inherits: false;
+ initial-value: #fff;
+}
+@property --tw-ring-offset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
@property --tw-outline-style {
syntax: "*";
inherits: false;
@@ -6579,6 +6987,29 @@ button.import-taxonomy-pill {
syntax: "*";
inherits: false;
}
+@property --tw-duration {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ease {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-scale-x {
+ syntax: "*";
+ inherits: false;
+ initial-value: 1;
+}
+@property --tw-scale-y {
+ syntax: "*";
+ inherits: false;
+ initial-value: 1;
+}
+@property --tw-scale-z {
+ syntax: "*";
+ inherits: false;
+ initial-value: 1;
+}
@keyframes spin {
to {
transform: rotate(360deg);
@@ -6587,13 +7018,32 @@ button.import-taxonomy-pill {
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-translate-z: 0;
--tw-rotate-x: initial;
--tw-rotate-y: initial;
--tw-rotate-z: initial;
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-border-style: solid;
+ --tw-leading: initial;
+ --tw-font-weight: initial;
--tw-tracking: initial;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-color: initial;
+ --tw-shadow-alpha: 100%;
+ --tw-inset-shadow: 0 0 #0000;
+ --tw-inset-shadow-color: initial;
+ --tw-inset-shadow-alpha: 100%;
+ --tw-ring-color: initial;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-inset-ring-color: initial;
+ --tw-inset-ring-shadow: 0 0 #0000;
+ --tw-ring-inset: initial;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-offset-shadow: 0 0 #0000;
--tw-outline-style: solid;
--tw-blur: initial;
--tw-brightness: initial;
@@ -6608,6 +7058,11 @@ button.import-taxonomy-pill {
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
+ --tw-duration: initial;
+ --tw-ease: initial;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-scale-z: 1;
}
}
}
diff --git a/priv/static/assets/app.js b/priv/static/assets/app.js
index d58b0bd..7c467f1 100644
--- a/priv/static/assets/app.js
+++ b/priv/static/assets/app.js
@@ -542,7 +542,7 @@
const _timeoutId = setTimeout(() => controller.abort(), timeout);
options.signal = controller.signal;
}
- global.fetch(endPoint, options).then((response) => response.text()).then((data) => this.parseJSON(data)).then((data) => callback && callback(data)).catch((err) => {
+ global.fetch(endPoint, options).then((response) => response.text()).then((data2) => this.parseJSON(data2)).then((data2) => callback && callback(data2)).catch((err) => {
if (err.name === "AbortError" && ontimeout) {
ontimeout();
} else {
@@ -869,8 +869,8 @@
offset = offset + topicSize;
let event = decoder.decode(buffer.slice(offset, offset + eventSize));
offset = offset + eventSize;
- let data = buffer.slice(offset, buffer.byteLength);
- return { join_ref: joinRef, ref: null, topic, event, payload: data };
+ let data2 = buffer.slice(offset, buffer.byteLength);
+ return { join_ref: joinRef, ref: null, topic, event, payload: data2 };
},
decodeReply(buffer, view, decoder) {
let joinRefSize = view.getUint8(1);
@@ -886,8 +886,8 @@
offset = offset + topicSize;
let event = decoder.decode(buffer.slice(offset, offset + eventSize));
offset = offset + eventSize;
- let data = buffer.slice(offset, buffer.byteLength);
- let payload = { status: event, response: data };
+ let data2 = buffer.slice(offset, buffer.byteLength);
+ let payload = { status: event, response: data2 };
return { join_ref: joinRef, ref, topic, event: CHANNEL_EVENTS.reply, payload };
},
decodeBroadcast(buffer, view, decoder) {
@@ -898,8 +898,8 @@
offset = offset + topicSize;
let event = decoder.decode(buffer.slice(offset, offset + eventSize));
offset = offset + eventSize;
- let data = buffer.slice(offset, buffer.byteLength);
- return { join_ref: null, ref: null, topic, event, payload: data };
+ let data2 = buffer.slice(offset, buffer.byteLength);
+ return { join_ref: null, ref: null, topic, event, payload: data2 };
}
};
var Socket = class {
@@ -972,8 +972,8 @@
};
this.logger = opts.logger || null;
if (!this.logger && opts.debug) {
- this.logger = (kind, msg, data) => {
- console.log(`${kind}: ${msg}`, data);
+ this.logger = (kind, msg, data2) => {
+ console.log(`${kind}: ${msg}`, data2);
};
}
this.longpollerTimeout = opts.longpollerTimeout || 2e4;
@@ -1089,8 +1089,8 @@
* @param {string} msg
* @param {Object} data
*/
- log(kind, msg, data) {
- this.logger && this.logger(kind, msg, data);
+ log(kind, msg, data2) {
+ this.logger && this.logger(kind, msg, data2);
}
/**
* Returns true if a logger has been set on this socket.
@@ -1419,15 +1419,15 @@
/**
* @param {Object} data
*/
- push(data) {
+ push(data2) {
if (this.hasLogger()) {
- let { topic, event, payload, ref, join_ref } = data;
+ let { topic, event, payload, ref, join_ref } = data2;
this.log("push", `${topic} ${event} (${join_ref}, ${ref})`, payload);
}
if (this.isConnected()) {
- this.encode(data, (result) => this.conn.send(result));
+ this.encode(data2, (result) => this.conn.send(result));
} else {
- this.sendBuffer.push(() => this.encode(data, (result) => this.conn.send(result)));
+ this.sendBuffer.push(() => this.encode(data2, (result) => this.conn.send(result)));
}
}
/**
@@ -2417,8 +2417,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
metadata() {
return this.meta;
}
- progress(progress) {
- this._progress = Math.floor(progress);
+ progress(progress2) {
+ this._progress = Math.floor(progress2);
if (this._progress > this._lastProgressSent) {
if (this._progress >= 100) {
this._progress = 100;
@@ -4693,10 +4693,10 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
var default_transition_time = 200;
var JS = {
// private
- exec(e, eventType, phxEvent, view, sourceEl, defaults) {
- const [defaultKind, defaultArgs] = defaults || [
+ exec(e, eventType, phxEvent, view, sourceEl, defaults2) {
+ const [defaultKind, defaultArgs] = defaults2 || [
null,
- { callback: defaults && defaults.callback }
+ { callback: defaults2 && defaults2.callback }
];
const commands = phxEvent.charAt(0) === "[" ? JSON.parse(phxEvent) : [[defaultKind, defaultArgs]];
commands.forEach(([kind, args]) => {
@@ -4742,7 +4742,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
exec_push(e, eventType, phxEvent, view, sourceEl, el, args) {
const {
event,
- data,
+ data: data2,
target,
page_loading,
loading,
@@ -4793,7 +4793,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
sourceEl,
targetCtx,
event || phxEvent,
- data,
+ data2,
pushOpts,
callback
);
@@ -4848,14 +4848,14 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
});
}
},
- exec_add_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {
- this.addOrRemoveClasses(el, names, [], transition, time, view, blocking);
+ exec_add_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time: time2, blocking }) {
+ this.addOrRemoveClasses(el, names, [], transition, time2, view, blocking);
},
- exec_remove_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {
- this.addOrRemoveClasses(el, [], names, transition, time, view, blocking);
+ exec_remove_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time: time2, blocking }) {
+ this.addOrRemoveClasses(el, [], names, transition, time2, view, blocking);
},
- exec_toggle_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {
- this.toggleClasses(el, names, transition, time, view, blocking);
+ exec_toggle_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time: time2, blocking }) {
+ this.toggleClasses(el, names, transition, time2, view, blocking);
},
exec_toggle_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val1, val2] }) {
this.toggleAttr(el, attr, val1, val2);
@@ -4863,17 +4863,17 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
exec_ignore_attrs(e, eventType, phxEvent, view, sourceEl, el, { attrs }) {
this.ignoreAttrs(el, attrs);
},
- exec_transition(e, eventType, phxEvent, view, sourceEl, el, { time, transition, blocking }) {
- this.addOrRemoveClasses(el, [], [], transition, time, view, blocking);
+ exec_transition(e, eventType, phxEvent, view, sourceEl, el, { time: time2, transition, blocking }) {
+ this.addOrRemoveClasses(el, [], [], transition, time2, view, blocking);
},
- exec_toggle(e, eventType, phxEvent, view, sourceEl, el, { display, ins, outs, time, blocking }) {
- this.toggle(eventType, view, el, display, ins, outs, time, blocking);
+ exec_toggle(e, eventType, phxEvent, view, sourceEl, el, { display, ins, outs, time: time2, blocking }) {
+ this.toggle(eventType, view, el, display, ins, outs, time2, blocking);
},
- exec_show(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) {
- this.show(eventType, view, el, display, transition, time, blocking);
+ exec_show(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time: time2, blocking }) {
+ this.show(eventType, view, el, display, transition, time2, blocking);
},
- exec_hide(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) {
- this.hide(eventType, view, el, display, transition, time, blocking);
+ exec_hide(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time: time2, blocking }) {
+ this.hide(eventType, view, el, display, transition, time2, blocking);
},
exec_set_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val] }) {
this.setOrRemoveAttrs(el, [[attr, val]], []);
@@ -4908,7 +4908,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}
},
// utils for commands
- show(eventType, view, el, display, transition, time, blocking) {
+ show(eventType, view, el, display, transition, time2, blocking) {
if (!this.isVisible(el)) {
this.toggle(
eventType,
@@ -4917,12 +4917,12 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
display,
transition,
null,
- time,
+ time2,
blocking
);
}
},
- hide(eventType, view, el, display, transition, time, blocking) {
+ hide(eventType, view, el, display, transition, time2, blocking) {
if (this.isVisible(el)) {
this.toggle(
eventType,
@@ -4931,13 +4931,13 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
display,
null,
transition,
- time,
+ time2,
blocking
);
}
},
- toggle(eventType, view, el, display, ins, outs, time, blocking) {
- time = time || default_transition_time;
+ toggle(eventType, view, el, display, ins, outs, time2, blocking) {
+ time2 = time2 || default_transition_time;
const [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []];
const [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []];
if (inClasses.length > 0 || outClasses.length > 0) {
@@ -4967,9 +4967,9 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
el.dispatchEvent(new Event("phx:hide-start"));
if (blocking === false) {
onStart();
- setTimeout(onEnd, time);
+ setTimeout(onEnd, time2);
} else {
- view.transition(time, onStart, onEnd);
+ view.transition(time2, onStart, onEnd);
}
} else {
if (eventType === "remove") {
@@ -5001,9 +5001,9 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
el.dispatchEvent(new Event("phx:show-start"));
if (blocking === false) {
onStart();
- setTimeout(onEnd, time);
+ setTimeout(onEnd, time2);
} else {
- view.transition(time, onStart, onEnd);
+ view.transition(time2, onStart, onEnd);
}
}
} else {
@@ -5031,7 +5031,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}
}
},
- toggleClasses(el, classes, transition, time, view, blocking) {
+ toggleClasses(el, classes, transition, time2, view, blocking) {
window.requestAnimationFrame(() => {
const [prevAdds, prevRemoves] = dom_default.getSticky(el, "classes", [[], []]);
const newAdds = classes.filter(
@@ -5045,7 +5045,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
newAdds,
newRemoves,
transition,
- time,
+ time2,
view,
blocking
);
@@ -5066,8 +5066,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
this.setOrRemoveAttrs(el, [[attr, val1]], []);
}
},
- addOrRemoveClasses(el, adds, removes, transition, time, view, blocking) {
- time = time || default_transition_time;
+ addOrRemoveClasses(el, adds, removes, transition, time2, view, blocking) {
+ time2 = time2 || default_transition_time;
const [transitionRun, transitionStart, transitionEnd] = transition || [
[],
[],
@@ -5094,9 +5094,9 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
);
if (blocking === false) {
onStart();
- setTimeout(onDone, time);
+ setTimeout(onDone, time2);
} else {
- view.transition(time, onStart, onDone);
+ view.transition(time2, onStart, onDone);
}
return;
}
@@ -5267,10 +5267,10 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
},
push(el, type, opts = {}) {
liveSocket.withinOwners(el, (view) => {
- const data = opts.value || {};
+ const data2 = opts.value || {};
delete opts.value;
let e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
- js_default.exec(e, eventType, type, view, el, ["push", { data, ...opts }]);
+ js_default.exec(e, eventType, type, view, el, ["push", { data: data2, ...opts }]);
});
},
navigate(href, opts = {}) {
@@ -5565,9 +5565,9 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
onlyHiddenInputs2[key] = true;
}
const isUsed = dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED);
- const isHidden = input.type === "hidden";
+ const isHidden2 = input.type === "hidden";
inputsUnused2[key] = inputsUnused2[key] && !isUsed;
- onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden;
+ onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden2;
return acc;
},
{ inputsUnused: {}, onlyHiddenInputs: {} }
@@ -5751,9 +5751,9 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
log(kind, msgCallback) {
this.liveSocket.log(this, kind, msgCallback);
}
- transition(time, onStart, onDone = function() {
+ transition(time2, onStart, onDone = function() {
}) {
- this.liveSocket.transition(time, onStart, onDone);
+ this.liveSocket.transition(time2, onStart, onDone);
}
// calls the callback with the view and target element for the given phxTarget
// targets can be:
@@ -6831,14 +6831,14 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}
).then(({ reply }) => onReply && onReply(reply)).catch((error) => logError("Failed to push event", error));
}
- pushFileProgress(fileEl, entryRef, progress, onReply = function() {
+ pushFileProgress(fileEl, entryRef, progress2, onReply = function() {
}) {
this.liveSocket.withinOwners(fileEl.form, (view, targetCtx) => {
view.pushWithReply(null, "progress", {
event: fileEl.getAttribute(view.binding(PHX_PROGRESS)),
ref: fileEl.getAttribute(PHX_UPLOAD_REF),
entry_ref: entryRef,
- progress,
+ progress: progress2,
cid: view.targetComponentID(fileEl.form, targetCtx)
}).then(() => onReply()).catch((error) => logError("Failed to push file progress", error));
});
@@ -7492,17 +7492,17 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
asyncTransition(promise) {
this.transitions.addAsyncTransition(promise);
}
- transition(time, onStart, onDone = function() {
+ transition(time2, onStart, onDone = function() {
}) {
- this.transitions.addTransition(time, onStart, onDone);
+ this.transitions.addTransition(time2, onStart, onDone);
}
onChannel(channel, event, cb) {
- channel.on(event, (data) => {
+ channel.on(event, (data2) => {
const latency = this.getLatencySim();
if (!latency) {
- cb(data);
+ cb(data2);
} else {
- setTimeout(() => cb(data), latency);
+ setTimeout(() => cb(data2), latency);
}
});
}
@@ -7788,16 +7788,16 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
if (matchKey && matchKey.toLowerCase() !== pressedKey) {
return;
}
- const data = { key: e.key, ...this.eventMeta(type, e, targetEl) };
- js_default.exec(e, type, phxEvent, view, targetEl, ["push", { data }]);
+ const data2 = { key: e.key, ...this.eventMeta(type, e, targetEl) };
+ js_default.exec(e, type, phxEvent, view, targetEl, ["push", { data: data2 }]);
}
);
this.bind(
{ blur: "focusout", focus: "focusin" },
(e, type, view, targetEl, phxEvent, phxTarget) => {
if (!phxTarget) {
- const data = { key: e.key, ...this.eventMeta(type, e, targetEl) };
- js_default.exec(e, type, phxEvent, view, targetEl, ["push", { data }]);
+ const data2 = { key: e.key, ...this.eventMeta(type, e, targetEl) };
+ js_default.exec(e, type, phxEvent, view, targetEl, ["push", { data: data2 }]);
}
}
);
@@ -7805,8 +7805,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
{ blur: "blur", focus: "focus" },
(e, type, view, targetEl, phxEvent, phxTarget) => {
if (phxTarget === "window") {
- const data = this.eventMeta(type, e, targetEl);
- js_default.exec(e, type, phxEvent, view, targetEl, ["push", { data }]);
+ const data2 = this.eventMeta(type, e, targetEl);
+ js_default.exec(e, type, phxEvent, view, targetEl, ["push", { data: data2 }]);
}
}
);
@@ -8360,13 +8360,13 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
this.pushPendingOp(callback);
}
}
- addTransition(time, onStart, onDone) {
+ addTransition(time2, onStart, onDone) {
onStart();
const timer = setTimeout(() => {
this.transitions.delete(timer);
onDone();
this.flushPendingOps();
- }, time);
+ }, time2);
this.transitions.add(timer);
}
addAsyncTransition(promise) {
@@ -9842,8 +9842,816 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}
};
+ // ../deps/live_toast/priv/static/live_toast.esm.js
+ function addUniqueItem(array, item) {
+ array.indexOf(item) === -1 && array.push(item);
+ }
+ var clamp2 = (min, max, v) => Math.min(Math.max(v, min), max);
+ var defaults = {
+ duration: 0.3,
+ delay: 0,
+ endDelay: 0,
+ repeat: 0,
+ easing: "ease"
+ };
+ var isNumber = (value) => typeof value === "number";
+ var isEasingList = (easing) => Array.isArray(easing) && !isNumber(easing[0]);
+ var wrap = (min, max, v) => {
+ const rangeSize = max - min;
+ return ((v - min) % rangeSize + rangeSize) % rangeSize + min;
+ };
+ function getEasingForSegment(easing, i) {
+ return isEasingList(easing) ? easing[wrap(0, easing.length, i)] : easing;
+ }
+ var mix = (min, max, progress2) => -progress2 * min + progress2 * max + min;
+ var noop2 = () => {
+ };
+ var noopReturn = (v) => v;
+ var progress = (min, max, value) => max - min === 0 ? 1 : (value - min) / (max - min);
+ function fillOffset(offset, remaining) {
+ const min = offset[offset.length - 1];
+ for (let i = 1; i <= remaining; i++) {
+ const offsetProgress = progress(0, remaining, i);
+ offset.push(mix(min, 1, offsetProgress));
+ }
+ }
+ function defaultOffset(length) {
+ const offset = [0];
+ fillOffset(offset, length - 1);
+ return offset;
+ }
+ function interpolate(output, input = defaultOffset(output.length), easing = noopReturn) {
+ const length = output.length;
+ const remainder = length - input.length;
+ remainder > 0 && fillOffset(input, remainder);
+ return (t) => {
+ let i = 0;
+ for (; i < length - 2; i++) {
+ if (t < input[i + 1])
+ break;
+ }
+ let progressInRange = clamp2(0, 1, progress(input[i], input[i + 1], t));
+ const segmentEasing = getEasingForSegment(easing, i);
+ progressInRange = segmentEasing(progressInRange);
+ return mix(output[i], output[i + 1], progressInRange);
+ };
+ }
+ var isCubicBezier = (easing) => Array.isArray(easing) && isNumber(easing[0]);
+ var isEasingGenerator = (easing) => typeof easing === "object" && Boolean(easing.createAnimation);
+ var isFunction = (value) => typeof value === "function";
+ var isString = (value) => typeof value === "string";
+ var time = {
+ ms: (seconds) => seconds * 1e3,
+ s: (milliseconds) => milliseconds / 1e3
+ };
+ var calcBezier = (t, a1, a2) => (((1 - 3 * a2 + 3 * a1) * t + (3 * a2 - 6 * a1)) * t + 3 * a1) * t;
+ var subdivisionPrecision = 1e-7;
+ var subdivisionMaxIterations = 12;
+ function binarySubdivide(x, lowerBound, upperBound, mX1, mX2) {
+ let currentX;
+ let currentT;
+ let i = 0;
+ do {
+ currentT = lowerBound + (upperBound - lowerBound) / 2;
+ currentX = calcBezier(currentT, mX1, mX2) - x;
+ if (currentX > 0) {
+ upperBound = currentT;
+ } else {
+ lowerBound = currentT;
+ }
+ } while (Math.abs(currentX) > subdivisionPrecision && ++i < subdivisionMaxIterations);
+ return currentT;
+ }
+ function cubicBezier(mX1, mY1, mX2, mY2) {
+ if (mX1 === mY1 && mX2 === mY2)
+ return noopReturn;
+ const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2);
+ return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2);
+ }
+ var steps = (steps2, direction = "end") => (progress2) => {
+ progress2 = direction === "end" ? Math.min(progress2, 0.999) : Math.max(progress2, 1e-3);
+ const expanded = progress2 * steps2;
+ const rounded = direction === "end" ? Math.floor(expanded) : Math.ceil(expanded);
+ return clamp2(0, 1, rounded / steps2);
+ };
+ var namedEasings = {
+ ease: cubicBezier(0.25, 0.1, 0.25, 1),
+ "ease-in": cubicBezier(0.42, 0, 1, 1),
+ "ease-in-out": cubicBezier(0.42, 0, 0.58, 1),
+ "ease-out": cubicBezier(0, 0, 0.58, 1)
+ };
+ var functionArgsRegex = /\((.*?)\)/;
+ function getEasingFunction(definition) {
+ if (isFunction(definition))
+ return definition;
+ if (isCubicBezier(definition))
+ return cubicBezier(...definition);
+ if (namedEasings[definition])
+ return namedEasings[definition];
+ if (definition.startsWith("steps")) {
+ const args = functionArgsRegex.exec(definition);
+ if (args) {
+ const argsArray = args[1].split(",");
+ return steps(parseFloat(argsArray[0]), argsArray[1].trim());
+ }
+ }
+ return noopReturn;
+ }
+ var Animation = class {
+ constructor(output, keyframes = [0, 1], { easing, duration: initialDuration = defaults.duration, delay = defaults.delay, endDelay = defaults.endDelay, repeat = defaults.repeat, offset, direction = "normal", autoplay = true } = {}) {
+ this.startTime = null;
+ this.rate = 1;
+ this.t = 0;
+ this.cancelTimestamp = null;
+ this.easing = noopReturn;
+ this.duration = 0;
+ this.totalDuration = 0;
+ this.repeat = 0;
+ this.playState = "idle";
+ this.finished = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ easing = easing || defaults.easing;
+ if (isEasingGenerator(easing)) {
+ const custom = easing.createAnimation(keyframes);
+ easing = custom.easing;
+ keyframes = custom.keyframes || keyframes;
+ initialDuration = custom.duration || initialDuration;
+ }
+ this.repeat = repeat;
+ this.easing = isEasingList(easing) ? noopReturn : getEasingFunction(easing);
+ this.updateDuration(initialDuration);
+ const interpolate$1 = interpolate(keyframes, offset, isEasingList(easing) ? easing.map(getEasingFunction) : noopReturn);
+ this.tick = (timestamp) => {
+ var _a;
+ delay = delay;
+ let t = 0;
+ if (this.pauseTime !== void 0) {
+ t = this.pauseTime;
+ } else {
+ t = (timestamp - this.startTime) * this.rate;
+ }
+ this.t = t;
+ t /= 1e3;
+ t = Math.max(t - delay, 0);
+ if (this.playState === "finished" && this.pauseTime === void 0) {
+ t = this.totalDuration;
+ }
+ const progress2 = t / this.duration;
+ let currentIteration = Math.floor(progress2);
+ let iterationProgress = progress2 % 1;
+ if (!iterationProgress && progress2 >= 1) {
+ iterationProgress = 1;
+ }
+ iterationProgress === 1 && currentIteration--;
+ const iterationIsOdd = currentIteration % 2;
+ if (direction === "reverse" || direction === "alternate" && iterationIsOdd || direction === "alternate-reverse" && !iterationIsOdd) {
+ iterationProgress = 1 - iterationProgress;
+ }
+ const p = t >= this.totalDuration ? 1 : Math.min(iterationProgress, 1);
+ const latest = interpolate$1(this.easing(p));
+ output(latest);
+ const isAnimationFinished = this.pauseTime === void 0 && (this.playState === "finished" || t >= this.totalDuration + endDelay);
+ if (isAnimationFinished) {
+ this.playState = "finished";
+ (_a = this.resolve) === null || _a === void 0 ? void 0 : _a.call(this, latest);
+ } else if (this.playState !== "idle") {
+ this.frameRequestId = requestAnimationFrame(this.tick);
+ }
+ };
+ if (autoplay)
+ this.play();
+ }
+ play() {
+ const now = performance.now();
+ this.playState = "running";
+ if (this.pauseTime !== void 0) {
+ this.startTime = now - this.pauseTime;
+ } else if (!this.startTime) {
+ this.startTime = now;
+ }
+ this.cancelTimestamp = this.startTime;
+ this.pauseTime = void 0;
+ this.frameRequestId = requestAnimationFrame(this.tick);
+ }
+ pause() {
+ this.playState = "paused";
+ this.pauseTime = this.t;
+ }
+ finish() {
+ this.playState = "finished";
+ this.tick(0);
+ }
+ stop() {
+ var _a;
+ this.playState = "idle";
+ if (this.frameRequestId !== void 0) {
+ cancelAnimationFrame(this.frameRequestId);
+ }
+ (_a = this.reject) === null || _a === void 0 ? void 0 : _a.call(this, false);
+ }
+ cancel() {
+ this.stop();
+ this.tick(this.cancelTimestamp);
+ }
+ reverse() {
+ this.rate *= -1;
+ }
+ commitStyles() {
+ }
+ updateDuration(duration) {
+ this.duration = duration;
+ this.totalDuration = duration * (this.repeat + 1);
+ }
+ get currentTime() {
+ return this.t;
+ }
+ set currentTime(t) {
+ if (this.pauseTime !== void 0 || this.rate === 0) {
+ this.pauseTime = t;
+ } else {
+ this.startTime = performance.now() - t / this.rate;
+ }
+ }
+ get playbackRate() {
+ return this.rate;
+ }
+ set playbackRate(rate) {
+ this.rate = rate;
+ }
+ };
+ var warning = function() {
+ };
+ var invariant = function() {
+ };
+ if (true) {
+ warning = function(check, message) {
+ if (!check && typeof console !== "undefined") {
+ console.warn(message);
+ }
+ };
+ invariant = function(check, message) {
+ if (!check) {
+ throw new Error(message);
+ }
+ };
+ }
+ var MotionValue = class {
+ setAnimation(animation) {
+ this.animation = animation;
+ animation === null || animation === void 0 ? void 0 : animation.finished.then(() => this.clearAnimation()).catch(() => {
+ });
+ }
+ clearAnimation() {
+ this.animation = this.generator = void 0;
+ }
+ };
+ var data = /* @__PURE__ */ new WeakMap();
+ function getAnimationData(element) {
+ if (!data.has(element)) {
+ data.set(element, {
+ transforms: [],
+ values: /* @__PURE__ */ new Map()
+ });
+ }
+ return data.get(element);
+ }
+ function getMotionValue(motionValues, name) {
+ if (!motionValues.has(name)) {
+ motionValues.set(name, new MotionValue());
+ }
+ return motionValues.get(name);
+ }
+ var axes = ["", "X", "Y", "Z"];
+ var order = ["translate", "scale", "rotate", "skew"];
+ var transformAlias = {
+ x: "translateX",
+ y: "translateY",
+ z: "translateZ"
+ };
+ var rotation = {
+ syntax: "",
+ initialValue: "0deg",
+ toDefaultUnit: (v) => v + "deg"
+ };
+ var baseTransformProperties = {
+ translate: {
+ syntax: "",
+ initialValue: "0px",
+ toDefaultUnit: (v) => v + "px"
+ },
+ rotate: rotation,
+ scale: {
+ syntax: "",
+ initialValue: 1,
+ toDefaultUnit: noopReturn
+ },
+ skew: rotation
+ };
+ var transformDefinitions = /* @__PURE__ */ new Map();
+ var asTransformCssVar = (name) => `--motion-${name}`;
+ var transforms = ["x", "y", "z"];
+ order.forEach((name) => {
+ axes.forEach((axis) => {
+ transforms.push(name + axis);
+ transformDefinitions.set(asTransformCssVar(name + axis), baseTransformProperties[name]);
+ });
+ });
+ var compareTransformOrder = (a, b) => transforms.indexOf(a) - transforms.indexOf(b);
+ var transformLookup = new Set(transforms);
+ var isTransform = (name) => transformLookup.has(name);
+ var addTransformToElement = (element, name) => {
+ if (transformAlias[name])
+ name = transformAlias[name];
+ const { transforms: transforms2 } = getAnimationData(element);
+ addUniqueItem(transforms2, name);
+ element.style.transform = buildTransformTemplate(transforms2);
+ };
+ var buildTransformTemplate = (transforms2) => transforms2.sort(compareTransformOrder).reduce(transformListToString, "").trim();
+ var transformListToString = (template, name) => `${template} ${name}(var(${asTransformCssVar(name)}))`;
+ var isCssVar = (name) => name.startsWith("--");
+ var registeredProperties = /* @__PURE__ */ new Set();
+ function registerCssVariable(name) {
+ if (registeredProperties.has(name))
+ return;
+ registeredProperties.add(name);
+ try {
+ const { syntax, initialValue } = transformDefinitions.has(name) ? transformDefinitions.get(name) : {};
+ CSS.registerProperty({
+ name,
+ inherits: false,
+ syntax,
+ initialValue
+ });
+ } catch (e) {
+ }
+ }
+ var testAnimation = (keyframes, options) => document.createElement("div").animate(keyframes, options);
+ var featureTests = {
+ cssRegisterProperty: () => typeof CSS !== "undefined" && Object.hasOwnProperty.call(CSS, "registerProperty"),
+ waapi: () => Object.hasOwnProperty.call(Element.prototype, "animate"),
+ partialKeyframes: () => {
+ try {
+ testAnimation({ opacity: [1] });
+ } catch (e) {
+ return false;
+ }
+ return true;
+ },
+ finished: () => Boolean(testAnimation({ opacity: [0, 1] }, { duration: 1e-3 }).finished),
+ linearEasing: () => {
+ try {
+ testAnimation({ opacity: 0 }, { easing: "linear(0, 1)" });
+ } catch (e) {
+ return false;
+ }
+ return true;
+ }
+ };
+ var results = {};
+ var supports = {};
+ for (const key in featureTests) {
+ supports[key] = () => {
+ if (results[key] === void 0)
+ results[key] = featureTests[key]();
+ return results[key];
+ };
+ }
+ var resolution = 0.015;
+ var generateLinearEasingPoints = (easing, duration) => {
+ let points = "";
+ const numPoints = Math.round(duration / resolution);
+ for (let i = 0; i < numPoints; i++) {
+ points += easing(progress(0, numPoints - 1, i)) + ", ";
+ }
+ return points.substring(0, points.length - 2);
+ };
+ var convertEasing = (easing, duration) => {
+ if (isFunction(easing)) {
+ return supports.linearEasing() ? `linear(${generateLinearEasingPoints(easing, duration)})` : defaults.easing;
+ } else {
+ return isCubicBezier(easing) ? cubicBezierAsString(easing) : easing;
+ }
+ };
+ var cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
+ function hydrateKeyframes(keyframes, readInitialValue) {
+ for (let i = 0; i < keyframes.length; i++) {
+ if (keyframes[i] === null) {
+ keyframes[i] = i ? keyframes[i - 1] : readInitialValue();
+ }
+ }
+ return keyframes;
+ }
+ var keyframesList = (keyframes) => Array.isArray(keyframes) ? keyframes : [keyframes];
+ function getStyleName(key) {
+ if (transformAlias[key])
+ key = transformAlias[key];
+ return isTransform(key) ? asTransformCssVar(key) : key;
+ }
+ var style = {
+ get: (element, name) => {
+ name = getStyleName(name);
+ let value = isCssVar(name) ? element.style.getPropertyValue(name) : getComputedStyle(element)[name];
+ if (!value && value !== 0) {
+ const definition = transformDefinitions.get(name);
+ if (definition)
+ value = definition.initialValue;
+ }
+ return value;
+ },
+ set: (element, name, value) => {
+ name = getStyleName(name);
+ if (isCssVar(name)) {
+ element.style.setProperty(name, value);
+ } else {
+ element.style[name] = value;
+ }
+ }
+ };
+ function stopAnimation(animation, needsCommit = true) {
+ if (!animation || animation.playState === "finished")
+ return;
+ try {
+ if (animation.stop) {
+ animation.stop();
+ } else {
+ needsCommit && animation.commitStyles();
+ animation.cancel();
+ }
+ } catch (e) {
+ }
+ }
+ function getUnitConverter(keyframes, definition) {
+ var _a;
+ let toUnit = (definition === null || definition === void 0 ? void 0 : definition.toDefaultUnit) || noopReturn;
+ const finalKeyframe = keyframes[keyframes.length - 1];
+ if (isString(finalKeyframe)) {
+ const unit = ((_a = finalKeyframe.match(/(-?[\d.]+)([a-z%]*)/)) === null || _a === void 0 ? void 0 : _a[2]) || "";
+ if (unit)
+ toUnit = (value) => value + unit;
+ }
+ return toUnit;
+ }
+ function getDevToolsRecord() {
+ return window.__MOTION_DEV_TOOLS_RECORD;
+ }
+ function animateStyle(element, key, keyframesDefinition, options = {}, AnimationPolyfill) {
+ const record = getDevToolsRecord();
+ const isRecording = options.record !== false && record;
+ let animation;
+ let { duration = defaults.duration, delay = defaults.delay, endDelay = defaults.endDelay, repeat = defaults.repeat, easing = defaults.easing, persist = false, direction, offset, allowWebkitAcceleration = false, autoplay = true } = options;
+ const data2 = getAnimationData(element);
+ const valueIsTransform = isTransform(key);
+ let canAnimateNatively = supports.waapi();
+ valueIsTransform && addTransformToElement(element, key);
+ const name = getStyleName(key);
+ const motionValue = getMotionValue(data2.values, name);
+ const definition = transformDefinitions.get(name);
+ stopAnimation(motionValue.animation, !(isEasingGenerator(easing) && motionValue.generator) && options.record !== false);
+ return () => {
+ const readInitialValue = () => {
+ var _a, _b;
+ return (_b = (_a = style.get(element, name)) !== null && _a !== void 0 ? _a : definition === null || definition === void 0 ? void 0 : definition.initialValue) !== null && _b !== void 0 ? _b : 0;
+ };
+ let keyframes = hydrateKeyframes(keyframesList(keyframesDefinition), readInitialValue);
+ const toUnit = getUnitConverter(keyframes, definition);
+ if (isEasingGenerator(easing)) {
+ const custom = easing.createAnimation(keyframes, key !== "opacity", readInitialValue, name, motionValue);
+ easing = custom.easing;
+ keyframes = custom.keyframes || keyframes;
+ duration = custom.duration || duration;
+ }
+ if (isCssVar(name)) {
+ if (supports.cssRegisterProperty()) {
+ registerCssVariable(name);
+ } else {
+ canAnimateNatively = false;
+ }
+ }
+ if (valueIsTransform && !supports.linearEasing() && (isFunction(easing) || isEasingList(easing) && easing.some(isFunction))) {
+ canAnimateNatively = false;
+ }
+ if (canAnimateNatively) {
+ if (definition) {
+ keyframes = keyframes.map((value) => isNumber(value) ? definition.toDefaultUnit(value) : value);
+ }
+ if (keyframes.length === 1 && (!supports.partialKeyframes() || isRecording)) {
+ keyframes.unshift(readInitialValue());
+ }
+ const animationOptions = {
+ delay: time.ms(delay),
+ duration: time.ms(duration),
+ endDelay: time.ms(endDelay),
+ easing: !isEasingList(easing) ? convertEasing(easing, duration) : void 0,
+ direction,
+ iterations: repeat + 1,
+ fill: "both"
+ };
+ animation = element.animate({
+ [name]: keyframes,
+ offset,
+ easing: isEasingList(easing) ? easing.map((thisEasing) => convertEasing(thisEasing, duration)) : void 0
+ }, animationOptions);
+ if (!animation.finished) {
+ animation.finished = new Promise((resolve, reject) => {
+ animation.onfinish = resolve;
+ animation.oncancel = reject;
+ });
+ }
+ const target = keyframes[keyframes.length - 1];
+ animation.finished.then(() => {
+ if (persist)
+ return;
+ style.set(element, name, target);
+ animation.cancel();
+ }).catch(noop2);
+ if (!allowWebkitAcceleration)
+ animation.playbackRate = 1.000001;
+ } else if (AnimationPolyfill && valueIsTransform) {
+ keyframes = keyframes.map((value) => typeof value === "string" ? parseFloat(value) : value);
+ if (keyframes.length === 1) {
+ keyframes.unshift(parseFloat(readInitialValue()));
+ }
+ animation = new AnimationPolyfill((latest) => {
+ style.set(element, name, toUnit ? toUnit(latest) : latest);
+ }, keyframes, Object.assign(Object.assign({}, options), {
+ duration,
+ easing
+ }));
+ } else {
+ const target = keyframes[keyframes.length - 1];
+ style.set(element, name, definition && isNumber(target) ? definition.toDefaultUnit(target) : target);
+ }
+ if (isRecording) {
+ record(element, key, keyframes, {
+ duration,
+ delay,
+ easing,
+ repeat,
+ offset
+ }, "motion-one");
+ }
+ motionValue.setAnimation(animation);
+ if (animation && !autoplay)
+ animation.pause();
+ return animation;
+ };
+ }
+ var getOptions = (options, key) => options[key] ? Object.assign(Object.assign({}, options), options[key]) : Object.assign({}, options);
+ function resolveElements(elements, selectorCache) {
+ var _a;
+ if (typeof elements === "string") {
+ if (selectorCache) {
+ (_a = selectorCache[elements]) !== null && _a !== void 0 ? _a : selectorCache[elements] = document.querySelectorAll(elements);
+ elements = selectorCache[elements];
+ } else {
+ elements = document.querySelectorAll(elements);
+ }
+ } else if (elements instanceof Element) {
+ elements = [elements];
+ }
+ return Array.from(elements || []);
+ }
+ var createAnimation = (factory) => factory();
+ var withControls = (animationFactory, options, duration = defaults.duration) => {
+ return new Proxy({
+ animations: animationFactory.map(createAnimation).filter(Boolean),
+ duration,
+ options
+ }, controls);
+ };
+ var getActiveAnimation = (state) => state.animations[0];
+ var controls = {
+ get: (target, key) => {
+ const activeAnimation = getActiveAnimation(target);
+ switch (key) {
+ case "duration":
+ return target.duration;
+ case "currentTime":
+ return time.s((activeAnimation === null || activeAnimation === void 0 ? void 0 : activeAnimation[key]) || 0);
+ case "playbackRate":
+ case "playState":
+ return activeAnimation === null || activeAnimation === void 0 ? void 0 : activeAnimation[key];
+ case "finished":
+ if (!target.finished) {
+ target.finished = Promise.all(target.animations.map(selectFinished)).catch(noop2);
+ }
+ return target.finished;
+ case "stop":
+ return () => {
+ target.animations.forEach((animation) => stopAnimation(animation));
+ };
+ case "forEachNative":
+ return (callback) => {
+ target.animations.forEach((animation) => callback(animation, target));
+ };
+ default:
+ return typeof (activeAnimation === null || activeAnimation === void 0 ? void 0 : activeAnimation[key]) === "undefined" ? void 0 : () => target.animations.forEach((animation) => animation[key]());
+ }
+ },
+ set: (target, key, value) => {
+ switch (key) {
+ case "currentTime":
+ value = time.ms(value);
+ case "playbackRate":
+ for (let i = 0; i < target.animations.length; i++) {
+ target.animations[i][key] = value;
+ }
+ return true;
+ }
+ return false;
+ }
+ };
+ var selectFinished = (animation) => animation.finished;
+ function resolveOption(option, i, total) {
+ return isFunction(option) ? option(i, total) : option;
+ }
+ function createAnimate(AnimatePolyfill) {
+ return function animate3(elements, keyframes, options = {}) {
+ elements = resolveElements(elements);
+ const numElements = elements.length;
+ invariant(Boolean(numElements), "No valid element provided.");
+ invariant(Boolean(keyframes), "No keyframes defined.");
+ const animationFactories = [];
+ for (let i = 0; i < numElements; i++) {
+ const element = elements[i];
+ for (const key in keyframes) {
+ const valueOptions = getOptions(options, key);
+ valueOptions.delay = resolveOption(valueOptions.delay, i, numElements);
+ const animation = animateStyle(element, key, keyframes[key], valueOptions, AnimatePolyfill);
+ animationFactories.push(animation);
+ }
+ }
+ return withControls(animationFactories, options, options.duration);
+ };
+ }
+ var animate = createAnimate(Animation);
+ function animateProgress(target, options = {}) {
+ return withControls([
+ () => {
+ const animation = new Animation(target, [0, 1], options);
+ animation.finished.catch(() => {
+ });
+ return animation;
+ }
+ ], options, options.duration);
+ }
+ function animate2(target, keyframesOrOptions, options) {
+ const factory = isFunction(target) ? animateProgress : animate;
+ return factory(target, keyframesOrOptions, options);
+ }
+ function isHidden(el) {
+ if (el === null) {
+ return true;
+ }
+ return el.offsetParent === null;
+ }
+ function isFlash(el) {
+ return el.dataset.component === "flash";
+ }
+ function flashCount() {
+ let num = 0;
+ if (!isHidden(document.getElementById("server-error"))) {
+ num += 1;
+ }
+ if (!isHidden(document.getElementById("client-error"))) {
+ num += 1;
+ }
+ if (!isHidden(document.getElementById("flash-info"))) {
+ num += 1;
+ }
+ if (!isHidden(document.getElementById("flash-error"))) {
+ num += 1;
+ }
+ return num;
+ }
+ var removalTime = 5;
+ var animationTime = 550;
+ var maxItemsIgnoresFlashes = true;
+ var gap = 15;
+ var lastTS = [];
+ function doAnimations(delayTime, maxItems, elToRemove) {
+ const ts = [];
+ let toasts = Array.from(document.querySelectorAll('#toast-group [phx-hook="LiveToast"]')).map((t) => {
+ if (isHidden(t)) {
+ return null;
+ } else {
+ return t;
+ }
+ }).filter(Boolean).reverse();
+ if (elToRemove) {
+ toasts = toasts.filter((t) => t !== elToRemove);
+ }
+ for (let i = 0; i < toasts.length; i++) {
+ const toast = toasts[i];
+ if (isHidden(toast)) {
+ continue;
+ }
+ toast.order = i;
+ ts[i] = toast;
+ }
+ for (let i = 0; i < ts.length; i++) {
+ const max = maxItemsIgnoresFlashes ? maxItems + flashCount() : maxItems;
+ const toast = ts[i];
+ let direction = "";
+ if (toast.dataset.corner === "bottom_left" || toast.dataset.corner === "bottom_right") {
+ direction = "-";
+ }
+ let val = 0;
+ for (let j = 0; j < toast.order; j++) {
+ val += ts[j].offsetHeight + gap;
+ }
+ const opacity = toast.order > max ? 0 : 1 - (toast.order - max + 1);
+ if (toast.order >= max) {
+ toast.classList.remove("pointer-events-auto");
+ } else {
+ toast.classList.add("pointer-events-auto");
+ }
+ const keyframes = { y: [`${direction}${val}px`], opacity: [opacity] };
+ if (toast.order === 0 && lastTS.includes(toast) === false) {
+ const val2 = toast.offsetHeight + gap;
+ const oppositeDirection = direction === "-" ? "" : "-";
+ keyframes.y.unshift(`${oppositeDirection}${val2}px`);
+ keyframes.opacity.unshift(0);
+ }
+ toast.targetDestination = `${direction}${val}px`;
+ const duration = animationTime / 1e3;
+ animate2(toast, keyframes, {
+ duration,
+ easing: [0.22, 1, 0.36, 1]
+ });
+ toast.order += 1;
+ toast.style.zIndex = (50 - toast.order).toString();
+ window.setTimeout(() => {
+ if (toast.order > max) {
+ this.pushEventTo("#toast-group", "clear", { id: toast.id });
+ }
+ }, delayTime + removalTime);
+ lastTS = ts;
+ }
+ }
+ async function animateOut() {
+ const val = (this.el.order - 2) * 100 + (this.el.order - 2) * gap;
+ let direction = "";
+ if (this.el.dataset.corner === "bottom_left" || this.el.dataset.corner === "bottom_right") {
+ direction = "-";
+ }
+ const animation = animate2(this.el, { y: `${direction}${val}%`, opacity: 0 }, {
+ opacity: {
+ duration: 0.2,
+ easing: "ease-out"
+ },
+ duration: 0.3,
+ easing: "ease-out"
+ });
+ await animation.finished;
+ }
+ function createLiveToastHook(duration = 6e3, maxItems = 3) {
+ return {
+ destroyed() {
+ doAnimations.bind(this)(duration, maxItems);
+ },
+ updated() {
+ const keyframes = { y: [this.el.targetDestination] };
+ animate2(this.el, keyframes, { duration: 0 });
+ },
+ mounted() {
+ if (["server-error", "client-error"].includes(this.el.id)) {
+ if (isHidden(document.getElementById(this.el.id))) {
+ return;
+ }
+ }
+ window.addEventListener("phx:clear-flash", (e) => {
+ this.pushEvent("lv:clear-flash", {
+ key: e.detail.key
+ });
+ });
+ window.addEventListener("flash-leave", async (event) => {
+ if (event.target === this.el) {
+ doAnimations.bind(this, duration, maxItems, this.el)();
+ await animateOut.bind(this)();
+ }
+ });
+ doAnimations.bind(this)(duration, maxItems);
+ if (isFlash(this.el)) {
+ return;
+ }
+ let durationOverride = duration;
+ if (this.el.dataset.duration !== void 0) {
+ durationOverride = Number.parseInt(this.el.dataset.duration);
+ }
+ window.setTimeout(async () => {
+ await animateOut.bind(this)();
+ this.pushEventTo("#toast-group", "clear", { id: this.el.id });
+ }, durationOverride + removalTime);
+ }
+ };
+ }
+
// js/hooks/index.js
var Hooks2 = {
+ LiveToast: createLiveToastHook(),
AppShell,
SidebarInteractions,
SettingsSectionScroll,
diff --git a/test/bds/ai_test.exs b/test/bds/ai_test.exs
index 4c2fa40..0a14c55 100644
--- a/test/bds/ai_test.exs
+++ b/test/bds/ai_test.exs
@@ -385,6 +385,28 @@ defmodule BDS.AITest do
assert Repo.get(Setting, "__encrypted_ai.online.api_key") == nil
end
+ test "put_endpoint clears fields on nil instead of crashing" do
+ assert {:ok, _endpoint} =
+ BDS.AI.put_endpoint(
+ :online,
+ %{url: "https://api.example.test/v1", api_key: "secret", model: "gpt-4o-mini"},
+ secret_backend: FakeSecretBackend
+ )
+
+ # A partial config (blank URL/model) must not raise — it clears those keys.
+ assert {:ok, endpoint} =
+ BDS.AI.put_endpoint(
+ :online,
+ %{url: nil, api_key: nil, model: nil},
+ secret_backend: FakeSecretBackend
+ )
+
+ assert endpoint.url == nil
+ assert Repo.get(Setting, "ai.online.url") == nil
+ assert Repo.get(Setting, "ai.online.model") == nil
+ assert Repo.get(Setting, "__encrypted_ai.online.api_key") == nil
+ end
+
test "airplane_endpoint_configured? reflects the airplane endpoint url and model" do
refute BDS.AI.airplane_endpoint_configured?()
diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs
index af126e0..1faea9e 100644
--- a/test/bds/desktop/shell_live_test.exs
+++ b/test/bds/desktop/shell_live_test.exs
@@ -2085,6 +2085,50 @@ defmodule BDS.Desktop.ShellLiveTest do
BDS.AI.Catalog.model_capabilities("llama3.3")
end
+ test "saving ai settings without editing preserves stored endpoints" do
+ assert {:ok, _online} =
+ AI.put_endpoint(:online, %{
+ url: "https://api.example.test/v1",
+ api_key: "keep-me",
+ model: "gpt-4.1"
+ })
+
+ assert {:ok, _airplane} =
+ AI.put_endpoint(:airplane, %{
+ url: "http://localhost:11434/v1",
+ api_key: nil,
+ model: "llama3.3"
+ })
+
+ {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
+
+ _html =
+ view
+ |> element("[data-testid='activity-button'][data-view='settings']")
+ |> render_click()
+
+ _html =
+ view
+ |> element("[data-testid='sidebar-open-item'][data-item-id='settings-project']")
+ |> render_click()
+
+ # Save with an untouched form (no change_settings_ai). The endpoints must
+ # survive instead of being wiped by an empty draft.
+ _html =
+ view
+ |> element("#settings-editor-shell button[phx-click='save_settings_ai']")
+ |> render_click()
+
+ assert {:ok, online} = AI.get_endpoint(:online)
+ assert online.url == "https://api.example.test/v1"
+ assert online.api_key == "keep-me"
+ assert online.model == "gpt-4.1"
+
+ assert {:ok, airplane} = AI.get_endpoint(:airplane)
+ assert airplane.url == "http://localhost:11434/v1"
+ assert airplane.model == "llama3.3"
+ end
+
test "ai settings refresh models from the configured endpoints" do
Application.put_env(:bds, :ai_http_client, FakeEndpointModelHttpClient)