fix: model selector works now

This commit is contained in:
2026-05-01 22:35:24 +02:00
parent d3aa7f2438
commit a5193240ad
11 changed files with 86 additions and 5 deletions

View File

@@ -169,6 +169,9 @@ defmodule BDS.AI do
@spec available_chat_models(String.t() | nil) :: [map()] @spec available_chat_models(String.t() | nil) :: [map()]
defdelegate available_chat_models(current_model \\ nil), to: Chat defdelegate available_chat_models(current_model \\ nil), to: Chat
@spec effective_chat_model(BDS.AI.ChatConversation.t() | map() | nil) :: String.t() | nil
defdelegate effective_chat_model(conversation), to: Chat
@spec set_conversation_model(String.t(), String.t()) :: @spec set_conversation_model(String.t(), String.t()) ::
{:ok, map()} | {:error, :not_found | Ecto.Changeset.t()} {:ok, map()} | {:error, :not_found | Ecto.Changeset.t()}
defdelegate set_conversation_model(conversation_id, model_id), to: Chat defdelegate set_conversation_model(conversation_id, model_id), to: Chat

View File

@@ -105,6 +105,15 @@ defmodule BDS.AI.Chat do
end) end)
end end
@spec effective_chat_model(ChatConversation.t() | map() | nil) :: String.t() | nil
def effective_chat_model(%ChatConversation{} = conversation) do
resolve_effective_chat_model(conversation.model)
end
def effective_chat_model(%{model: model}), do: resolve_effective_chat_model(model)
def effective_chat_model(%{"model" => model}), do: resolve_effective_chat_model(model)
def effective_chat_model(_conversation), do: resolve_effective_chat_model(nil)
@spec set_conversation_model(String.t(), String.t()) :: @spec set_conversation_model(String.t(), String.t()) ::
{:ok, map()} | {:error, :not_found | Ecto.Changeset.t()} {:ok, map()} | {:error, :not_found | Ecto.Changeset.t()}
def set_conversation_model(conversation_id, model_id) def set_conversation_model(conversation_id, model_id)
@@ -282,6 +291,25 @@ defmodule BDS.AI.Chat do
end end
end end
defp resolve_effective_chat_model(model) when is_binary(model) and model != "", do: model
defp resolve_effective_chat_model(_model) do
mode = if AI.airplane_mode?(), do: :airplane, else: :online
preference_key = if mode == :airplane, do: :airplane_chat, else: :chat
case Runtime.model_preference_value(preference_key) do
model when is_binary(model) and model != "" ->
model
_other ->
case AI.get_endpoint(mode) do
{:ok, %{model: model}} when is_binary(model) and model != "" -> model
_other -> nil
end
end
end
defp catalog_provider_name_map do defp catalog_provider_name_map do
Repo.all(from provider in CatalogProvider, select: {provider.id, provider.name}) Repo.all(from provider in CatalogProvider, select: {provider.id, provider.name})
|> Map.new() |> Map.new()

View File

@@ -15,12 +15,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
%ChatConversation{} = conversation -> %ChatConversation{} = conversation ->
messages = AI.list_chat_messages(conversation.id) messages = AI.list_chat_messages(conversation.id)
request = Map.get(assigns.chat_editor_requests, conversation.id) request = Map.get(assigns.chat_editor_requests, conversation.id)
available_models = AI.available_chat_models(conversation.model) effective_model = AI.effective_chat_model(conversation)
available_models = AI.available_chat_models(effective_model)
%{ %{
id: conversation.id, id: conversation.id,
title: conversation.title || translated("chat.newChat"), title: conversation.title || translated("chat.newChat"),
model: conversation.model, model: conversation.model,
effective_model: effective_model,
available_models: available_models, available_models: available_models,
available_model_groups: ModelSelection.group_available_models(available_models), available_model_groups: ModelSelection.group_available_models(available_models),
model_selector_open?: model_selector_open?:

View File

@@ -17,7 +17,7 @@
phx-click="toggle_chat_model_selector" phx-click="toggle_chat_model_selector"
data-testid="chat-model-selector-button" data-testid="chat-model-selector-button"
> >
<span><%= @chat_editor.model || translated("chat.newChat") %></span> <span><%= @chat_editor.effective_model || translated("chat.modelUnavailable") %></span>
<span class="chat-model-selector-caret">▾</span> <span class="chat-model-selector-caret">▾</span>
</button> </button>
@@ -33,7 +33,7 @@
<button <button
class={[ class={[
"chat-model-selector-option", "chat-model-selector-option",
if(model.id == @chat_editor.model, do: "active") if(model.id == @chat_editor.effective_model, do: "active")
]} ]}
type="button" type="button"
phx-click="select_chat_model" phx-click="select_chat_model"

View File

@@ -107,6 +107,7 @@
"chat.welcomeTipTabs": "Beitragsstatistiken pro Jahr in Tabs mit Diagrammen", "chat.welcomeTipTabs": "Beitragsstatistiken pro Jahr in Tabs mit Diagrammen",
"chat.role.you": "Du", "chat.role.you": "Du",
"chat.role.assistant": "Assistent", "chat.role.assistant": "Assistent",
"chat.modelUnavailable": "Kein Modell",
"chat.inputPlaceholder": "Nachricht eingeben...", "chat.inputPlaceholder": "Nachricht eingeben...",
"chat.stop": "Stopp", "chat.stop": "Stopp",
"chat.toolArguments": "Argumente", "chat.toolArguments": "Argumente",

View File

@@ -107,6 +107,7 @@
"chat.welcomeTipTabs": "Show post statistics by year in tabs with charts", "chat.welcomeTipTabs": "Show post statistics by year in tabs with charts",
"chat.role.you": "You", "chat.role.you": "You",
"chat.role.assistant": "Assistant", "chat.role.assistant": "Assistant",
"chat.modelUnavailable": "No model",
"chat.inputPlaceholder": "Type a message...", "chat.inputPlaceholder": "Type a message...",
"chat.stop": "Stop", "chat.stop": "Stop",
"chat.toolArguments": "Arguments", "chat.toolArguments": "Arguments",

View File

@@ -107,6 +107,7 @@
"chat.welcomeTipTabs": "Muestre estadísticas por año en pestañas con gráficos", "chat.welcomeTipTabs": "Muestre estadísticas por año en pestañas con gráficos",
"chat.role.you": "Tú", "chat.role.you": "Tú",
"chat.role.assistant": "Asistente", "chat.role.assistant": "Asistente",
"chat.modelUnavailable": "Sin modelo",
"chat.inputPlaceholder": "Escribe un mensaje...", "chat.inputPlaceholder": "Escribe un mensaje...",
"chat.stop": "Detener", "chat.stop": "Detener",
"chat.toolArguments": "Argumentos", "chat.toolArguments": "Argumentos",

View File

@@ -107,6 +107,7 @@
"chat.welcomeTipTabs": "Afficher les statistiques par année dans des onglets avec graphiques", "chat.welcomeTipTabs": "Afficher les statistiques par année dans des onglets avec graphiques",
"chat.role.you": "Vous", "chat.role.you": "Vous",
"chat.role.assistant": "Assistant IA", "chat.role.assistant": "Assistant IA",
"chat.modelUnavailable": "Aucun modèle",
"chat.inputPlaceholder": "Saisissez un message...", "chat.inputPlaceholder": "Saisissez un message...",
"chat.stop": "Arrêter", "chat.stop": "Arrêter",
"chat.toolArguments": "Arguments", "chat.toolArguments": "Arguments",

View File

@@ -107,6 +107,7 @@
"chat.welcomeTipTabs": "Mostrare statistiche per anno in schede con grafici", "chat.welcomeTipTabs": "Mostrare statistiche per anno in schede con grafici",
"chat.role.you": "Tu", "chat.role.you": "Tu",
"chat.role.assistant": "Assistente", "chat.role.assistant": "Assistente",
"chat.modelUnavailable": "Nessun modello",
"chat.inputPlaceholder": "Scrivi un messaggio...", "chat.inputPlaceholder": "Scrivi un messaggio...",
"chat.stop": "Ferma", "chat.stop": "Ferma",
"chat.toolArguments": "Argomenti", "chat.toolArguments": "Argomenti",

View File

@@ -3565,7 +3565,7 @@ button svg * {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
overflow: hidden; overflow: visible;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--vscode-foreground, inherit); color: var(--vscode-foreground, inherit);
@@ -5145,7 +5145,7 @@ button svg * {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
overflow: hidden; overflow: visible;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--vscode-foreground, inherit); color: var(--vscode-foreground, inherit);

View File

@@ -2160,6 +2160,49 @@ defmodule BDS.Desktop.ShellLiveTest do
assert css =~ "position: static;" assert css =~ "position: static;"
end end
test "chat editor model selector uses effective model for new chats and persists selection" do
assert :ok = AI.set_airplane_mode(true)
assert {:ok, _endpoint} =
AI.put_endpoint(:airplane, %{
url: "http://localhost:11434/v1",
api_key: nil,
model: "llama-default"
})
assert :ok = AI.put_model_preference(:airplane_chat, "llama-current")
assert {:ok, conversation} = AI.start_chat(%{title: "New Chat"})
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
render_click(view, "pin_sidebar_item", %{
"route" => "chat",
"id" => conversation.id,
"title" => conversation.title,
"subtitle" => "chat"
})
assert html =~ ~s(data-testid="chat-model-selector-button")
assert html =~ "llama-current"
refute html =~ ~s(<span>New Chat</span><span class="chat-model-selector-caret">▾</span>)
selector_html = render_click(view, "toggle_chat_model_selector", %{})
assert selector_html =~ ~s(class="chat-model-selector-menu")
assert selector_html =~ ~s(data-testid="chat-model-selector-option")
assert selector_html =~ "llama-current"
css = File.read!(Path.expand("../../../priv/ui/app.css", __DIR__))
assert css =~ ".chat-panel-title {"
assert css =~ "overflow: visible;"
refute css =~ ".chat-panel-title {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n overflow: hidden;"
render_click(view, "select_chat_model", %{"model" => "llama-next"})
assert AI.get_chat_conversation(conversation.id).model == "llama-next"
assert render(view) =~ "llama-next"
end
test "chat editor renders legacy model controls, collapsed tool pills, and dismissible A2UI surfaces" do test "chat editor renders legacy model controls, collapsed tool pills, and dismissible A2UI surfaces" do
assert {:ok, conversation} = AI.start_chat(%{title: "Editor Chat", model: "gpt-4.1"}) assert {:ok, conversation} = AI.start_chat(%{title: "Editor Chat", model: "gpt-4.1"})