Skip to content

Create Dialog Routing (Legacy vs Faceted)

Create Dialog Routing (Legacy vs Faceted)

How the create-image modal decides, per open, whether the user gets the legacy dialog or its workflow-driven (“faceted”) twin — and how a generation product converts from hardcoded dispatch to workflows without touching any of the code that opens the modal.

The protocol: promptData is the only state

Every surface that opens the create modal writes the complete legacy dispatch to promptDataAtom and sets showCreateImageAtom. This matters because dozens of modules bind promptData (NSFW gates, analytics, joyride, credits pricing): faceted resolution is additive and its failure always leaves a valid legacy fallback. Explore and Gallery reuse use shared workflow-aware openers; other actions remain legacy-only.

Gallery also copies the media’s optional mediaWorkflowName into promptData. It is inert in the legacy dialog; the faceted reuse path uses it to restore the source workflow’s current family tuple when that variant remains compatible.

One sanctioned side-channel: an example click may additionally write exampleSeedAtom — the runtime-resolved CatalogItem plus prefill/override config, which can’t live in storage-backed promptData. Workflow-backed examples use their own family/workflow; examples with no workflow metadata (no family, no workflow) carry no seed and stay on the legacy flow. It is a data payload, not open-state: the modal still opens through showCreateImageAtom, promptData still carries the full legacy dispatch (so a seedless open degrades to the legacy flow), and the dialog clears the seed on close.

The router

FacetedGenerateDialog owns the create-modal slot in the main layout (the legacy <CreateImage/> is no longer mounted directly — the router renders it). On each open:

flowchart LR
A["opener writes promptData<br/>+ showCreateImageAtom"] --> S{"exampleSeedAtom set?<br/>latched at open"}
S -->|yes| R{"seed.item<br/>present?"}
R -->|yes| E
R -->|"no (`{ exampleId }` only)"| L["faceted loading skeleton"]
L --> F["opener fetches example/family<br/>and resolves seed"]
F -->|"resolved item"| E
F -->|"null / lookup failed<br/>seed cleared"| C
S -->|no| B["router: productForPromptData()<br/>latched at open"]
B -->|"null (any legacy mode,<br/>seedless example recipe,<br/>empty prompt)"| C["legacy CreateImage<br/>(untouched)"]
B -->|"product + flag on"| D["resolveProductFamily()"]
D -->|family published| E["faceted twin:<br/>legacy CreateImageForm chrome<br/>+ faceted engine"]
D -->|"family missing /<br/>query failed"| C
  • A latched example seed wins outright over product mapping. A resolved seed already carries its family item or synthesized singleton and needs no further lookup. An item-less resolving seed initially carries only { exampleId }: it latches example mode and shows the loading skeleton while the opener performs the example/family lookup, then replaces itself with the resolved seed or clears itself so the still-open modal falls back to legacy. These are opener-owned lookups, not the router’s product-family query. Authored workflow-backed Explore examples are data-gated and remain independent of rollout flags; an example with no workflow metadata carries no seed and stays on legacy.
  • productForPromptData (create-generate/helper-functions.ts) maps the promptData shape to a generation product. Today only the plain custom-prompt image reuse maps (to custom_text_to_image); every legacy mode (edit, video, carousel, kack), seedless example recipes (exampleId/isSignature — the legacy engine’s recipe reuse), and empty-prompt flows return null → legacy modal.
  • The verdict is latched when the modal opens — promptData changes while it’s open (typing) can never swap dialogs under the user.
  • The reuse takeover sits behind the faceted-reuse-edit PostHog flag (useFacetedReuse); flag off ⇒ legacy behavior for Gallery reuse, while authored workflow-backed Explore examples remain available (data-gated, not flag-gated).
  • If the product’s family isn’t in the catalog (unpublished, gated, or the query failed), the faceted half hands the still-open modal back to legacy — the click never dies silently. Gallery example resolution likewise clears its pending seed on failure; a completed example seed already owns a resolved item.

The faceted twin

Same look as the deprecated modal, workflow engine underneath:

  • Chrome: the real CreateImageForm — legacy prompt field (1500-char counter), legacy NsfwToggle (with its lock semantics), character alerts, amount selector — via three additive props: promptToolbar (the ModelPicker, mounted inside the prompt field), hidePromptTips, and extraControls (the family contract’s generic control stream, so extra axes/fields/media inputs a workflow declares render with no new UI code).
  • Engine: useFacetedSurface(product, { seed, … }) — the same spine as the create page. The seed carries axis picks (mediaIsNsfwis_nsfw) and optional Gallery workflow provenance. A matching available workflow with the same explicitness adopts its complete tuple, so Recreate starts on the source model; missing, retired, unavailable, incomplete, or rating-mismatched provenance falls to the first model in fennec’s option order that serves the source’s 18+ mode. There is no remembered model — nothing is FE-pinned across opens. The legacy chrome keeps owning promptData — the prompt, the 18+ toggle, and the aspect ratio (a plain contract input) are mirrored live into the form, so the dispatch always matches what the UI shows.
  • Example mode (useFacetedSurface(null, { example, … })): the same chrome titled “Create from example”, but the engine runs off the seed — item override, prefill/override config, seedAxes, and exampleId. Workflow-backed examples use exampleId for server-owned authored inputs and provenance.
  • Gallery example mode: the shared opener preserves the legacy Gallery prompt/state, then resolves the backing example. Family examples require mediaWorkflowName to match an available variant and seed that exact tuple. Direct workflows accept an absent media workflow name or an exact match. Signatures use the faceted form when compatible family or direct-workflow metadata resolves; they fall back only when that metadata cannot resolve to an available compatible item or variant. Examples with no workflow metadata (legacy BASIC, video, carousel), edit-source, incompatible format/rating, and missing/unavailable-family cases fall back to the already-open legacy dialog. Async results replace only their own resolving seed, so a close or newer click cannot be overwritten.
  • 18+ is locked to the source media (legacy reuse semantics): the surface is told not to coerce the seeded rating away; authorization runs through the legacy form’s own alert flow plus a generate-time getNsfwRequiredActionForCreate gate. If the family can’t serve an 18+ source at all, the modal hands back to legacy rather than silently downgrading the dispatch.
  • Dispatch goes through generationCatalog.generate with generationAxes; success closes the modal via the same showCreateImageAtom.

Converting the next product to workflows

The rollout recipe, per product:

  1. Author + publish the family (fennec); agree the family key.
  2. Map the key in GENERATION_PRODUCT_FAMILY (product-family.ts).
  3. Add the shape branch in productForPromptData (and any product-specific seed derivation, e.g. edit’s source media into the family’s media input).

Nothing else: openers, fallback, flag gating, and contract-driven rendering are already generic. Kill the family (or the flag) and the product reverts to legacy automatically.

Invariants & gotchas

  • Never add a parallel open atom. showCreateImageAtom + promptDataAtom decide everything; the router’s latch is component-local state. exampleSeedAtom is the one sanctioned data side-channel (a runtime CatalogItem can’t live in storage), and it never opens anything.
  • Openers always preserve legacy state. Workflow-aware example openers may resolve exampleSeedAtom, but may not replace or delay the promptData + showCreateImageAtom dispatch.
  • z-index: the dialog sits at z-[330]/z-[320] (must clear gallery-v2’s fullscreen detail at z-[300]); popovers portaled from inside it (ModelPicker, selects) sit at z-[360].
  • The legacy form’s internal CTAs (view characters, 18+ onboarding) close the modal through the same atom, so they work in both dialogs unchanged.

See also