Generalize AI translation: shared pipeline, glue, hint + quality#582
Merged
Conversation
… context)
Hoists the orchestration every consumer re-implemented on top of the
Modules.AI.Translation engine into core, so a feature module needs only a
small adapter + a UI call:
- Modules.AI.Translatable — behaviour a module implements
(fetch/source_fields/put_translation/optional pubsub_topics) to make a
resource AI-translatable.
- ModuleRegistry.all_ai_translatables/0 + find_ai_translatable/1 —
discovery via the new optional ai_translatables/0 module callback.
- Modules.AI.TranslateWorker — generic Oban worker: resolve adapter by
resource_type, translate one target language, persist via the adapter,
broadcast {:ai_translation, _, _}, audit, retry only transient AI errors
(and stay quiet on non-final retries).
- Modules.AI.Translations — availability, endpoint/prompt resolution, an
idempotent shared default prompt, enqueue/1 + enqueue_all_missing/2,
PubSub topics, missing_languages/3. De-dup is app-level (querying the
four always-present Oban states) rather than Oban unique:, whose query
references the :suspended state absent from oban_job_state on some hosts
(22P02) — same trade-off as the catalogue PDF worker.
Additive: existing consumers (projects, publishing) are untouched.
See dev_docs/plans/2026-06-02-ai-translation-generalization.md.
PhoenixKitWeb.Components.AITranslate — a reusable button + modal + progress bar driven by a single ai_translate config map, on top of the Modules.AI.Translations pipeline. The modal carries endpoint + prompt selectors, a Generate-Default-Prompt button, a scope picker (missing-only / all-overwrite / current-tab), in-flight status, and one scope-driven Translate action. Generic (core Gettext + Core.Icon) so any multilang form LV can drop it in; the host owns the state + event handlers. Also adds Translations.default_prompt_exists?/0 to gate the modal's generate-prompt affordance.
- TranslateWorker: classify {:ai_error, :timeout} as retryable (it's the
transient request-timeout the AI HTTP client surfaces) so a one-off
slow/hung provider call re-runs instead of being discarded.
- AITranslate component: add <.ai_translate_hint> — a reassurance line
(short text + info-icon tooltip) shown when a translation has been in
flight past the host's threshold, telling the user it runs in the
background and they can leave. Driven by a new :slow config key.
Extracts the AI-translate modal's stateful LiveView glue — mount state +
per-resource PubSub subscription, modal open/close, endpoint/prompt/scope
selection, generate-default-prompt, scope-driven dispatch, live progress,
the "taking a while" stall hint (per-arm timer with a token guard), and
folding `{:ai_translation, _, _}` events back into the form — into a single
shared module, the stateful counterpart to the render-only
`PhoenixKitWeb.Components.AITranslate` components.
The only storage-specific behaviour is delegated to a tiny per-consumer
`FormBinding` (3 callbacks): which langs already have a translation, how to
merge a completed translation into the live changeset, and the actor uuid.
The LV still supplies its own `(socket, changeset) -> socket` assign helper,
so each form's exact assign/sync behaviour is preserved.
Catalogue and projects both adopt this in follow-up commits, replacing two
near-identical ~570/200-line copies. Stall threshold is overridable via
`config :phoenix_kit, :ai_translation_stall_ms` (default 5s).
Quality-sweep Phase 1 catch-up for core's recent Max-authored PRs (the author-filter excludes BeamLabEU#571/BeamLabEU#574/BeamLabEU#575 — alexdont/timujinne). No bugs: - BeamLabEU#572 (V125 project statuses): two intentional NITPICKs (test async:false matches sibling V-test precedent; schema_for/1 symmetry) — skipped. - BeamLabEU#573 (catalogue_pdf Oban queue): five duplicate ensure_*_queue/2 installer fns punted to a dedicated installer-dedup refactor (PR deferred it). Adds a FOLLOW_UP.md for each.
Phase 2 sweep finding: `Translations.broadcast/3` fanned the full payload — including the translated `fields` — to the global `phoenix_kit:ai_translation` topic as well as the per-resource topic. Nothing subscribes globally today (the form glue subscribes per-resource), so it's not an active leak, but it violates the payload-minimal rule and would expose resource text to any future global monitor/dashboard. Now the FULL payload (with `fields`) goes ONLY to the per-resource topic — the one the form LV consumes to patch its changeset — while the global topic and any adapter-supplied topics get a content-free summary (everything but `:fields`). Browser-verified a single-language translation still fills the form field (per-resource payload intact). Rest of the core AI surface (FormGlue stall-timer/token guard, progress accounting, Oban dedup/retry, scope dispatch, foreign-job branch, plugin guarding) triaged clean; format + credo --strict clean.
Phase 2 coverage follow-up for the shared AI-translation engine (no live PhoenixKitAI plugin needed): - translations_test: missing_languages/3 set logic; available?/list_endpoints /list_prompts degrade to safe defaults when the plugin is absent; and the broadcast/3 payload-scoping fix — pins that the FULL payload (with :fields) reaches only the per-resource topic while the global + adapter topics get a content-free summary. - translate_worker_test: retryable?/1 classification (transient retries, 5xx retries, 4xx/deterministic don't) and perform/1 setup-failure paths (missing arg / unknown adapter) discard cleanly and broadcast a normalised :translation_failed before any AI call. 14 tests, all green. The success-path round-trip (real ask_with_prompt + persist) stays covered by each consumer's browser/integration verification.
This was referenced Jun 3, 2026
ddon
pushed a commit
that referenced
this pull request
Jun 4, 2026
…tion generalization) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ddon
pushed a commit
that referenced
this pull request
Jun 4, 2026
- media_canvas_viewer: sanitize the client-supplied 'etcher:colors-changed' palette before persisting into user custom_fields — keep only short color-shaped strings, dedupe, cap at 24, and ignore the event when nothing valid survives (no garbage stored, saved palette not wiped). Closes the IMPROVEMENT-MEDIUM from the #581 review. - translations_test: alias PhoenixKit.PubSub.Manager so credo --strict passes (the merged #582 left a nested-module-alias suggestion that exits non-zero). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ddon
pushed a commit
that referenced
this pull request
Jun 4, 2026
ddon
pushed a commit
that referenced
this pull request
Jun 4, 2026
Covers the generic AI-translation pipeline (PR #582) and the Etcher palette sanitization fix. 1.7.129 was bumped in-repo but never published; 1.7.130 is the cumulative release since 1.7.128. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ddon
added a commit
to BeamLabEU/phoenix_kit_catalogue
that referenced
this pull request
Jun 4, 2026
Bump version 0.5.0 -> 0.6.0 and add the CHANGELOG entry for PR #32 (AI translation via core's shared pipeline, catalogue-detail status-filter and reorder UX) plus the post-merge quality fixes. Per the loose-constraint convention the mix.exs phoenix_kit requirement is left untouched; the CHANGELOG notes that the AI-translation pipeline (BeamLabEU/phoenix_kit#582) requires core 1.7.130+. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ddon
pushed a commit
that referenced
this pull request
Jun 5, 2026
retryable?/1 now retries {:ai_error, {:api_error, 429}} as defense-in-depth:
the built-in OpenRouter client maps 429 -> :rate_limited (snoozed), but a
custom/future provider may surface a bare {:api_error, 429}, which is the
canonical retry-after and should retry rather than discard on first attempt.
Also corrects the :timeout clause comment (Completion remaps transport
timeouts to :request_timeout before they reach the worker) and updates the
test (429 now asserts retryable; added a 404 refute).
Updates the PR #582 review doc: 429 item RESOLVED+FIXED with the verified
PhoenixKitAI contract, a dated follow-up section splitting fixed vs
developer-owned items, and a PR #583 (V129) verdict (no issues).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Generalizes AI-driven translation into core so feature modules plug in via a small adapter instead of re-implementing the whole stack (projects + publishing each carried ~1000 lines of it). Adds the generic per-language Oban worker, the orchestration context, the shared modal + LiveView glue with a "taking a while" stall hint, retry-on-timeout, and a Phase 1/2 quality pass.
Generic pipeline
Modules.AI.Translatablebehaviour (fetch/2,source_fields/2,put_translation/4, optionalpubsub_topics/1) +ai_translatables/0module callback +ModuleRegistrydiscovery.Modules.AI.Translationsorchestration — availability, endpoint/prompt defaults, idempotent shared prompt,enqueue/1+enqueue_all_missing/2, PubSub,missing_languages/3.Modules.AI.TranslateWorker— generic one-job-per-language Oban worker: retry classification (transient incl.:timeoutretries, 5xx retries, deterministic discards),{:snooze, 30}on rate-limit,ai.translation_addedaudit.Shared UI
PhoenixKitWeb.Components.AITranslate— button / modal / progress / hint (the "taking a while, runs in the background" reassurance on a stalled bar).PhoenixKitWeb.Components.AITranslate.{FormGlue,FormBinding}— the shared LiveView state machine (modal events, scope dispatch, live progress, the stall timer with a per-arm token guard) behind a 3-callback binding, so consumers wire an adapter + a tiny binding and delegate.Hardening
broadcast/3keeps translated content off the global topic: the full payload (with:fields) goes only to the per-resource topic the form consumes; the global + adapter topics get a content-free summary.Tests & docs
translations_test+translate_worker_test(14 tests):missing_languages/3, plugin-absent defaults, broadcast payload-scoping,retryable?/1, andperform/1setup-failure paths.FOLLOW_UP.mdfor PRs V125: project workflow statuses table + columns + external_id #572, Add catalogue_pdf Oban queue to the installer #573.Verification
mix format --check-formatted/mix credo --strict/mix dialyzerclean; 14 new tests green.Note
This is the foundational PR — the catalogue / projects / publishing PRs consume this generalization and need a release containing it before they build standalone.