PhoenixKitProjects. Web. Components. AITranslateBar
(PhoenixKitProjects v0.5.1)
Copy Markdown
View Source
AI-translation affordance for project / template / task forms.
Provides two surfaces:
<.ai_translate_button>— a single compact button rendered above the multilang tabs. Click toggles the modal below. Shows a "(N missing)" badge so the user knows there's something to do. Spinner badge while any job is in-flight.<.ai_translate_modal>— a daisyUI dialog modal containing endpoint + prompt selectors, a "Generate Default Prompt" button when none is provisioned, in-flight status, and two action buttons:- "Translate Missing Only" — enqueues a job per missing lang
- "Translate to Current Language" — enqueues a single job for the active tab's language (when not on the primary)
This is the publishing-style replacement for the earlier 40-button inline bar, which became unusable on apps with many enabled languages.
Host contract
Pass ai_translate: %{...}:
%{
enabled: true, # boolean — gates render
event: "translate_lang", # phx-click event name
toggle_event: "toggle_ai", # opens/closes the modal
select_endpoint_event: "...", # endpoint dropdown change
select_prompt_event: "...", # prompt dropdown change
select_scope_event: "...", # scope radio change
generate_prompt_event: "...", # generate-default-prompt
missing: ["es", "de"], # langs still to translate
all_langs: ["es", "de", "fr"], # every non-primary enabled lang
in_flight: ["es"], # jobs running now
modal_open: false, # is the modal visible?
endpoints: [{uuid, name}, ...], # AI endpoints list
prompts: [{uuid, name}, ...], # AI prompts list
selected_endpoint_uuid: "...", # current endpoint choice
selected_prompt_uuid: "...", # current prompt choice
scope: :missing, # :missing | :all | :current
default_prompt_exists: true, # hides the generate-button
current_lang: "es", # active multilang tab
primary_lang: "en", # source for translations + disables :current on primary
primary_lang_name: "English" # friendly label; falls back to upcased code, then a generic string
}Action contract
The modal renders a single "Translate" button driven by scope:
:missing(default) — bulk:phx-value-lang="*". Worker only enqueues langs without translations.:all— bulk with overwrite warning:phx-value-lang="**". Enqueues every non-primary lang (existing translations get overwritten on completion via the host's same-target merge).:current— single lang:phx-value-lang=<current_lang>.
Host's handle_event(@ai_translate.event, %{"lang" => lang}, socket)
branches on the value: "*" → missing-only path; "**" → all
path; concrete code → single-lang path.
Host embedding contract — render placement matters
The modal contains its own <form phx-change> elements for the
endpoint / prompt selectors. HTML forbids nested <form> and the
browser silently flattens them, which makes the selectors' change
events fire against the outer form's phx-change handler.
Render the modal outside your outer form, after </.form>:
<.form for={@form} phx-change="validate" phx-submit="save" ...>
<.ai_translate_button ai_translate={...} />
<%!-- rest of the form fields here --%>
</.form>
<%!-- Modal AFTER the form close, not inside it. --%>
<.ai_translate_modal ai_translate={...} />Both components accept the same ai_translate map, so a host can
compute it once per render and pass it to both.
LiveComponent hosts
If the host renders the button from inside a Phoenix.LiveComponent
(e.g. a translation tab strip rendered via <.live_component>),
the modal's selector events still need to land on the parent
LiveView — modal placement at the LV root is the simplest path.
Embed the modal at the LV's outer render template, not inside the
LiveComponent's HEEx, and have the LiveComponent emit a
send(self(), {:ai_translate, ...}) (or call the host event via
phx-target={@myself} on the button only) so the modal's PubSub
/ handle_event consumers stay on the LV process.
If the modal MUST render from inside a LiveComponent (rare —
e.g. embedded analytics panel that wants the picker self-contained),
pass phx-target={@myself} on every <.form> / event-emitting
element inside the modal so the LiveComponent receives the events
instead of routing them up. The current <.ai_translate_modal>
does not plumb a target attr; in that case render a thin wrapper
inside the LiveComponent and replicate the modal markup.
Summary
Functions
Compact trigger button. Shows a small spinner glyph while any
translation is in flight (no count, no missing badge — the
sibling <.ai_translate_progress> carries the per-session status,
and the modal's scope picker shows the missing count if the user
needs it).
Modal dialog with endpoint/prompt selectors + action buttons.
Slim inline progress bar — designed to sit on the same row as
<.ai_translate_button>. No text, no counter, no language list:
the bar's fill level is the only signal. Color flips to
progress-success when the session reaches :completed.
Functions
Compact trigger button. Shows a small spinner glyph while any
translation is in flight (no count, no missing badge — the
sibling <.ai_translate_progress> carries the per-session status,
and the modal's scope picker shows the missing count if the user
needs it).
Hidden when ai_translate.enabled != true or the toggle event is
blank. Stays visible after all langs are translated so the user can
re-translate at any time.
Attributes
ai_translate(:map) - Configuration map — see moduledoc for the full shape. Defaults tonil.
Modal dialog with endpoint/prompt selectors + action buttons.
Renders nothing when ai_translate.enabled != true — the gate
matches ai_translate_button/1 so a host can always render both
components and only the relevant one shows.
Slim inline progress bar — designed to sit on the same row as
<.ai_translate_button>. No text, no counter, no language list:
the bar's fill level is the only signal. Color flips to
progress-success when the session reaches :completed.
The wrapper is flex-1 min-w-0 so the bar fills the remaining
horizontal space in its parent flex container without bleeding past
it; the inner <progress> is w-full so its daisyUI default
width: 100% resolves against the wrapper, not the row.
Renders nothing until the host has dispatched at least one
translation in the session (translation_status flips to
:in_progress). The bar persists in the :completed state until
the next dispatch resets it.
Attributes
ai_translate(:map) (required) - Sameai_translateconfig map the button + modal accept. Reads:translation_status,:translation_progress, and:translation_totalkeys for the bar fill state.wrapper_class(:string) - Defaults to"flex-1 min-w-0".class(:string) - Defaults to"progress h-2 w-full block".