fix: fix airplane mode for AI usage and qwen 3.6 one-shot parsing

This commit is contained in:
2026-06-11 22:28:44 +02:00
parent d8b24c9b72
commit 8546080a3d
12 changed files with 269 additions and 24 deletions

View File

@@ -111,6 +111,19 @@ defmodule BDS.AI do
end
end
@doc """
True when the airplane (local) endpoint has both a URL and a model
configured, so gated AI features can run against the local model.
"""
@spec airplane_endpoint_configured?() :: boolean()
def airplane_endpoint_configured? do
present_setting?(get_setting("ai.airplane.url")) and
present_setting?(get_setting("ai.airplane.model"))
end
defp present_setting?(value) when is_binary(value), do: String.trim(value) != ""
defp present_setting?(_value), do: false
@spec put_model_preference(atom(), String.t()) ::
:ok | {:error, :unknown_model_preference | term()}
def put_model_preference(key, model) when is_atom(key) and is_binary(model) do

View File

@@ -4,6 +4,7 @@ defmodule BDS.AI.OneShot do
require Logger
alias BDS.AI.Chat
alias BDS.AI.JsonContent
alias BDS.AI.OpenAICompatibleRuntime
alias BDS.AI.Runtime
alias BDS.Media.Media
@@ -213,7 +214,9 @@ defmodule BDS.AI.OneShot do
messages: [
%{
"role" => "system",
"content" => one_shot_system_prompt(operation, language, source_language)
"content" =>
one_shot_system_prompt(operation, language, source_language) <>
" Output raw JSON only, without markdown code fences."
},
%{
"role" => "user",
@@ -351,11 +354,11 @@ defmodule BDS.AI.OneShot do
defp extract_json_response(%{json: json}) when is_map(json), do: {:ok, json}
defp extract_json_response(%{content: content}) when is_binary(content) do
case Jason.decode(content) do
{:ok, json} when is_map(json) ->
case JsonContent.decode(content) do
json when is_map(json) ->
{:ok, json}
_other ->
nil ->
Logger.error(
"AI extract_json_response failed to parse content as JSON. Content: #{String.slice(content, 0, 1000)}"
)

View File

@@ -182,14 +182,7 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
end
end
defp decode_json_content(nil), do: nil
defp decode_json_content(content) when is_binary(content) do
case Jason.decode(content) do
{:ok, decoded} when is_map(decoded) -> decoded
_other -> nil
end
end
defp decode_json_content(content), do: BDS.AI.JsonContent.decode(content)
defp completions_url(url) do
cond do

View File

@@ -414,7 +414,7 @@ defmodule BDS.Desktop.ShellLive do
do: OverlayManager.handle_event("overlay_lightbox_next", params, socket, overlay_callbacks())
def handle_event("add_gallery_images", %{"post-id" => post_id}, socket) do
if socket.assigns.offline_mode do
if socket.assigns.offline_mode and not AI.airplane_endpoint_configured?() do
{:noreply,
append_output_entry(
socket,

View File

@@ -230,7 +230,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
not is_nil(socket.assigns.request) ->
build_data(socket)
socket.assigns.offline_mode ->
socket.assigns.offline_mode and not AI.airplane_endpoint_configured?() ->
Notify.output(
dgettext("ui", "Chat"),
dgettext("ui", "Automatic AI actions stay gated by airplane mode."),
@@ -239,7 +239,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
build_data(socket)
ModelSelection.needs_api_key?(false) ->
ModelSelection.needs_api_key?(socket.assigns.offline_mode) ->
build_data(socket)
true ->

View File

@@ -434,7 +434,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
socket =
with %{} = definition <- ImportDefinitions.get_definition(definition_id),
%{} = report <- ImportDefinitions.decode_analysis_result(definition) do
if socket.assigns.offline_mode? do
if socket.assigns.offline_mode? and not AI.airplane_endpoint_configured?() do
notify_output(
dgettext("ui", "Import"),
BDS.Gettext.lgettext(

View File

@@ -82,7 +82,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
%{} = definition <- ImportDefinitions.get_definition(definition_id),
%{} = report <- ImportDefinitions.decode_analysis_result(definition) do
cond do
socket.assigns.offline_mode ->
socket.assigns.offline_mode and not AI.airplane_endpoint_configured?() ->
socket
|> append_output.(
dgettext("ui", "Import"),

View File

@@ -153,7 +153,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
def handle_event("detect_media_editor_language", _params, socket) do
if socket.assigns.offline_mode do
if socket.assigns.offline_mode and not AI.airplane_endpoint_configured?() do
notify_output(
socket,
dgettext("ui", "Detect Language"),
@@ -346,7 +346,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
def handle_event("refresh_media_translation", %{"language" => language}, socket) do
media = socket.assigns.media
if socket.assigns.offline_mode do
if socket.assigns.offline_mode and not AI.airplane_endpoint_configured?() do
notify_output(
socket,
dgettext("ui", "Translate"),
@@ -539,7 +539,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
defp do_translate(socket, language) do
if socket.assigns.offline_mode do
if socket.assigns.offline_mode and not AI.airplane_endpoint_configured?() do
notify_output(
socket,
dgettext("ui", "Translate"),

View File

@@ -66,7 +66,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
socket =
if kind == "ai_suggestions" and not is_nil(overlay) do
if socket.assigns.offline_mode do
if socket.assigns.offline_mode and not AI.airplane_endpoint_configured?() do
callbacks.append_output.(
socket,
dgettext("ui", "AI Suggestions"),

View File

@@ -707,7 +707,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
defp do_detect_language(socket) do
if Map.get(socket.assigns, :offline_mode, true) do
if Map.get(socket.assigns, :offline_mode, true) and not AI.airplane_endpoint_configured?() do
notify_output(
socket,
dgettext("ui", "Detect Language"),
@@ -756,7 +756,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
defp do_translate(socket, language) do
if Map.get(socket.assigns, :offline_mode, true) do
if Map.get(socket.assigns, :offline_mode, true) and not AI.airplane_endpoint_configured?() do
notify_output(
socket,
dgettext("ui", "Translate"),