Shared AI-translation UI for multilang form LiveViews — the
publishing/projects-style trigger button + modal, on top of core's
PhoenixKitAI.Translations pipeline.
Render-only function components driven by a single ai_translate config
map the host LV builds each render; the host owns the state + event
handlers (see PhoenixKitAI.Translations for the backend and
any consumer's form LV for the wiring shape). Five surfaces:
<.ai_multilang_tabs>— core's multilang tabs with the button/progress/hint row bundled underneath in the canonical placement; the preferred entry point for new consumers (the modal still renders separately — see its doc).<.ai_translate_button>— compact "AI Translate" trigger; toggles the modal. Spinner while a job is in flight.<.ai_translate_modal>— daisyUI dialog: endpoint + prompt selectors, a "Generate Default Prompt" button when none exists, a scope picker (missing-only / all-overwrite / current tab), in-flight status, and one scope-driven "Translate" action.<.ai_translate_progress>— slim inline progress bar for the session.<.ai_translate_hint>— the "taking a while…" stall reassurance.
Host contract
Pass ai_translate: %{...} (string OR atom keys accepted):
%{
enabled: true, # gates render
event: "translate_lang", # dispatch (phx-value-lang)
toggle_event: "toggle_ai", # open/close 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 lacking a translation
all_langs: ["es", "de", "fr"], # every non-primary enabled lang
in_flight: ["es"], # jobs running now
modal_open: false,
endpoints: [{uuid, name}, ...],
prompts: [{uuid, name}, ...],
selected_endpoint_uuid: "...",
selected_prompt_uuid: "...",
scope: :missing, # :missing | :all | :current
default_prompt_exists: true,
current_lang: "es",
primary_lang: "en",
primary_lang_name: "English",
# progress bar (optional):
translation_status: :in_progress, # nil | :in_progress | :completed
translation_progress: 1,
translation_total: 3
}Action contract
The modal's single "Translate" button sends event with a lang value
driven by scope:
:missing→phx-value-lang="*"(bulk, missing only):all→phx-value-lang="**"(bulk, overwrite all non-primary):current→phx-value-lang=<current_lang>(single)
Host's handle_event(event, %{"lang" => lang}, socket) branches on "*",
"**", or a concrete code.
Placement
The modal contains its own <form phx-change> selectors — HTML forbids
nested forms, so render <.ai_translate_modal> outside (after) the
host's outer </.form>. The button can live inside the form. Both take
the same config map; build it once and pass to both.
Summary
Functions
Core's <.multilang_tabs> with the AI-translate row bundled underneath —
the "just enable it" wrapper for the canonical placement every consumer
was hand-building (a compact button/progress/hint row tucked under the
tabs). Attrs mirror PhoenixKitWeb.Components.MultilangForm.multilang_tabs/1
plus the ai_translate config map from FormGlue.ai_translate_config/1;
when that is nil or disabled, the tabs render exactly as core's component
alone would, so the attr is safe to pass unconditionally.
Compact trigger button. Hidden when disabled / no toggle event.
Reassurance line shown when a translation is taking a while (slow: true
in the config while jobs are still in flight). A leading attention icon
flags the line, then the visible message, then a trailing info icon whose
tooltip carries the fuller "you can leave, nothing is lost" detail.
Renders nothing otherwise.
Modal dialog: endpoint/prompt selectors + scope picker + action.
Slim inline session progress bar. Renders once a dispatch has started.
Functions
Core's <.multilang_tabs> with the AI-translate row bundled underneath —
the "just enable it" wrapper for the canonical placement every consumer
was hand-building (a compact button/progress/hint row tucked under the
tabs). Attrs mirror PhoenixKitWeb.Components.MultilangForm.multilang_tabs/1
plus the ai_translate config map from FormGlue.ai_translate_config/1;
when that is nil or disabled, the tabs render exactly as core's component
alone would, so the attr is safe to pass unconditionally.
<.ai_multilang_tabs
multilang_enabled={@multilang_enabled}
language_tabs={@language_tabs}
current_lang={@current_lang}
ai_translate={FormGlue.ai_translate_config(assigns)}
/>The modal is NOT bundled — it carries its own selector <form>s and HTML
forbids nesting, so keep <.ai_translate_modal> outside (after) the host's
outer </.form> as before.
Rendering alone is not enough: the config comes from the LV wiring —
use PhoenixKitAI.Components.AITranslate.Embed (event/PubSub hooks) plus a
FormGlue.assign_ai_translation/4 call in mount with the consumer's
FormBinding. Without it the config is nil and this renders tabs-only.
class and ai_row_class defaults are tuned as a pair (the row is a
sibling of the tabs div and pads itself to match card-body); override
both together when embedding in a container that owns the padding.
Attributes
multilang_enabled(:boolean) (required)language_tabs(:list) (required)current_lang(:string) (required)compact(:boolean) - Defaults tonil.show_header(:boolean) - Defaults totrue.show_info(:boolean) - Defaults totrue.class(:string) - Defaults to"card-body pb-0".ai_translate(:map) - FormGlue config map; nil or disabled renders the tabs alone. Defaults tonil.ai_row_class(:any) - Class of the button/progress/hint row under the tabs. Tuned as a PAIR withclass: replaceclass(e.g. with "" inside an already-padded container) and adjust this to match — typically dropping the px-6.Defaults to
"flex items-center gap-3 -mt-3 px-6".
Compact trigger button. Hidden when disabled / no toggle event.
Attributes
ai_translate(:map) - Config map — see moduledoc. Defaults tonil.
Reassurance line shown when a translation is taking a while (slow: true
in the config while jobs are still in flight). A leading attention icon
flags the line, then the visible message, then a trailing info icon whose
tooltip carries the fuller "you can leave, nothing is lost" detail.
Renders nothing otherwise.
Attributes
ai_translate(:map) - Same config map; reads:slow+:in_flight. Defaults tonil.
Modal dialog: endpoint/prompt selectors + scope picker + action.
Attributes
ai_translate(:map) - Config map — see moduledoc. Defaults tonil.
Slim inline session progress bar. Renders once a dispatch has started.
Attributes
ai_translate(:map) (required) - Same config map; reads translation_status/progress/total.wrapper_class(:string) - Defaults to"flex-1 min-w-0".class(:string) - Defaults to"progress h-2 w-full block".