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 (tocustom_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 returnnull→ 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-editPostHog 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), legacyNsfwToggle(with its lock semantics), character alerts, amount selector — via three additive props:promptToolbar(the ModelPicker, mounted inside the prompt field),hidePromptTips, andextraControls(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 (mediaIsNsfw→is_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 owningpromptData— 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, andexampleId. Workflow-backed examples useexampleIdfor 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
mediaWorkflowNameto 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
getNsfwRequiredActionForCreategate. 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.generatewithgenerationAxes; success closes the modal via the sameshowCreateImageAtom.
Converting the next product to workflows
The rollout recipe, per product:
- Author + publish the family (fennec); agree the family key.
- Map the key in
GENERATION_PRODUCT_FAMILY(product-family.ts). - 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+promptDataAtomdecide everything; the router’s latch is component-local state.exampleSeedAtomis the one sanctioned data side-channel (a runtimeCatalogItemcan’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 thepromptData+showCreateImageAtomdispatch. - z-index: the dialog sits at
z-[330]/z-[320](must clear gallery-v2’s fullscreen detail atz-[300]); popovers portaled from inside it (ModelPicker, selects) sit atz-[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
- Generation Families — Frontend — the descriptor model, seeding guarantees (
WorkflowSeed, tuple repair), and the shared surface hook. - Generation Families — the concept: families, axes, tuples, resolution.