feat: preliminary work on assistant

This commit is contained in:
2026-04-26 09:48:53 +02:00
parent 50d8e88ce8
commit 8e8a2e2cd2
10 changed files with 313 additions and 13 deletions

View File

@@ -47,6 +47,8 @@ defmodule BDS.Desktop.ShellLive do
|> assign(:page_language, ShellData.ui_language())
|> assign(:client_shortcuts, Commands.client_shortcuts())
|> assign(:offline_mode, true)
|> assign(:assistant_prompt, "")
|> assign(:assistant_messages, [])
|> assign(:is_mac_ui, mac_ui?())
|> assign(:menu_groups, titlebar_menu_groups())
|> assign(:titlebar_menu_group, nil)
@@ -275,6 +277,25 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, reload_shell(socket, socket.assigns.workbench)}
end
def handle_event("update_assistant_prompt", %{"assistant" => %{"prompt" => prompt}}, socket) do
{:noreply, assign(socket, :assistant_prompt, prompt)}
end
def handle_event("submit_assistant_prompt", %{"assistant" => %{"prompt" => prompt}}, socket) do
prompt = prompt |> to_string() |> String.trim()
socket =
if prompt == "" do
assign(socket, :assistant_prompt, "")
else
socket
|> assign(:assistant_prompt, "")
|> assign(:assistant_messages, socket.assigns.assistant_messages ++ assistant_turn(prompt, socket))
end
{:noreply, socket}
end
def handle_event("open_tasks_panel", _params, socket) do
workbench =
socket.assigns.workbench
@@ -1506,6 +1527,34 @@ defmodule BDS.Desktop.ShellLive do
defp tab_icon_id(%{type: :style}), do: "settings"
defp tab_icon_id(%{type: type}), do: Atom.to_string(type)
defp assistant_turn(prompt, socket) do
[
%{role: "user", content: prompt},
%{role: "assistant", content: assistant_reply(socket)}
]
end
defp assistant_reply(socket) do
if socket.assigns.offline_mode do
ShellData.translate("Automatic AI actions stay gated by airplane mode.", %{}, socket.assigns.page_language)
else
ShellData.translate(
"The assistant sidebar chat surface is ready, but model execution is not connected yet.",
%{},
socket.assigns.page_language
)
end
end
defp assistant_project_name(nil), do: translated("Projects")
defp assistant_project_name(project), do: project.name
defp assistant_message_label("assistant"), do: translated("Assistant")
defp assistant_message_label("user"), do: translated("You")
defp assistant_message_label(_role), do: translated("Assistant")
defp assistant_message_testid(role), do: "assistant-message-#{role}"
defp media_thumbnail_glyph(mime_type) do
case String.split(to_string(mime_type || ""), "/", parts: 2) do
["image", _rest] -> "IMG"

View File

@@ -416,15 +416,77 @@
>
<div class="resizable-panel-divider assistant-divider" data-resize="assistant" data-role="resize-handle"></div>
<aside class="assistant-sidebar" data-region="assistant-sidebar">
<div class="assistant-header">
<strong><%= translated("Assistant") %></strong>
</div>
<div class="assistant-content">
<%= for card <- @assistant_cards do %>
<section class="assistant-card">
<strong><%= translated(card.label) %></strong>
<span><%= translated(card.text) %></span>
</section>
<header class="assistant-sidebar-header">
<div class="assistant-sidebar-heading">
<strong><%= translated("AI Assistant") %></strong>
<span class="assistant-sidebar-description"><%= translated("AI conversations") %></span>
</div>
<span class={[
"assistant-sidebar-status",
if(@offline_mode, do: "is-offline", else: "is-online")
]}>
<%= if @offline_mode, do: translated("Offline"), else: translated("Chat") %>
</span>
</header>
<section class="assistant-sidebar-context" data-testid="assistant-context">
<div class="assistant-sidebar-context-row">
<span class="assistant-sidebar-context-label"><%= translated("Project") %></span>
<span class="assistant-sidebar-context-value"><%= assistant_project_name(@current_project) %></span>
</div>
<div class="assistant-sidebar-context-row">
<span class="assistant-sidebar-context-label"><%= translated("Editor") %></span>
<span class="assistant-sidebar-context-value"><%= tab_title(@current_tab, @tab_meta) %></span>
</div>
<p class="assistant-sidebar-context-text"><%= tab_subtitle(@current_tab, @tab_meta) %></p>
</section>
<form
class="assistant-sidebar-prompt-form"
data-testid="assistant-prompt-form"
phx-change="update_assistant_prompt"
phx-submit="submit_assistant_prompt"
>
<textarea
class="assistant-sidebar-prompt"
data-testid="assistant-prompt-input"
name="assistant[prompt]"
rows="6"
placeholder={translated("Ask the assistant about the active project or editor.")}
><%= @assistant_prompt %></textarea>
<button
class="assistant-sidebar-start-button"
data-testid="assistant-start-button"
type="submit"
disabled={String.trim(@assistant_prompt || "") == ""}
>
<%= translated("Start chat") %>
</button>
</form>
<%= if Enum.empty?(@assistant_messages) do %>
<div class="assistant-sidebar-welcome">
<%= for card <- @assistant_cards do %>
<section class="assistant-card">
<strong><%= translated(card.label) %></strong>
<span><%= translated(card.text) %></span>
</section>
<% end %>
</div>
<% else %>
<div class="assistant-sidebar-transcript">
<%= for message <- @assistant_messages do %>
<article
class={["assistant-sidebar-message", message.role]}
data-testid={assistant_message_testid(message.role)}
>
<span class="assistant-sidebar-message-role"><%= assistant_message_label(message.role) %></span>
<p class="assistant-sidebar-message-content"><%= message.content %></p>
</article>
<% end %>
</div>
<% end %>
</div>
</aside>