fix: fixed behaviour of bundled app for decrypt and ai chat
This commit is contained in:
@@ -602,14 +602,16 @@
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
opacity: 0.4;
|
||||
font-size: 13px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.status-bar-item.offline-badge.active {
|
||||
background-color: rgba(255, 196, 0, 0.28);
|
||||
background-color: #e6a800;
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
opacity: 1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.project-selector {
|
||||
|
||||
@@ -3,6 +3,9 @@ export const ChatSurface = {
|
||||
this.stickToBottom = true;
|
||||
this.scrollContainer = null;
|
||||
|
||||
this._enterKeyHandled = false;
|
||||
this._prevInputValue = "";
|
||||
|
||||
this.autoResize = () => {
|
||||
const textarea = this.el.querySelector(".chat-input");
|
||||
|
||||
@@ -85,11 +88,34 @@ export const ChatSurface = {
|
||||
this.stickToBottom = distanceFromBottom < 48;
|
||||
};
|
||||
|
||||
this._submitChat = () => {
|
||||
const form = this.el.querySelector(".chat-input-wrapper");
|
||||
if (form && typeof form.requestSubmit === "function") {
|
||||
form.requestSubmit();
|
||||
} else {
|
||||
const sendButton = this.el.querySelector("[data-testid='chat-send-button']");
|
||||
if (sendButton) sendButton.click();
|
||||
}
|
||||
};
|
||||
|
||||
this.handleInput = (event) => {
|
||||
if (!event.target.closest(".chat-input")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = event.target;
|
||||
|
||||
if (!this._enterKeyHandled && textarea.value.includes("\n") && !this._prevInputValue.includes("\n")) {
|
||||
textarea.value = textarea.value.replace(/\n/g, "");
|
||||
this._prevInputValue = textarea.value;
|
||||
this.stickToBottom = true;
|
||||
this.autoResize();
|
||||
this._submitChat();
|
||||
return;
|
||||
}
|
||||
|
||||
this._enterKeyHandled = false;
|
||||
this._prevInputValue = textarea.value;
|
||||
this.stickToBottom = true;
|
||||
this.autoResize();
|
||||
};
|
||||
@@ -101,12 +127,8 @@ export const ChatSurface = {
|
||||
|
||||
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
||||
event.preventDefault();
|
||||
|
||||
const sendButton = this.el.querySelector("[data-testid='chat-send-button']");
|
||||
|
||||
if (sendButton && !sendButton.disabled) {
|
||||
sendButton.click();
|
||||
}
|
||||
this._enterKeyHandled = true;
|
||||
this._submitChat();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ config :bds,
|
||||
ecto_repos: [BDS.Repo]
|
||||
|
||||
config :bds, BDS.Repo,
|
||||
database: Path.expand("../priv/data/bds_dev.db", __DIR__),
|
||||
database:
|
||||
System.get_env("BDS_DATABASE_PATH") ||
|
||||
Path.expand("~/Library/Application Support/BDS2/bds_dev.db"),
|
||||
pool_size: 5,
|
||||
journal_mode: :wal,
|
||||
busy_timeout: 15_000,
|
||||
@@ -21,6 +23,9 @@ config :bds, :desktop,
|
||||
title: "Blogging Desktop Server",
|
||||
secret_key_base: "bds_desktop_shell_secret_key_base_64_chars_minimum_seed_value_001"
|
||||
|
||||
config :bds, :ai_secret_key,
|
||||
"bds_desktop_shell_secret_key_base_64_chars_minimum_seed_value_001"
|
||||
|
||||
config :bds, BDS.Desktop.Endpoint,
|
||||
url: [host: "127.0.0.1"],
|
||||
adapter: Bandit.PhoenixAdapter,
|
||||
@@ -80,7 +85,9 @@ config :bds, :embeddings,
|
||||
|
||||
# Cache downloaded model files under the app data directory so they persist
|
||||
# across sessions (ModelCaching invariant). Overridden at runtime in prod.
|
||||
config :bumblebee, :cache_dir, Path.expand("../priv/data/models", __DIR__)
|
||||
config :bumblebee, :cache_dir,
|
||||
System.get_env("BDS_MODEL_CACHE_DIR") ||
|
||||
Path.expand("~/Library/Application Support/BDS2/models")
|
||||
|
||||
config :logger, :console,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Config
|
||||
|
||||
config :bds, BDS.Repo,
|
||||
database: Path.expand("../priv/data/bds_prod.db", __DIR__),
|
||||
pool_size: 5,
|
||||
stacktrace: false,
|
||||
show_sensitive_data_on_connection_error: false
|
||||
|
||||
@@ -3,7 +3,9 @@ import Config
|
||||
if config_env() == :prod do
|
||||
database_path =
|
||||
System.get_env("BDS_DATABASE_PATH") ||
|
||||
Path.expand("../priv/data/bds_prod.db", __DIR__)
|
||||
Path.expand("~/Library/Application Support/BDS2/bds.db")
|
||||
|
||||
File.mkdir_p!(Path.dirname(database_path))
|
||||
|
||||
config :bds, BDS.Repo,
|
||||
database: database_path,
|
||||
|
||||
@@ -190,7 +190,7 @@ defmodule BDS.Desktop.MainWindow do
|
||||
end
|
||||
|
||||
defp config_dir do
|
||||
case :filename.basedir(:user_config, "bds") do
|
||||
case :filename.basedir(:user_config, "BDS2") do
|
||||
path when is_list(path) -> List.to_string(path)
|
||||
path -> path
|
||||
end
|
||||
|
||||
@@ -398,15 +398,6 @@ defmodule BDS.Desktop.ShellLive do
|
||||
def handle_event("overlay_confirm", params, socket),
|
||||
do: OverlayManager.handle_event("overlay_confirm", params, socket, overlay_callbacks())
|
||||
|
||||
def handle_event("overlay_select_gallery_image", params, socket),
|
||||
do:
|
||||
OverlayManager.handle_event(
|
||||
"overlay_select_gallery_image",
|
||||
params,
|
||||
socket,
|
||||
overlay_callbacks()
|
||||
)
|
||||
|
||||
def handle_event("overlay_close_lightbox", params, socket),
|
||||
do: OverlayManager.handle_event("overlay_close_lightbox", params, socket, overlay_callbacks())
|
||||
|
||||
|
||||
@@ -90,10 +90,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
end
|
||||
|
||||
def handle_event("send_chat_editor_message", _params, socket) do
|
||||
Logger.info("CHAT send_chat_editor_message called, input=#{inspect(socket.assigns.input)}")
|
||||
{:noreply, do_send_message(socket)}
|
||||
end
|
||||
|
||||
def handle_event("abort_chat_editor_message", _params, socket) do
|
||||
Logger.info("CHAT abort_chat_editor_message called")
|
||||
{:noreply, do_abort_message(socket)}
|
||||
end
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
|
||||
<form class="chat-input-wrapper flex items-end gap-2" phx-change="change_chat_editor_input" phx-submit="send_chat_editor_message" phx-target={@myself}>
|
||||
<textarea class="chat-input chat-surface-input ui-textarea" name="message" rows="1" placeholder={dgettext("ui", "Type a message...")} disabled={@chat_editor.is_streaming}><%= @chat_editor.input %></textarea>
|
||||
<button class="chat-send-button ui-button ui-button-primary" data-testid="chat-send-button" type="button" phx-click="send_chat_editor_message" phx-target={@myself} disabled={@chat_editor.send_disabled?}>↑</button>
|
||||
<button class="chat-send-button ui-button ui-button-primary" data-testid="chat-send-button" type="button" phx-click="send_chat_editor_message" phx-target={@myself}>↑</button>
|
||||
</form>
|
||||
|
||||
<%= if @chat_editor.action_error do %>
|
||||
|
||||
@@ -9,8 +9,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
|
||||
@spec ai_form(term()) :: term()
|
||||
def ai_form(assigns) do
|
||||
{:ok, online_endpoint} = AI.get_endpoint(:online)
|
||||
{:ok, airplane_endpoint} = AI.get_endpoint(:airplane)
|
||||
online_endpoint = safe_endpoint(:online)
|
||||
airplane_endpoint = safe_endpoint(:airplane)
|
||||
|
||||
%{
|
||||
"online_url" => Map.get(online_endpoint || %{}, :url, ""),
|
||||
@@ -168,6 +168,13 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
end
|
||||
end
|
||||
|
||||
defp safe_endpoint(kind) do
|
||||
case AI.get_endpoint(kind) do
|
||||
{:ok, ep} -> ep
|
||||
_error -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp ai_attrs(assigns) do
|
||||
draft = Map.get(assigns, :settings_editor_ai_draft, %{})
|
||||
|
||||
|
||||
@@ -76,23 +76,48 @@ defmodule BDS.MacBundle.Dylibs do
|
||||
File.mkdir_p!(frameworks_dir)
|
||||
|
||||
with {:ok, {_seen, externals}} <- collect(nif_path, MapSet.new(), []) do
|
||||
copied =
|
||||
externals = Enum.reverse(externals)
|
||||
|
||||
{physical, logical_entries, _inode_map} =
|
||||
externals
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(fn src ->
|
||||
|> Enum.reduce({[], [], %{}}, fn src, {phys_acc, log_acc, inode_map} ->
|
||||
dest = Path.join(frameworks_dir, Path.basename(src))
|
||||
|
||||
if File.exists?(dest) do
|
||||
{phys_acc, [{src, dest} | log_acc], inode_map}
|
||||
else
|
||||
ino = file_inode(src)
|
||||
|
||||
case Map.get(inode_map, ino) do
|
||||
nil ->
|
||||
File.cp!(src, dest)
|
||||
File.chmod!(dest, 0o644)
|
||||
{src, dest}
|
||||
{[{src, dest} | phys_acc], [{src, dest} | log_acc],
|
||||
Map.put(inode_map, ino, dest)}
|
||||
|
||||
existing_dest ->
|
||||
# Same inode already copied under a different name.
|
||||
# Point this logical entry to the physical copy.
|
||||
{phys_acc, [{src, existing_dest} | log_acc], inode_map}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
copied = Enum.reverse(logical_entries)
|
||||
physical = Enum.reverse(physical)
|
||||
|
||||
with :ok <- rewrite(nif_path, copied, nif_loader_prefix),
|
||||
:ok <- rewrite_each(copied) do
|
||||
{:ok, Enum.map(copied, &elem(&1, 1))}
|
||||
:ok <- rewrite_each(physical) do
|
||||
{:ok, Enum.map(physical, &elem(&1, 1))}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp file_inode(path) do
|
||||
stat = File.stat!(path)
|
||||
{stat.major_device, stat.inode}
|
||||
end
|
||||
|
||||
# Depth-first transitive collection of external dependency paths. Returns
|
||||
# `{:ok, {seen, acc}}` where `acc` is the reverse-discovery-order path list.
|
||||
defp collect(binary, seen, acc) do
|
||||
@@ -100,16 +125,17 @@ defmodule BDS.MacBundle.Dylibs do
|
||||
{:ok, deps} ->
|
||||
deps
|
||||
|> Enum.filter(&external?/1)
|
||||
|> Enum.reject(&MapSet.member?(seen, &1))
|
||||
|> Enum.reduce_while({:ok, {seen, acc}}, fn dep, {:ok, {seen_acc, list_acc}} ->
|
||||
if MapSet.member?(seen_acc, dep) do
|
||||
{:cont, {:ok, {seen_acc, list_acc}}}
|
||||
else
|
||||
seen_acc = MapSet.put(seen_acc, dep)
|
||||
|
||||
case collect(dep, seen_acc, [dep | list_acc]) do
|
||||
{:ok, _} = ok -> {:cont, ok}
|
||||
error -> {:halt, error}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
|
||||
@@ -329,7 +329,7 @@ defmodule BDS.Projects do
|
||||
end
|
||||
|
||||
defp private_app_dir do
|
||||
case :filename.basedir(:user_config, "bds") do
|
||||
case :filename.basedir(:user_config, "BDS2") do
|
||||
path when is_list(path) -> List.to_string(path)
|
||||
path -> path
|
||||
end
|
||||
|
||||
@@ -1076,13 +1076,15 @@ button svg, button svg * {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
opacity: 0.4;
|
||||
font-size: 13px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.status-bar-item.offline-badge.active {
|
||||
background-color: rgba(255, 196, 0, 0.28);
|
||||
background-color: #e6a800;
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
opacity: 1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.project-selector {
|
||||
position: relative;
|
||||
|
||||
@@ -9143,6 +9143,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
|
||||
mounted() {
|
||||
this.stickToBottom = true;
|
||||
this.scrollContainer = null;
|
||||
this._enterKeyHandled = false;
|
||||
this._prevInputValue = "";
|
||||
this.autoResize = () => {
|
||||
const textarea = this.el.querySelector(".chat-input");
|
||||
if (!textarea) {
|
||||
@@ -9202,10 +9204,30 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
|
||||
const distanceFromBottom = this.scrollContainer.scrollHeight - this.scrollContainer.scrollTop - this.scrollContainer.clientHeight;
|
||||
this.stickToBottom = distanceFromBottom < 48;
|
||||
};
|
||||
this._submitChat = () => {
|
||||
const form = this.el.querySelector(".chat-input-wrapper");
|
||||
if (form && typeof form.requestSubmit === "function") {
|
||||
form.requestSubmit();
|
||||
} else {
|
||||
const sendButton = this.el.querySelector("[data-testid='chat-send-button']");
|
||||
if (sendButton) sendButton.click();
|
||||
}
|
||||
};
|
||||
this.handleInput = (event) => {
|
||||
if (!event.target.closest(".chat-input")) {
|
||||
return;
|
||||
}
|
||||
const textarea = event.target;
|
||||
if (!this._enterKeyHandled && textarea.value.includes("\n") && !this._prevInputValue.includes("\n")) {
|
||||
textarea.value = textarea.value.replace(/\n/g, "");
|
||||
this._prevInputValue = textarea.value;
|
||||
this.stickToBottom = true;
|
||||
this.autoResize();
|
||||
this._submitChat();
|
||||
return;
|
||||
}
|
||||
this._enterKeyHandled = false;
|
||||
this._prevInputValue = textarea.value;
|
||||
this.stickToBottom = true;
|
||||
this.autoResize();
|
||||
};
|
||||
@@ -9215,10 +9237,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
|
||||
}
|
||||
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
||||
event.preventDefault();
|
||||
const sendButton = this.el.querySelector("[data-testid='chat-send-button']");
|
||||
if (sendButton && !sendButton.disabled) {
|
||||
sendButton.click();
|
||||
}
|
||||
this._enterKeyHandled = true;
|
||||
this._submitChat();
|
||||
}
|
||||
};
|
||||
this.el.addEventListener("input", this.handleInput);
|
||||
|
||||
@@ -2,6 +2,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.SettingsSearchTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias BDS.Desktop.ShellLive.SettingsEditor
|
||||
alias BDS.Repo
|
||||
alias BDS.Settings.Setting
|
||||
|
||||
describe "build_settings/1 — search filter" do
|
||||
setup do
|
||||
@@ -111,4 +113,62 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.SettingsSearchTest do
|
||||
assert SettingsEditor.build_settings(%{projects: %{active_project_id: nil}}) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "build_settings/1 — handles undecryptable API key gracefully" do
|
||||
setup do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||
temp_dir =
|
||||
Path.join(System.tmp_dir!(), "bds-settings-cipher-#{System.unique_integer([:positive])}")
|
||||
|
||||
File.mkdir_p!(temp_dir)
|
||||
|
||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||
|
||||
{:ok, project} =
|
||||
BDS.Projects.create_project(%{name: "CipherTest", data_path: temp_dir})
|
||||
|
||||
now = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
|
||||
|
||||
Repo.insert!(%Setting{
|
||||
key: "ai.online.url",
|
||||
value: "https://example.com",
|
||||
updated_at: now
|
||||
})
|
||||
|
||||
Repo.insert!(%Setting{
|
||||
key: "ai.online.model",
|
||||
value: "gpt-4",
|
||||
updated_at: now
|
||||
})
|
||||
|
||||
Repo.insert!(%Setting{
|
||||
key: "__encrypted_ai.online.api_key",
|
||||
value: "NOT_VALID_BASE64",
|
||||
updated_at: now
|
||||
})
|
||||
|
||||
%{project: project, temp_dir: temp_dir}
|
||||
end
|
||||
|
||||
test "does not crash when encrypted API key cannot be decrypted", %{
|
||||
project: project,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
result =
|
||||
SettingsEditor.build_settings(%{
|
||||
projects: %{active_project_id: project.id},
|
||||
current_project: %{data_path: temp_dir},
|
||||
settings_editor_search: "",
|
||||
settings_editor_project_draft: %{},
|
||||
settings_editor_editor_draft: %{},
|
||||
settings_editor_ai_draft: %{},
|
||||
settings_editor_publishing_draft: %{},
|
||||
current_tab: %{type: :settings, id: "settings"},
|
||||
tab_meta: %{}
|
||||
})
|
||||
|
||||
assert is_map(result)
|
||||
assert result.ai_visible?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -119,7 +119,7 @@ defmodule BDS.ProjectsTest do
|
||||
|
||||
test "project_cache_dir never falls back into the project data directory" do
|
||||
# Private app-internal artifacts (the embeddings index) must live under the
|
||||
# OS private app directory (macOS: ~/Library/Application Support/bds), never
|
||||
# OS private app directory (macOS: ~/Library/Application Support/BDS2), never
|
||||
# inside priv/data/projects/<id> — leaving them in the project tree pollutes
|
||||
# the repository.
|
||||
saved = Application.get_env(:bds, :project_cache_root)
|
||||
@@ -134,7 +134,7 @@ defmodule BDS.ProjectsTest do
|
||||
refute String.starts_with?(cache_dir, Path.expand("../../priv/data", __DIR__))
|
||||
|
||||
private_app_dir =
|
||||
case :filename.basedir(:user_config, "bds") do
|
||||
case :filename.basedir(:user_config, "BDS2") do
|
||||
path when is_list(path) -> List.to_string(path)
|
||||
path -> path
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user