defmodule BDS.Desktop.ShellLive do @moduledoc false use Phoenix.LiveView import Phoenix.HTML alias BDS.Desktop.ShellData alias BDS.UI.Workbench @refresh_interval 1_500 @impl true def mount(_params, _session, socket) do if connected?(socket) do :timer.send_interval(@refresh_interval, :refresh_task_status) end workbench = Workbench.new() {:ok, socket |> assign(:page_title, ShellData.title()) |> assign(:page_language, ShellData.ui_language()) |> reload_shell(workbench)} end @impl true def handle_event("toggle_sidebar", _params, socket) do {:noreply, reload_shell(socket, Workbench.toggle_sidebar(socket.assigns.workbench))} end def handle_event("toggle_panel", _params, socket) do {:noreply, reload_shell(socket, Workbench.toggle_panel(socket.assigns.workbench))} end def handle_event("toggle_assistant_sidebar", _params, socket) do {:noreply, reload_shell(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))} end def handle_event("select_view", %{"view" => view_id}, socket) do workbench = Workbench.click_activity(socket.assigns.workbench, String.to_existing_atom(view_id)) {:noreply, reload_shell(socket, workbench)} end def handle_event("select_panel_tab", %{"tab" => tab}, socket) do workbench = socket.assigns.workbench |> Workbench.set_panel_visible(true) |> Workbench.set_panel_tab(String.to_existing_atom(tab)) {:noreply, reload_shell(socket, workbench)} end @impl true def handle_info(:refresh_task_status, socket) do task_status = BDS.Tasks.status_snapshot() {:noreply, socket |> assign(:task_status, task_status) |> assign(:editor_meta, ShellData.editor_meta(task_status)) |> assign(:status, ShellData.status_bar(socket.assigns.workbench, task_status, socket.assigns.dashboard))} end @impl true def render(assigns) do ~H"""
<%= @page_title %>

<%= translated("dashboard.title") %>

<%= translated("dashboard.subtitle") %>

<%= @dashboard.post_stats.total_posts || 0 %>
<%= translated("dashboard.stats.totalPosts") %>
<%= translated("dashboard.stats.published", %{count: @dashboard.post_stats.published_count || 0}) %> <%= translated("dashboard.stats.drafts", %{count: @dashboard.post_stats.draft_count || 0}) %> <%= if (@dashboard.post_stats.archived_count || 0) > 0 do %> <%= translated("dashboard.stats.archived", %{count: @dashboard.post_stats.archived_count || 0}) %> <% end %>
<%= @dashboard.media_stats.media_count || 0 %>
<%= translated("dashboard.stats.mediaFiles") %>
<%= translated("dashboard.stats.images", %{count: @dashboard.media_stats.image_count || 0}) %> <%= ShellData.format_bytes(@dashboard.media_stats.total_bytes || 0) %>
<%= length(@dashboard.tag_cloud_items || []) %>
<%= translated("dashboard.stats.tags") %>
<%= translated("dashboard.stats.categories", %{count: length(@dashboard.category_counts || [])}) %>
<%= if Enum.any?(@dashboard.timeline_entries || []) do %>

<%= translated("dashboard.section.postsOverTime") %>

<%= for entry <- @dashboard.timeline_entries || [] do %>
<%= entry.count || 0 %>
<%= ShellData.format_dashboard_month(entry.year, entry.month) %> <%= entry.year %>
<% end %>
<% end %> <%= if Enum.any?(@dashboard_tag_cloud_items) do %>

<%= translated("dashboard.section.tags") %>

<%= for item <- @dashboard_tag_cloud_items do %> <%= item.tag %> <% end %> <%= if length(@dashboard.tag_cloud_items || []) > 40 do %> <%= translated("dashboard.tagCloud.more", %{count: length(@dashboard.tag_cloud_items) - 40}) %> <% end %>
<% end %> <%= if Enum.any?(@dashboard.category_counts || []) do %>

<%= translated("dashboard.section.categories") %>

<%= for category <- @dashboard.category_counts || [] do %> <%= category.category || "" %> <%= category.count || 0 %> <% end %>
<% end %> <%= if Enum.any?(@dashboard.recent_posts || []) do %>

<%= translated("dashboard.section.recentlyUpdated") %>

<%= for post <- @dashboard.recent_posts || [] do %> <% end %>
<% end %>
<%= for tab <- @panel_tabs do %> <% end %>
<%= render_panel_body(assigns) %>
""" end defp reload_shell(socket, workbench) do projects = ShellData.project_snapshot() dashboard = ShellData.dashboard(projects.active_project_id) sidebar_data = ShellData.sidebar_view(projects.active_project_id, Atom.to_string(workbench.active_view)) task_status = BDS.Tasks.status_snapshot() activity_buttons = Workbench.activity_buttons(workbench, 0) socket |> assign(:workbench, workbench) |> assign(:projects, projects) |> assign(:current_project, ShellData.current_project(projects)) |> assign(:dashboard, dashboard) |> assign(:dashboard_tag_cloud_items, ShellData.dashboard_tag_cloud_items(dashboard.tag_cloud_items || [])) |> assign(:sidebar_data, sidebar_data) |> assign(:sidebar_header, active_sidebar_label(activity_buttons, workbench.active_view, sidebar_data)) |> assign(:assistant_cards, ShellData.assistant_cards()) |> assign(:editor_meta, ShellData.editor_meta(task_status)) |> assign(:task_status, task_status) |> assign(:status, ShellData.status_bar(workbench, task_status, dashboard)) |> assign(:activity_buttons, activity_buttons) |> assign(:panel_tabs, ShellData.panel_tabs(workbench)) |> assign(:supported_ui_languages, ShellData.supported_ui_languages()) end defp render_sidebar_body(assigns) do case assigns.sidebar_data.layout do "post_list" -> render_post_sidebar(assigns) "media_grid" -> render_media_sidebar(assigns) "entity_list" -> render_entity_sidebar(assigns) "nav_list" -> render_nav_sidebar(assigns) _other -> render_default_sidebar(assigns) end end defp render_post_sidebar(assigns) do ~H""" <%= for section <- @sidebar_data.sections || [] do %> <% end %> <%= if Enum.empty?(@sidebar_data.sections || []) do %> <% end %> """ end defp render_media_sidebar(assigns) do ~H""" <%= if Enum.any?(@sidebar_data.items || []) do %> <% else %> <% end %> """ end defp render_entity_sidebar(assigns) do ~H""" <%= if Enum.any?(@sidebar_data.items || []) do %>
<%= for item <- @sidebar_data.items || [] do %> <% end %>
<% else %> <% end %> """ end defp render_nav_sidebar(assigns) do ~H"""
<%= for item <- @sidebar_data.items || [] do %> <% end %>
""" end defp render_default_sidebar(assigns) do ~H""" <%= for section <- @sidebar_data.sections || [] do %> <% end %> """ end defp render_panel_body(assigns) do case assigns.workbench.panel.active_tab do :tasks -> render_task_entries(assigns) :output -> render_output_entries(assigns) :git_log -> render_git_log(assigns) other -> render_generic_panel(assigns, other) end end defp render_task_entries(assigns) do ~H""" <%= if Enum.empty?(@task_status.tasks || []) do %>
<%= translated("Tasks") %> <%= translated("No background tasks running") %>
<% else %>
<%= for task <- @task_status.tasks || [] do %>
<%= task.name %> <%= task.status |> to_string() |> String.capitalize() %>
<%= task.message || task.group_name || "" %>
<% end %>
<% end %> """ end defp render_output_entries(assigns) do ~H"""
<%= translated("Output") %> <%= translated("No shell output yet") %>
""" end defp render_git_log(assigns) do ~H"""
<%= translated("Git Log") %> <%= translated("Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.") %>
""" end defp render_generic_panel(assigns, tab) do assigns = assign(assigns, :panel_label, ShellData.route_label(tab)) ~H"""
<%= @panel_label %> <%= translated("The shared lower panel is available for tasks, output, git details, and editor-specific diagnostics.") %>
""" end defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings) defp panel_tab_label(:tasks), do: translated("Tasks") defp panel_tab_label(:output), do: translated("Output") defp panel_tab_label(:git_log), do: translated("Git Log") defp panel_tab_label(tab), do: ShellData.route_label(tab) defp activity_label("AI Assistant"), do: "Chat" defp activity_label("Source Control"), do: "Git" defp activity_label(label), do: translated(label) defp active_sidebar_label(activity_buttons, active_view, sidebar_data) do Enum.find_value(activity_buttons, translated(sidebar_data.title || ""), fn button -> if button.id == active_view, do: activity_label(button.label), else: nil end) end defp sidebar_header_label(label), do: translated(label) defp timeline_height(entry, entries) do max_count = entries |> Enum.map(&(&1.count || 0)) |> Enum.max(fn -> 1 end) max(4, ((entry.count || 0) / max_count) * 100) end defp format_sidebar_timestamp(nil), do: "" defp format_sidebar_timestamp(timestamp) do timestamp |> DateTime.from_unix!(:millisecond) |> Calendar.strftime("%x") end defp image_media?(item), do: String.starts_with?(to_string(item.mime_type || ""), "image/") defp media_thumbnail_class(item) do if image_media?(item), do: "media-thumbnail has-image", else: "media-thumbnail" end defp media_thumbnail_glyph(mime_type) do case String.split(to_string(mime_type || ""), "/", parts: 2) do ["image", _rest] -> "IMG" ["video", _rest] -> "VID" ["audio", _rest] -> "AUD" ["application", _rest] -> "DOC" _other -> "FILE" end end end