Shared LiveView glue for the AI-translate modal — the stateful counterpart
to the render-only PhoenixKitWeb.Components.AITranslate components.
Owns the whole modal/progress/stall state machine so consumers (catalogue, projects, …) don't each re-implement it:
- mount state + per-resource PubSub subscription (
assign_ai_translation/4) - modal open/close, endpoint/prompt/scope selection, generate-default-prompt
- scope-driven dispatch (missing
"*"/ overwrite"**"/ single tab) via corePhoenixKit.Modules.AI.Translations - live progress + a STALL hint ("taking a while…") driven by a per-arm
timer that fires only when no language completes for
ai_stall_ms/0 - folding
{:ai_translation, _, _}PubSub events back into the form
The only storage-specific behaviour — which langs already have a translation,
how to merge a completed translation into the live changeset, and who the
actor is — is supplied by a PhoenixKitWeb.Components.AITranslate.FormBinding
module, passed once at mount.
Host wiring (thin)
use Gettext, ...
alias PhoenixKitWeb.Components.AITranslate.FormGlue
# mount/3 (on :edit pass the resource, on :new pass nil)
|> FormGlue.assign_ai_translation("project", project_or_nil, MyApp.AITranslateBinding)
# one config call feeds button + modal + progress + hint
<.ai_translate_button ai_translate={FormGlue.ai_translate_config(assigns)} />
# six thin handle_event clauses delegate to:
# toggle_ai_modal/1, select_ai_endpoint/2, select_ai_prompt/2,
# select_ai_scope/2, generate_ai_prompt/1, dispatch_ai_translate/2
# one handle_info:
def handle_info({:ai_translation, event, payload}, socket),
do: {:noreply, FormGlue.handle_ai_translation_event(socket, event, payload, &assign_form/2)}assign_form/2 (the 4th arg to handle_ai_translation_event/4) is the LV's
own (socket, changeset) -> socket helper — the glue uses it to re-assign
the patched changeset so the LV's exact form-sync behaviour is preserved.
Summary
Functions
Build the ai_translate config map for the AITranslate components from the
form's assigns, or nil when AI translation isn't available. The missing
list comes from the binding (live changeset), so it reflects unsaved +
just-translated state.
Assign the AI-translate mount state and (on the connected mount of an existing resource) subscribe to its core translation topic.
Dispatch a translation from the modal's "Translate" action. lang is the
scope sentinel: "*" (missing only), "**" (all non-primary, overwrite),
or a concrete language code (current tab). Closes the modal, grows
:ai_in_flight, advances the progress session, and flashes the outcome.
Provision the shared default translation prompt and select it.
Fold a {:ai_translation, event, payload} message into the form socket.
Endpoint dropdown change.
Prompt dropdown change.
Scope radio change (missing | all | current).
Modal open/close toggle.
Functions
Build the ai_translate config map for the AITranslate components from the
form's assigns, or nil when AI translation isn't available. The missing
list comes from the binding (live changeset), so it reflects unsaved +
just-translated state.
@spec assign_ai_translation( Phoenix.LiveView.Socket.t(), String.t(), struct() | nil, module() ) :: Phoenix.LiveView.Socket.t()
Assign the AI-translate mount state and (on the connected mount of an existing resource) subscribe to its core translation topic.
Pass the resource struct on :edit; pass nil on :new. resource_type
is the key registered via ai_translatables/0; binding is the consumer's
FormBinding module.
@spec dispatch_ai_translate(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
Dispatch a translation from the modal's "Translate" action. lang is the
scope sentinel: "*" (missing only), "**" (all non-primary, overwrite),
or a concrete language code (current tab). Closes the modal, grows
:ai_in_flight, advances the progress session, and flashes the outcome.
Provision the shared default translation prompt and select it.
@spec handle_ai_translation_event( Phoenix.LiveView.Socket.t(), atom(), map(), (Phoenix.LiveView.Socket.t(), Ecto.Changeset.t() -> Phoenix.LiveView.Socket.t()) ) :: Phoenix.LiveView.Socket.t()
Fold a {:ai_translation, event, payload} message into the form socket.
assign_cs is the LV's own (socket, changeset) -> socket helper — on
:translation_completed the binding merges the translated fields into the
live changeset and assign_cs re-assigns it (so the result shows without a
DB reload and unsaved edits survive). Returns the updated socket.
Endpoint dropdown change.
Prompt dropdown change.
Scope radio change (missing | all | current).
Modal open/close toggle.