defmodule BDS.Desktop.ShellLive.ImportEditor do @moduledoc false use Phoenix.Component alias BDS.AI alias BDS.Desktop.ShellData alias BDS.Desktop.ShellLive.ImportEditor.{ AnalysisState, ConflictResolution, ProgressTracking, TaxonomyEditing } alias BDS.ImportDefinitions import AnalysisState, only: [ default_analysis_state: 0, default_sections: 0, detail_items: 2, importable_counts: 1 ] import ProgressTracking, only: [ default_execution_state: 0, execution_progress_width: 1, format_eta: 1 ] import TaxonomyEditing, only: [ existing_taxonomy_terms: 1, taxonomy_item_editing?: 3, taxonomy_mapping_tooltip: 1, taxonomy_pill_class: 1 ] defdelegate change_definition(socket, params, reload), to: AnalysisState defdelegate select_uploads_folder(socket, reload, append_output), to: AnalysisState defdelegate select_and_analyze(socket, reload, append_output), to: AnalysisState defdelegate note_analysis_progress(socket, definition_id, step, detail, reload), to: AnalysisState defdelegate finish_analysis(socket, ref, result, reload, append_output), to: AnalysisState defdelegate execute_import(socket, reload, append_output), to: ProgressTracking defdelegate note_execution_progress( socket, definition_id, phase, current, total, detail, reload ), to: ProgressTracking defdelegate finish_execution(socket, ref, result, reload, append_output), to: ProgressTracking defdelegate handle_task_down(socket, kind, ref, reason, reload, append_output), to: ProgressTracking defdelegate change_conflict_resolution(socket, params, reload), to: ConflictResolution defdelegate start_taxonomy_edit(socket, params, reload), to: TaxonomyEditing defdelegate cancel_taxonomy_edit(socket, reload), to: TaxonomyEditing defdelegate save_taxonomy_edit(socket, params, reload), to: TaxonomyEditing defdelegate clear_taxonomy_mapping(socket, params, reload), to: TaxonomyEditing defdelegate analyze_taxonomy_ai(socket, reload, append_output), to: TaxonomyEditing @spec assign_socket(term()) :: term() def assign_socket(socket) do case socket.assigns[:current_tab] do %{type: :import, id: definition_id} -> case ImportDefinitions.get_definition(definition_id) do nil -> assign(socket, :import_editor, nil) definition -> report = ImportDefinitions.decode_analysis_result(definition) taxonomy_terms = existing_taxonomy_terms(socket.assigns.projects.active_project_id) analysis_state = Map.get( socket.assigns.import_editor_analysis_states, definition.id, default_analysis_state() ) execution_state = Map.get( socket.assigns.import_editor_execution_states, definition.id, default_execution_state() ) sections = Map.get(socket.assigns.import_editor_sections, definition.id, default_sections()) selected_model = selected_model(socket.assigns, definition.id) available_models = AI.available_chat_models(selected_model) import_editor = %{ definition_id: definition.id, definition_name: definition.name, uploads_folder_path: definition.uploads_folder_path, wxr_file_path: definition.wxr_file_path, report: report, taxonomy_terms: taxonomy_terms, taxonomy_edit: Map.get(socket.assigns.import_editor_taxonomy_edits, definition.id), analysis_state: analysis_state, execution_state: execution_state, importable_counts: importable_counts(report), sections: sections, selected_model: selected_model, selected_model_label: selected_model_label(selected_model, available_models), model_selector_open?: Map.get(socket.assigns.import_editor_model_selectors_open, definition.id, false), available_models: available_models, offline?: Map.get(socket.assigns, :offline_mode, true), is_loading: analysis_state.loading } socket |> assign(:import_editor, import_editor) |> assign( :tab_meta, Map.put(socket.assigns.tab_meta, {:import, definition.id}, %{ title: definition.name || translated("importAnalysis.untitledImport"), subtitle: translated("importAnalysis.headerDescription") }) ) end _other -> assign(socket, :import_editor, nil) end end @spec toggle_section(term(), term(), term()) :: term() def toggle_section(socket, section, reload) do with %{id: definition_id} <- socket.assigns.current_tab, section_key when section_key in [ "post_conflicts", "page_conflicts", "posts", "other", "pages", "media", "taxonomy", "macros" ] <- section, section_atom when not is_nil(section_atom) <- BDS.BoundedAtoms.import_section(section_key) do next_sections = socket.assigns.import_editor_sections |> Map.get(definition_id, default_sections()) |> Map.update!(section_atom, &(!&1)) socket |> assign( :import_editor_sections, Map.put(socket.assigns.import_editor_sections, definition_id, next_sections) ) |> reload.(socket.assigns.workbench) else _other -> reload.(socket, socket.assigns.workbench) end end @spec toggle_model_selector(term(), term()) :: term() def toggle_model_selector(socket, reload) do with %{id: definition_id} <- socket.assigns.current_tab do current = Map.get(socket.assigns.import_editor_model_selectors_open, definition_id, false) socket |> assign( :import_editor_model_selectors_open, Map.put(socket.assigns.import_editor_model_selectors_open, definition_id, not current) ) |> reload.(socket.assigns.workbench) else _other -> reload.(socket, socket.assigns.workbench) end end @spec select_ai_model(term(), term(), term()) :: term() def select_ai_model(socket, model_id, reload) do with %{id: definition_id} <- socket.assigns.current_tab do socket |> assign( :import_editor_selected_models, Map.put(socket.assigns.import_editor_selected_models, definition_id, model_id) ) |> assign( :import_editor_model_selectors_open, Map.put(socket.assigns.import_editor_model_selectors_open, definition_id, false) ) |> reload.(socket.assigns.workbench) else _other -> reload.(socket, socket.assigns.workbench) end end attr(:import_editor, :map, required: true) @spec import_editor(term()) :: term() def import_editor(assigns) do assigns = assigns |> assign(:report, Map.get(assigns.import_editor, :report)) |> assign( :analysis_state, Map.get(assigns.import_editor, :analysis_state, default_analysis_state()) ) |> assign(:execution_state, Map.get(assigns.import_editor, :execution_state)) |> assign( :counts, Map.get(assigns.import_editor, :importable_counts, %{ total: 0, tags: 0, posts: 0, media: 0, pages: 0 }) ) |> assign(:sections, Map.get(assigns.import_editor, :sections, default_sections())) |> assign(:detail_posts, detail_items(Map.get(assigns.import_editor, :report), :posts)) |> assign(:detail_pages, detail_items(Map.get(assigns.import_editor, :report), :pages)) |> assign(:detail_media, detail_items(Map.get(assigns.import_editor, :report), :media)) |> assign( :post_conflicts, Enum.filter( detail_items(Map.get(assigns.import_editor, :report), :posts), &(&1.status == "conflict") ) ) |> assign( :page_conflicts, Enum.filter( detail_items(Map.get(assigns.import_editor, :report), :pages), &(&1.status == "conflict") ) ) |> assign( :post_items, Enum.filter( detail_items(Map.get(assigns.import_editor, :report), :posts), &(Map.get(&1, :post_type, "post") == "post") ) ) |> assign( :other_items, Enum.reject( detail_items(Map.get(assigns.import_editor, :report), :posts), &(Map.get(&1, :post_type, "post") == "post") ) ) ~H"""

<%= translated("importAnalysis.headerDescription") %>

<%= @import_editor.uploads_folder_path || translated("importAnalysis.noFolderSelected") %>
<%= @import_editor.wxr_file_path || translated("importAnalysis.selectFileToAnalyze") %>
<%= if @import_editor.is_loading do %>
<%= @analysis_state.step || translated("importAnalysis.analyzingWxr") %>
<%= if present?(@analysis_state.detail) do %>
<%= @analysis_state.detail %>
<% end %>
<% end %> <%= if not is_nil(@report) and not @import_editor.is_loading do %>
<%= translated("importAnalysis.site") %> <%= get_in(@report, [:site_info, :title]) || translated("importAnalysis.untitled") %>
<%= translated("importAnalysis.url") %> <%= get_in(@report, [:site_info, :url]) || translated("importAnalysis.notAvailable") %>
<%= translated("importAnalysis.language") %> <%= get_in(@report, [:site_info, :language]) || translated("importAnalysis.notAvailable") %>
<%= translated("importAnalysis.file") %> <%= @import_editor.wxr_file_path |> to_string() |> Path.basename() %>
<.stat_card label={translated("importAnalysis.posts")} stats={@report.post_stats} /> <%= if Map.get(@report, :other_stats) && Map.get(@report.other_stats, :total, 0) > 0 do %> <.other_stat_card label={translated("importAnalysis.other")} stats={@report.other_stats} /> <% end %> <.stat_card label={translated("importAnalysis.pages")} stats={@report.page_stats} /> <.media_stat_card label={translated("importAnalysis.media")} stats={@report.media_stats} /> <.taxonomy_stat_card label={translated("importAnalysis.categories")} stats={@report.category_stats} /> <.taxonomy_stat_card label={translated("importAnalysis.tags")} stats={@report.tag_stats} />
<%= if Enum.any?(Map.get(@report, :date_distribution, [])) do %>

<%= translated("importAnalysis.dateDistribution") %>

<%= for row <- @report.date_distribution do %>
<%= row.year %>
<%= row.post_count %> / <%= row.media_count %>
<% end %>
<% end %> <%= if @execution_state.is_executing do %>

<%= translated("importAnalysis.importing") %>

<%= @execution_state.phase || translated("importAnalysis.executionStarting") %> <%= if present?(@execution_state.detail) do %> <%= @execution_state.detail %> <% end %> <%= @execution_state.current || 0 %> / <%= @execution_state.total || @counts.total %> <%= if eta = format_eta(Map.get(@execution_state, :eta)) do %> <%= eta %> <% end %>
<% end %> <%= if not @execution_state.is_executing and not @execution_state.completed do %>
<%= translated("importAnalysis.readyToImport") %> <%= if @counts.tags > 0 do %><%= @counts.tags %> <%= translated("importAnalysis.tagsCategories") %><% end %> <%= if @counts.posts > 0 do %><%= @counts.posts %> <%= translated("importAnalysis.posts") %><% end %> <%= if @counts.media > 0 do %><%= @counts.media %> <%= translated("importAnalysis.media") %><% end %> <%= if @counts.pages > 0 do %><%= @counts.pages %> <%= translated("importAnalysis.pages") %><% end %>
<% end %> <%= if @execution_state.completed do %>
<%= translated("importAnalysis.importComplete", %{count: @execution_state.count || @counts.total}) %>
<% end %> <%= if present?(@execution_state.error) do %>
<%= translated("importAnalysis.importFailed", %{error: @execution_state.error}) %>
<% end %> <%= if Enum.any?(@post_conflicts) do %> <.conflict_section title={translated("importAnalysis.postSlugConflicts")} items={@post_conflicts} expanded={@sections.post_conflicts} section="post_conflicts" /> <% end %> <%= if Enum.any?(@page_conflicts) do %> <.conflict_section title={translated("importAnalysis.pageSlugConflicts")} items={@page_conflicts} expanded={@sections.page_conflicts} section="page_conflicts" /> <% end %> <%= if Enum.any?(@post_items) do %> <.post_detail_section title={translated("importAnalysis.postsWithCount", %{count: length(@post_items)})} items={@post_items} expanded={@sections.posts} section="posts" /> <% end %> <%= if Enum.any?(@other_items) do %> <.post_detail_section title={translated("importAnalysis.otherWithCount", %{count: length(@other_items)})} items={@other_items} expanded={@sections.other} section="other" show_type={true} /> <% end %> <%= if Enum.any?(@detail_pages) do %> <.post_detail_section title={translated("importAnalysis.pagesWithCount", %{count: length(@detail_pages)})} items={@detail_pages} expanded={@sections.pages} section="pages" /> <% end %> <%= if Enum.any?(@detail_media) do %> <.media_detail_section title={translated("importAnalysis.mediaWithCount", %{count: length(@detail_media)})} items={@detail_media} expanded={@sections.media} section="media" /> <% end %> <%= if Enum.any?(Map.get(@report.items, :categories, [])) or Enum.any?(Map.get(@report.items, :tags, [])) do %>
<%= if @sections.taxonomy do %>
<%= if @import_editor.model_selector_open? do %>
<%= for model <- @import_editor.available_models do %> <% end %>
<% end %>
<%= translated("importAnalysis.aiMappingHint") %>
<.taxonomy_group title={translated("importAnalysis.categories")} items={Map.get(@report.items, :categories, [])} suggestions={Map.get(@import_editor.taxonomy_terms, :categories, [])} edit={@import_editor.taxonomy_edit} type="categories" /> <.taxonomy_group title={translated("importAnalysis.tags")} items={Map.get(@report.items, :tags, [])} suggestions={Map.get(@import_editor.taxonomy_terms, :tags, [])} edit={@import_editor.taxonomy_edit} type="tags" />
<% end %>
<% end %> <% macros = Map.get(@report, :macros, %{}) %> <%= if Enum.any?(Map.get(macros, :discovered, [])) do %>
<%= if @sections.macros do %>
<%= translated("importAnalysis.mappedCount", %{count: macros.mapped_count || 0}) %> <%= translated("importAnalysis.unmappedCount", %{count: macros.unmapped_count || 0}) %>
<%= for macro <- macros.discovered do %>
<%= macro.name %> <%= if macro.mapped, do: translated("importAnalysis.macroStatusMapped"), else: translated("importAnalysis.macroStatusUnknown") %> <%= translated("importAnalysis.macroUses", %{count: macro.total_count}) %>
<%= if Enum.any?(Map.get(macro, :usages, [])) do %>
<%= for usage <- macro.usages do %>
<%= if Enum.any?(Map.get(usage, :params, %{})) do %> <%= for {k, v} <- usage.params do %> <%= k %>=<%= v %> <% end %> <% else %> <%= translated("importAnalysis.noParameters") %> <% end %> <%= translated("importAnalysis.macroUses", %{count: usage.count}) %>
<% end %>
<% end %> <%= if Enum.any?(Map.get(macro, :post_slugs, [])) do %>
<%= translated("importAnalysis.usedIn", %{items: Enum.join(Enum.take(macro.post_slugs, 5), ", "), more: if(length(macro.post_slugs) > 5, do: translated("importAnalysis.moreSuffix", %{count: length(macro.post_slugs) - 5}), else: "")}) %>
<% end %>
<% end %>
<% end %>
<% end %> <% else %> <%= if @import_editor.is_loading do %> <% else %>

<%= translated("importAnalysis.emptyState") %>

<% end %> <% end %>
""" end attr(:title, :string, required: true) attr(:items, :list, required: true) attr(:expanded, :boolean, required: true) attr(:section, :string, required: true) @spec conflict_section(term()) :: term() def conflict_section(assigns) do ~H"""
<%= if @expanded do %> <%= for item <- @items do %> <% end %>
<%= translated("importAnalysis.slug") %> <%= translated("importAnalysis.newEntryWxr") %> <%= translated("importAnalysis.existingEntry") %> <%= translated("importAnalysis.resolution") %>
<%= Map.get(item, :slug) %> <%= Map.get(item, :title) %> <%= Map.get(item, :existing_title) || translated("importAnalysis.none") %>
<% end %>
""" end attr(:title, :string, required: true) attr(:items, :list, required: true) attr(:expanded, :boolean, required: true) attr(:section, :string, required: true) attr(:show_type, :boolean, default: false) @spec post_detail_section(term()) :: term() def post_detail_section(assigns) do ~H"""
<%= if @expanded do %> <%= if @show_type do %> <% end %> <%= for item <- @items do %> <%= if @show_type do %> <% end %> <% end %>
<%= translated("importAnalysis.status") %><%= translated("importAnalysis.type") %><%= translated("importAnalysis.title") %> <%= translated("importAnalysis.slug") %> <%= translated("importAnalysis.categories") %> <%= translated("importAnalysis.wpStatus") %> <%= translated("importAnalysis.existingMatch") %>
<%= item.status %><%= Map.get(item, :post_type, Map.get(item, :item_type)) %><%= Map.get(item, :title) %> <%= Map.get(item, :slug) %> <%= joined_or_none(Map.get(item, :categories)) %> <%= Map.get(item, :wp_status) || translated("importAnalysis.none") %> <%= Map.get(item, :existing_title) || translated("importAnalysis.none") %>
<% end %>
""" end attr(:title, :string, required: true) attr(:items, :list, required: true) attr(:expanded, :boolean, required: true) attr(:section, :string, required: true) @spec media_detail_section(term()) :: term() def media_detail_section(assigns) do ~H"""
<%= if @expanded do %> <%= for item <- @items do %> <% end %>
<%= translated("importAnalysis.status") %> <%= translated("importAnalysis.filename") %> <%= translated("importAnalysis.type") %> <%= translated("importAnalysis.path") %> <%= translated("importAnalysis.existingMatch") %>
<%= item.status %> <%= Map.get(item, :filename) %> <%= Map.get(item, :mime_type) || translated("importAnalysis.none") %> <%= Map.get(item, :relative_path) %> <%= Map.get(item, :existing_title) || translated("importAnalysis.none") %>
<% end %>
""" end attr(:label, :string, required: true) attr(:stats, :map, required: true) @spec stat_card(term()) :: term() def stat_card(assigns) do ~H"""

<%= @label %>

<%= total_stats(@stats) %>
<%= if @stats.new_count > 0 do %><%= @stats.new_count %> <%= translated("importAnalysis.new") %><% end %> <%= if @stats.update_count > 0 do %><%= @stats.update_count %> <%= translated("importAnalysis.update") %><% end %> <%= if @stats.conflict_count > 0 do %><%= @stats.conflict_count %> <%= translated("importAnalysis.conflict") %><% end %> <%= if @stats.duplicate_count > 0 do %><%= @stats.duplicate_count %> <%= translated("importAnalysis.duplicate") %><% end %>
""" end attr(:label, :string, required: true) attr(:stats, :map, required: true) @spec other_stat_card(term()) :: term() def other_stat_card(assigns) do ~H"""

<%= @label %>

<%= Map.get(@stats, :total, 0) %>
<%= for type <- Map.get(@stats, :types, []) do %> <%= type %> <% end %>
""" end attr(:label, :string, required: true) attr(:stats, :map, required: true) @spec media_stat_card(term()) :: term() def media_stat_card(assigns) do ~H"""

<%= @label %>

<%= total_media_stats(@stats) %>
<%= if @stats.new_count > 0 do %><%= @stats.new_count %> <%= translated("importAnalysis.new") %><% end %> <%= if @stats.update_count > 0 do %><%= @stats.update_count %> <%= translated("importAnalysis.update") %><% end %> <%= if @stats.conflict_count > 0 do %><%= @stats.conflict_count %> <%= translated("importAnalysis.conflict") %><% end %> <%= if @stats.duplicate_count > 0 do %><%= @stats.duplicate_count %> <%= translated("importAnalysis.duplicate") %><% end %> <%= if @stats.missing_count > 0 do %><%= @stats.missing_count %> <%= translated("importAnalysis.missing") %><% end %>
""" end attr(:label, :string, required: true) attr(:stats, :map, required: true) @spec taxonomy_stat_card(term()) :: term() def taxonomy_stat_card(assigns) do ~H"""

<%= @label %>

<%= @stats.existing_count + @stats.mapped_count + @stats.new_count %>
<%= if @stats.existing_count > 0 do %><%= @stats.existing_count %> <%= translated("importAnalysis.existing") %><% end %> <%= if @stats.mapped_count > 0 do %><%= @stats.mapped_count %> <%= translated("importAnalysis.mapped") %><% end %> <%= if @stats.new_count > 0 do %><%= @stats.new_count %> <%= translated("importAnalysis.new") %><% end %>
""" end attr(:title, :string, required: true) attr(:items, :list, required: true) attr(:suggestions, :list, required: true) attr(:edit, :map, default: nil) attr(:type, :string, required: true) @spec taxonomy_group(term()) :: term() def taxonomy_group(assigns) do ~H"""

<%= @title %>

<%= for term <- @suggestions do %> <% end %>
<%= for item <- @items do %> <%= if taxonomy_item_editing?(@edit, @type, item.name) do %>
<%= item.name %> <%= if present?(item.mapped_to) do %> <% end %>
<% else %>
<%= if item.exists_in_project do %> <%= item.name %> <% else %> <% end %> <%= if present?(item.mapped_to) do %> <% end %>
<% end %> <% end %>
""" end defp joined_or_none(values) when is_list(values) and values != [], do: Enum.join(values, ", ") defp joined_or_none(_values), do: translated("importAnalysis.none") defp status_badge_class(status), do: ["status-badge", status] defp distribution_width(value, rows, key) do max_value = rows |> Enum.map(&Map.get(&1, key, 0)) |> Enum.max(fn -> 1 end) max(8, value / max(max_value, 1) * 100) end defp total_stats(stats), do: stats.new_count + stats.update_count + stats.conflict_count + stats.duplicate_count defp total_media_stats(stats), do: total_stats(stats) + stats.missing_count defp selected_model(assigns, definition_id) do Map.get(assigns.import_editor_selected_models, definition_id) || preferred_model(assigns) end defp preferred_model(assigns) do preference_key = if Map.get(assigns, :offline_mode, true), do: :airplane_chat, else: :chat case AI.get_model_preference(preference_key) do {:ok, model} when is_binary(model) and model != "" -> model _other -> nil end end defp selected_model_label(nil, []), do: translated("importAnalysis.analyzeWith") defp selected_model_label(nil, [model | _rest]), do: model.name || model.id defp selected_model_label(model_id, available_models) do case Enum.find(available_models, &(&1.id == model_id)) do nil -> model_id model -> model.name || model.id end end defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) defp present?(value), do: value not in [nil, ""] defp blank?(value), do: value in [nil, ""] defp conflict_resolution_selected?(item, "ignore") do Map.get(item, :resolution, "ignore") in ["ignore", "skip"] end defp conflict_resolution_selected?(item, "overwrite") do Map.get(item, :resolution) in ["overwrite", "merge"] end end