# `PhoenixKitWeb.Components.AITranslate.FormGlue`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.130/lib/phoenix_kit_web/components/ai_translate/form_glue.ex#L1)

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 core `PhoenixKit.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.

# `ai_translate_config`

```elixir
@spec ai_translate_config(map()) :: map() | nil
```

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_ai_translation`

```elixir
@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.

# `dispatch_ai_translate`

```elixir
@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.

# `generate_ai_prompt`

Provision the shared default translation prompt and select it.

# `handle_ai_translation_event`

```elixir
@spec handle_ai_translation_event(
  Phoenix.LiveView.Socket.t(),
  atom(),
  map(),
  (Phoenix.LiveView.Socket.t(), Ecto.Changeset.t() -&gt;
     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.

# `select_ai_endpoint`

Endpoint dropdown change.

# `select_ai_prompt`

Prompt dropdown change.

# `select_ai_scope`

Scope radio change (`missing` | `all` | `current`).

# `toggle_ai_modal`

Modal open/close toggle.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
