feat: preliminary work on assistant
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -221,5 +221,9 @@
|
||||
"View": "Ansicht",
|
||||
"Welcome to bDS2": "Willkommen bei bDS2",
|
||||
"Workbench Notes": "Workbench-Hinweise",
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "Die Integration des Arbeitsverzeichnisses ist in der Shell noch nicht verdrahtet, aber der Tab ist auswählbar und bereit für Befehlsausgaben."
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "Die Integration des Arbeitsverzeichnisses ist in der Shell noch nicht verdrahtet, aber der Tab ist auswählbar und bereit für Befehlsausgaben.",
|
||||
"Ask the assistant about the active project or editor.": "Frage den Assistenten zum aktiven Projekt oder Editor.",
|
||||
"Start chat": "Chat starten",
|
||||
"You": "Du",
|
||||
"The assistant sidebar chat surface is ready, but model execution is not connected yet.": "Die Chat-Oberfläche der Assistenten-Seitenleiste ist bereit, aber die Modellausführung ist noch nicht verbunden."
|
||||
}
|
||||
@@ -221,5 +221,9 @@
|
||||
"View": "View",
|
||||
"Welcome to bDS2": "Welcome to bDS2",
|
||||
"Workbench Notes": "Workbench Notes",
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output."
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.",
|
||||
"Ask the assistant about the active project or editor.": "Ask the assistant about the active project or editor.",
|
||||
"Start chat": "Start chat",
|
||||
"You": "You",
|
||||
"The assistant sidebar chat surface is ready, but model execution is not connected yet.": "The assistant sidebar chat surface is ready, but model execution is not connected yet."
|
||||
}
|
||||
@@ -221,5 +221,9 @@
|
||||
"View": "Ver",
|
||||
"Welcome to bDS2": "Bienvenido a bDS2",
|
||||
"Workbench Notes": "Notas del área de trabajo",
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "La integración del árbol de trabajo aún no está conectada en el shell, pero la pestaña es seleccionable y está lista para la salida de comandos."
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "La integración del árbol de trabajo aún no está conectada en el shell, pero la pestaña es seleccionable y está lista para la salida de comandos.",
|
||||
"Ask the assistant about the active project or editor.": "Pregunta al asistente sobre el proyecto o editor activo.",
|
||||
"Start chat": "Iniciar chat",
|
||||
"You": "Tú",
|
||||
"The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La superficie de chat de la barra lateral del asistente está lista, pero la ejecución del modelo aún no está conectada."
|
||||
}
|
||||
@@ -221,5 +221,9 @@
|
||||
"View": "Affichage",
|
||||
"Welcome to bDS2": "Bienvenue dans bDS2",
|
||||
"Workbench Notes": "Notes d’atelier",
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "L’intégration de l’arbre de travail n’est pas encore câblée dans le shell, mais l’onglet est sélectionnable et prêt pour la sortie des commandes."
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "L’intégration de l’arbre de travail n’est pas encore câblée dans le shell, mais l’onglet est sélectionnable et prêt pour la sortie des commandes.",
|
||||
"Ask the assistant about the active project or editor.": "Interrogez l’assistant sur le projet ou l’éditeur actif.",
|
||||
"Start chat": "Démarrer la conversation",
|
||||
"You": "Vous",
|
||||
"The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La surface de discussion de la barre latérale de l’assistant est prête, mais l’exécution du modèle n’est pas encore connectée."
|
||||
}
|
||||
@@ -221,5 +221,9 @@
|
||||
"View": "Vista",
|
||||
"Welcome to bDS2": "Benvenuto in bDS2",
|
||||
"Workbench Notes": "Note del banco di lavoro",
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "L’integrazione del working tree non è ancora collegata nella shell, ma la scheda è selezionabile e pronta per l’output dei comandi."
|
||||
"Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "L’integrazione del working tree non è ancora collegata nella shell, ma la scheda è selezionabile e pronta per l’output dei comandi.",
|
||||
"Ask the assistant about the active project or editor.": "Chiedi all’assistente del progetto o editor attivo.",
|
||||
"Start chat": "Avvia chat",
|
||||
"You": "Tu",
|
||||
"The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La superficie chat della barra laterale assistente è pronta, ma l’esecuzione del modello non è ancora collegata."
|
||||
}
|
||||
128
priv/ui/app.css
128
priv/ui/app.css
@@ -2010,6 +2010,134 @@ button {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-description,
|
||||
.assistant-sidebar-context-text,
|
||||
.assistant-sidebar-message-content {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.assistant-sidebar-status {
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.assistant-sidebar-status.is-offline {
|
||||
background: rgba(255, 196, 0, 0.18);
|
||||
border-color: rgba(255, 196, 0, 0.35);
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
.assistant-sidebar-context {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--vscode-sideBar-background) 78%, var(--vscode-editor-background));
|
||||
}
|
||||
|
||||
.assistant-sidebar-context-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-context-label,
|
||||
.assistant-sidebar-message-role {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.assistant-sidebar-context-value {
|
||||
text-align: right;
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
.assistant-sidebar-context-text,
|
||||
.assistant-sidebar-message-content {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.assistant-sidebar-prompt-form,
|
||||
.assistant-sidebar-welcome,
|
||||
.assistant-sidebar-transcript {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-prompt {
|
||||
width: 100%;
|
||||
min-height: 112px;
|
||||
resize: vertical;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 10px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
padding: 10px 12px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.assistant-sidebar-prompt:focus {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-start-button {
|
||||
align-self: flex-start;
|
||||
border: 1px solid var(--vscode-button-border, transparent);
|
||||
border-radius: 999px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.assistant-sidebar-start-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.assistant-card,
|
||||
.assistant-sidebar-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 10px;
|
||||
border-bottom-width: 1px;
|
||||
background: color-mix(in srgb, var(--vscode-sideBar-background) 82%, var(--vscode-editor-background));
|
||||
}
|
||||
|
||||
.assistant-sidebar-message.user {
|
||||
background: color-mix(in srgb, var(--vscode-list-hoverBackground) 76%, transparent);
|
||||
}
|
||||
|
||||
.assistant-sidebar-message.assistant {
|
||||
background: color-mix(in srgb, var(--vscode-sideBar-background) 70%, var(--vscode-editor-background));
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
height: 22px;
|
||||
display: flex;
|
||||
|
||||
@@ -177,6 +177,32 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
assert html =~ ">7<"
|
||||
end
|
||||
|
||||
test "assistant sidebar exposes context, prompt, and offline-gated transcript" do
|
||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("[data-testid='toggle-assistant']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ ~s(data-testid="assistant-shell")
|
||||
assert html =~ ~s(data-testid="assistant-context")
|
||||
assert html =~ ~s(data-testid="assistant-prompt-form")
|
||||
assert html =~ ~s(data-testid="assistant-prompt-input")
|
||||
assert html =~ ~s(data-testid="assistant-start-button")
|
||||
assert html =~ ~s(>Dashboard<)
|
||||
|
||||
html =
|
||||
render_submit(view, "submit_assistant_prompt", %{
|
||||
"assistant" => %{"prompt" => "Summarize the current project"}
|
||||
})
|
||||
|
||||
assert html =~ ~s(data-testid="assistant-message-user")
|
||||
assert html =~ ~s(data-testid="assistant-message-assistant")
|
||||
assert html =~ "Summarize the current project"
|
||||
assert html =~ "Automatic AI actions stay gated by airplane mode."
|
||||
end
|
||||
|
||||
test "sidebar open supports preview and pin intents for entity tabs" do
|
||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||
|
||||
|
||||
@@ -160,4 +160,19 @@ defmodule BDS.UI.ShellTest do
|
||||
assert template =~ "tab-actions"
|
||||
assert template =~ "tab-dirty-indicator"
|
||||
end
|
||||
|
||||
test "desktop shell assets keep the assistant sidebar chat surface contract" do
|
||||
css = File.read!("/Users/gb/Projects/bDS2/priv/ui/app.css")
|
||||
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
|
||||
|
||||
assert css =~ ".assistant-sidebar-context"
|
||||
assert css =~ ".assistant-sidebar-prompt"
|
||||
assert css =~ ".assistant-sidebar-start-button"
|
||||
assert css =~ ".assistant-sidebar-message"
|
||||
assert template =~ "data-testid=\"assistant-context\""
|
||||
assert template =~ "data-testid=\"assistant-prompt-form\""
|
||||
assert template =~ "data-testid=\"assistant-prompt-input\""
|
||||
assert template =~ "data-testid=\"assistant-start-button\""
|
||||
assert template =~ "assistant-sidebar-transcript"
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user