Skip to content

Generator Workbench

generator-workbench is the official host shell for standard generators. It sits above generator-sdk and the runtime contract: the SDK provides platform capabilities, the runtime provides business rendering and state, and the workbench provides the unified shell UI that hosts both.

If you want one dedicated page for all generator-workbench <-> runtime communication rules, read Workbench Runtime Communication.

Core Concepts

Before reading this page, the following terms are used throughout. If any of them are unfamiliar, read Architecture and Runtime Contract first.

Generator Runtime — the generator application written by you. It owns canvas rendering, business state, the parameter panel, and export data. It is not the workbench. The workbench wraps it.

Official Host Shell (generator-workbench) — this package. It owns all platform-facing UI: the top bar, login/user entry, export actions, credits badge, billing, cloud save controls, and template publishing. You do not re-implement any of these in your runtime.

Full mode — the complete editor layout: top bar + full workspace area (or right-side parameter panel in full shell mode). The workbench renders the surrounding chrome; your runtime fills the interior.

Embed mode — the generator is embedded inside a template page via ?mode=embed. The workbench hides platform chrome and keeps only the canvas, and optionally a filtered parameter panel, visible to the outer page.

Generator SDK@atomm-developer/generator-sdk. Connects your runtime to platform APIs: auth, billing, credits, cloud save, history, and export to Studio. Initialized with your appKey.

What It Owns

  • Top bar title and branding
  • Guest avatar / login / avatar / logout entry
  • Credits badge in the top bar
  • Optional invite action powered by @atomm/atomm-pro InvitationModal
  • Template import entry and template publish modal
  • Export and Open in Studio entry with billing consumption
  • Optional shell actions for cloud save and history restore/delete
  • Optional runtime-driven auto-save orchestration when the host enables it
  • Runtime mounting into either a free workspace host or split canvas / panel hosts
  • A consistent standard layout for new generators

Shell Modes

generator-workbench supports three host shell modes:

  • shell: keep the top bar, render id="sidebar-footer" as a fixed bottom-right floating export entry, and mount the runtime into a free workspace host so the generator owns the full internal layout.
  • full: render both the top bar and the classic right sidebar layout, and keep id="sidebar-footer" as the footer export area.
  • template: hide the top bar, keep id="sidebar-footer", and let the outer page own branding and login entry.

These host shell modes are different from the route capability mode. If the page URL contains ?mode=embed, the workbench keeps the current shell layout but force-disables these shell-owned SDK integrations:

  • cloud save
  • history
  • credits badge and export credits hint
  • billing-backed export consumption
  • invitation / earn credits entry

This is mainly for main-site iframe embedding, where the host wants a lighter shell surface while still reusing the same runtime mount path. If the route omits mode or uses mode=full, the existing behavior stays unchanged.

In this embed route, the shell also hides the top bar and #sidebar-footer export area. The related workbench capabilities remain available to the host and bridge, but the iframe no longer shows those shell chrome blocks.

When ?mode=embed is active, the workbench also starts an iframe bridge compatible with the main-site generator protocol:

  • outgoing ready event: generator_pageLoaded
  • incoming commands: generator_loadTemplateData, generator_setGeneratorData, generator_getTemplateData, generator_getGeneratorData, generator_getFile
  • outgoing responses: generator_toTemplateLoaded, generator_toTemplateData, generator_toGeneratorData, generator_toFile, generator_toFileError, generator_toTemplateError, generator_toSelectTemplate

The default bridge behavior is:

  • apply incoming templates with sdk.template.applyToRuntime(...)
  • restore generator_setGeneratorData.data.info through runtime.setState(...)
  • build { template, info, cover, originImageUrl } from the current runtime state when the host asks for template data
  • resolve file export data from config.embedBridge.getExportData(...) first, then fall back to the SDK export provider registered through sdk.export.register(...)

Recommended host handshake:

  • treat generator_pageLoaded as the only bridge-ready signal because it is emitted only after the iframe has attached its message listener
  • do not use iframe.onload as the business-ready event for template/state bridge commands
  • queue generator_loadTemplateData / generator_setGeneratorData on the host until generator_pageLoaded arrives, then flush the pending payloads
  • prefer generator_loadTemplateData for template bootstrap because it receives the explicit generator_toTemplateLoaded response
  • keep generator_setGeneratorData for runtime snapshot restore; it does not emit a dedicated success ack event
  • if generator_setGeneratorData fails, the iframe reports generator_toTemplateError with action: 'setGeneratorData'

Runtime Communication Overview

Keep the main integration surface small:

  • runtime reads route capability through mount({ routeMode })
  • runtime reports business events through runtime.subscribe(listener)
  • workbench or host sends reverse commands through workbench.dispatchRuntimeCommand(...)
  • the runtime receives those commands through runtime.dispatchWorkbenchCommand?(...)

Minimal route-capability example:

ts
await runtime.mount({
  mode: 'full',
  routeMode: 'embed',
  target: 'full',
  container,
})

Minimal reverse-command example:

ts
await workbench.dispatchRuntimeCommand({
  type: 'open-login',
  data: { source: 'topbar' },
})

Current built-in communication behavior includes:

  • state-change -> auto-save orchestration
  • params_change -> DOM event runtime-params-change
  • select_template -> DOM event runtime-select-template
  • select_template in ?mode=embed -> parent bridge event generator_toSelectTemplate

For full event tables, payload rules, runtime examples, error-handling notes, and the public RuntimeEventRegistry / RuntimeEventChannel model, read:

If you are working on parent-page iframe integration instead of local workbench/runtime communication, read:

When you use template mode, the host shell can decide when the workbench should accept an external SDK token:

js
await workbench.setAuthToken(token)

This passes the token to sdk.auth.syncToken(token), letting the SDK persist it into the shared-domain utoken cookie and refresh the login state.

What It Does Not Own

  • Generator business state
  • Custom drawing logic and rendering details
  • Runtime schema design
  • Platform API implementation details
  • A full route-based project gallery or work manager

In other words: keep generator behavior in the runtime, keep platform capabilities in generator-sdk, and let generator-workbench render the standard shell around them.

Common Capability Profiles

When planning a generator-workbench integration, first decide which capability profile the generator actually needs. This is a documentation concept that helps scope the integration work; it is not an additional required runtime config field.

  • basic: login, export, Open in Studio, credits, and billing shell experience only
  • cloud: basic plus cloud save / restore and history
  • template: publish as template, template page embedding, customize flow, template import/export, or other template-authoring flows

Important:

  • using generator-workbench does not automatically mean the generator needs the template profile
  • if the task only needs shell-level capabilities, do not add template publishing UI, template JSON protocol work, or customize flow by default
  • if the task needs cloud save but not template publishing, cloud is enough; do not silently upgrade the project to template

Demo

Open demo in a new tab

Integration Flow

  1. Initialize GeneratorSDK
  2. Define the custom element with defineGeneratorWorkbench()
  3. Pass sdk, runtime, and config to <generator-workbench>
  4. Call mount()
  5. Let the workbench mount either the full runtime workspace (shell) or the split canvas / panel hosts (full / template)

If you want the host page to remove a page-level loading overlay as soon as the shell chrome is visible, set:

ts
workbench.config = {
  title: 'Frame Generator',
  mode: 'shell',
  readyPolicy: 'shell-ready',
}

Lifecycle behavior:

  • readyPolicy: 'runtime-ready' is the default. mount() resolves after runtime mount completes.
  • readyPolicy: 'shell-ready' makes mount() resolve after the shell UI is rendered, while runtime mount continues in the background.

Lifecycle events:

  • workbench-shell-ready: shell UI and workspace host are visible
  • workbench-runtime-ready: runtime mount is complete
  • workbench-ready: compatibility event emitted together with workbench-runtime-ready

Environment Configuration

Generator Workbench environment configuration has two layers:

ConfigWhereControls
GeneratorSDK.init({ env })SDK layerAll platform API base URLs (auth, cloud, history, credits, billing, export) and Passport login area (US / CN)
workbench.config.atommProEnvShell layerWorkbench shell behavior, community template API routing, atomm-pro modal context

atommProEnv is automatically derived from the injected SDK's env. When the host passes workbench.sdk = sdk, the workbench calls sdk.getEnv() and uses that as atommProEnv. This means you typically only need to set the SDK env — the shell layer stays in sync automatically.

ts
// Recommended: set env once in the SDK, shell derives it automatically
const sdk = GeneratorSDK.init({ appKey: 'my-generator', env: 'prod_cn' })

workbench.sdk = sdk
workbench.config = {
  // atommProEnv is NOT required — derived from sdk.getEnv() automatically
  // ...
}

If you do set atommProEnv explicitly and it differs from the SDK's env, the workbench logs a warning and uses the SDK's env as the source of truth.

SDK env — Supported Values

ValueAPI Base URLAuth Area
prodhttps://api.xtool.comUS
prod_cnhttps://api.makeblock.comCN
prehttps://api-pre.xtool.comUS
testhttps://api-test.xtool.comUS
devhttps://api-dev.makeblock.comCN

When env is omitted, GeneratorSDK.init() defaults to prod.

workbench.config.atommProEnv — Supported Values

Accepts the same identifiers as the SDK env. This field is optional when an SDK instance is injected. When no SDK is available, it falls back to 'prod'.

China Production (prod_cn)

When sdk env is set to prod_cn (and atommProEnv is derived from it automatically), the full China production behavior is active:

  • All platform API calls route to https://api.makeblock.com
  • Passport login uses the CN authentication area
  • Invitation entry (share / earn-credits button) — hidden automatically by the shell. Do not set invitationEnabled: false manually.
  • Publish as Template entry — hidden automatically by the shell, regardless of templateEnabled.
  • Default language — when the host page URL has no ?lang= query parameter, the workbench shell defaults to zh instead of en. An explicit ?lang=en in the URL still overrides this default.

If your generator runtime owns its own locale bootstrap (for example a Vue i18n setup that reads ?lang=), apply the same defaulting logic there: fall back to 'zh' when the resolved env is prod_cn and no explicit ?lang= is present.

ts
const sdk = GeneratorSDK.init({ appKey: 'my-generator', env: 'prod_cn' })

workbench.sdk = sdk
workbench.config = {
  title: 'My Generator',
  mode: 'shell',
  // atommProEnv is derived from sdk automatically — no need to repeat it
  atommProDomain: 'atomm',
  ...createAtommProHostI18nConfig(), // resolves 'zh' by default when env is prod_cn
}

Export Analytics

generator-workbench attaches Glow-compatible analytics for Download and Open in Studio by default.

If config.analytics.reporter is omitted, the workbench falls back to the built-in createGlowSensorsReporter(). Set analytics.reporter only when you need to customize payload mapping or replace the default targets.

ts
import { createGlowSensorsReporter } from '@atomm-developer/generator-workbench'

workbench.config = {
  title: 'Frame Generator',
  mode: 'shell',
  analytics: {
    reporter: createGlowSensorsReporter({
      resolveExportPayload({ config, runtime, sdk }) {
        const state = runtime.getState()

        return {
          content_type: config.title,
          content_id: sdk.getAppKey?.() || '',
          element_name: String(state.params?.vars?.kerf_offset ?? ''),
          content_name: String(state.params?.vars?.material_preset ?? ''),
        }
      },
    }),
  },
}

The built-in reporter emits:

  • event name: Generator_export
  • click_position: download or openinstudio
  • common fields: scene_name, item_type
  • item_type defaults to sdk.getAppKey()
  • GA4 target: window.dataLayer.push(...)
  • Sensors target: window.sensors.track(...)

For the top-bar Publish as Template click, the same reporter also emits:

  • event name: publishTemplateClick
  • payload: { content_type: workbench.config.title }

If your runtime state already exposes modelTitle, modelId, params.vars.kerf_offset, and params.vars.material_preset, you can omit the custom resolver and use the default field inference.

Performance marks:

  • generator-workbench:mount:start: emitted when mount() starts a new mount run
  • generator-workbench:shell-ready: emitted together with workbench-shell-ready
  • generator-workbench:runtime-ready: emitted together with workbench-runtime-ready
  • generator-workbench:ready: emitted together with the compatibility workbench-ready

Host staged loading example:

ts
const pageLoading = document.querySelector('#page-loading')
const workspaceSkeleton = document.querySelector('#workspace-skeleton')

workbench.config = {
  title: 'Frame Generator',
  mode: 'shell',
  readyPolicy: 'shell-ready',
}

workbench.addEventListener('workbench-shell-ready', () => {
  pageLoading?.remove()
  workspaceSkeleton?.removeAttribute('hidden')

  console.debug(
    performance.getEntriesByName('generator-workbench:shell-ready').at(-1),
  )
})

workbench.addEventListener('workbench-runtime-ready', () => {
  workspaceSkeleton?.setAttribute('hidden', 'true')
})

await workbench.mount()

Recommended host behavior:

  • Remove the page-level loading overlay on workbench-shell-ready
  • Keep a local workspace skeleton or spinner until workbench-runtime-ready
  • If you keep the default readyPolicy: 'runtime-ready', await mount() still waits for runtime completion for backwards compatibility
  • If you switch to readyPolicy: 'shell-ready', rely on the staged events above instead of treating await mount() as the final interactive signal
  • If runtime mount fails after shell-ready, listen to workbench-error / config.onError so the host can remove the skeleton and show an error state

Billing And Credits

When the injected SDK exposes credits and billing, generator-workbench will automatically:

  • show the current credits balance in the top bar after login
  • render an export hint from sdk.billing.getUsage()
  • hide export-credits-hint when usage.isEnabled = false
  • switch the hint between freeRemaining/freeTotal, creditsPerUse, and the 30s free-period countdown when billing is enabled
  • hide the credits token icon when free quota is still available and only show the freeRemaining/freeTotal text
  • when free quota is exhausted and the current credits balance is insufficient, open the @atomm/atomm-pro credits purchase modal before continuing
  • call sdk.billing.consume() before the actual export action when billing is enabled; if it returns isBlacklisted = true, the shell blocks Download / Open in Studio and shows an atomm-ui error message
  • call sdk.billing.refreshCredits() after a successful export on the credits path

This keeps the runtime focused on business rendering while the host shell owns the platform-side billing experience.

In practice, the standard export flow now looks like this:

  1. user clicks the footer export trigger
  2. workbench ensures the user is logged in
  3. workbench calls sdk.billing.getUsage() to read the latest UsageInfo
  4. if usage.isEnabled = false, it hides the hint and performs sdk.export.download() or sdk.export.openInStudio() directly
  5. if usage.isEnabled = true, it updates the hint from inFreePeriod / freeRemaining / creditsPerUse
  6. if the user is already inside the 30s free period, or still has free quota, the shell runs the export directly
  7. if free quota is exhausted, the shell compares usage.creditsPerUse with usage.creditsBalance
  8. when the balance is sufficient, it runs the export directly; when the balance is insufficient, it opens the @atomm/atomm-pro credits purchase modal, then re-reads usage before continuing
  9. before the actual export action, workbench calls sdk.billing.consume() as a billing gate
  10. if consume() returns isBlacklisted = true, the shell blocks sdk.export.download() / sdk.export.openInStudio() and shows Your account has been suspended due to security concerns
  11. if consume() succeeds normally, the shell continues with the export action
  12. on the credits path, it additionally calls sdk.billing.refreshCredits() after the export succeeds
  13. after consume succeeds, the shell enters a 30-second free period, persists the countdown in localStorage, and restores it after page refresh while keeping the hint text in sync

So the runtime still does not own platform billing logic, credits display, or export shell actions.

Cloud Save And History

When the injected SDK exposes cloud and history, generator-workbench can optionally add shell-owned project actions:

  1. Save Draft in the top bar
  2. History in the top bar
  3. saveToCloud() for host-triggered save orchestration
  4. loadHistory(), restoreHistoryItem(id), and deleteHistoryItem(id) for shell-level history flows

These flows intentionally stay at the shell layer:

  • the runtime still owns the real business state
  • the workbench only reads state and writes snapshots back through runtime.setState(...)
  • the host page still owns any route, workspace list, project gallery, or broader application workflow

In other words, this is an optional shell capability, not a full “project center.”

Cloud save has two different levels:

  • state-only save: the shell saves the runtime snapshot without a cover image; this is still a valid cloud integration
  • cover-enhanced save: the host or runtime also provides a current preview image through cover

Do not treat missing cover as "cloud save is not integrated". cover is an enhancement for draft thumbnails and history visuals, not a requirement for saving the business state itself.

Before an explicit cloud or history action such as saveToCloud(), loadHistory(), restoreHistoryItem(id), or deleteHistoryItem(id), the shell checks sdk.auth.getToken(). If there is no token yet, it calls sdk.auth.login() first and only continues once that user-triggered login flow succeeds.

When cloudEnabled is active, the shell also supports a lightweight route bootstrap:

  • the runtime mounts first so the main canvas and panel can render immediately
  • after the runtime is mounted, the workbench evaluates the route bootstrap in the background
  • if the route already contains gid and a local token already exists, the workbench restores the cloud record through sdk.cloud.restore(gid)
  • if the route already contains gid but the user is still logged out, the workbench defers the cloud bootstrap, shows a lightweight hint, and waits for a later explicit login or cloud/history action
  • after that explicit login succeeds, the workbench binds the route gid into the current shell session without automatically restoring server content back into the runtime
  • if gid is missing but the route contains templateId, the workbench requests /community/v1/web/making/:id, logs the full response plus data.generatorInfo.info, and restores that info object into the runtime
  • if gid is missing and a local token already exists, the workbench creates one cloud record through sdk.cloud.save(...) and writes the returned id back into the route
  • if gid is missing and the user is logged out, the workbench skips the initial cloud record creation until the user explicitly saves

For the route template request, the API base URL is resolved from ?env=dev|test|prod first. If that query is absent, the shell falls back to config.atommProEnv (dev -> dev, test / test_us -> test, everything else -> prod).

This keeps login or cloud bootstrap latency from blocking the runtime render path. More importantly, a logged-out gid route no longer forces an automatic restore that could overwrite local edits made before the user explicitly enters a cloud/history flow.

When the route contains ?mode=embed, this cloud bootstrap is skipped even if the host config enabled cloudEnabled.

This keeps the route-level record identity in the host shell without turning the workbench into a full project manager.

Invitation Modal

By default, the workbench adds an Earn Credits button to the left of the top-bar credits badge. When logged out, the workbench replaces the text Login button with a 40x40 guest avatar. To keep the invitation flow working, the host should still provide:

  • config.atommProEnv
  • config.atommProDomain

Under the hood, the shell starts an async background preload for atomm-pro.css as soon as invite / publish / credits flows are enabled, but still keeps Pinia, Vue Router, Vue I18n, and the bundled atomm-pro JavaScript runtime lazy until the user actually opens the flow. It then mounts XtAtommProContext inside the workbench app and opens InvitationModal with the provided configKey.

Keep these constraints in mind:

  • the invite entry is visible even before login; clicking it while logged out first calls sdk.auth.login() and then opens InvitationModal
  • if you need to hide the invite entry, explicitly pass config.invitationEnabled = false
  • if invitationConfigKey is omitted, the shell defaults it to generator_${sdk.getAppKey()}
  • if you need a different referral/share key, explicitly pass config.invitationConfigKey
  • if you need localized atomm-pro copy, either pass config.atommProLocale / config.atommProMessages manually or reuse createAtommProHostI18nConfig() from the package entry

Optional Auto-Save

If the host enables:

  • config.cloudEnabled = true
  • config.autoSaveEnabled = true
  • config.getCloudSaveOptions(...)

and the runtime implements subscribe(listener), the workbench can debounce runtime changes into cloud saves.

This gives AI-generated or rapidly integrated generators a standard autosave path without moving business state ownership out of the runtime.

If the runtime also includes data.cover on params_change events (for example a thumbnail data URL), the workbench caches the latest non-empty value and automatically reuses it for later sdk.cloud.save(...) calls whenever the host did not explicitly provide cover.

In practice, the cover source priority is:

  1. config.getCloudSaveOptions(...) explicitly returns cover
  2. config.getCloudCover(...) is called and returns a non-empty value, which the shell injects as cover
  3. the runtime emits a later non-empty params_change.data.cover, which the shell reuses as a cached thumbnail
  4. no cover is available, so the shell still saves the current snapshot without thumbnail enhancement

This is why a generator can be fully integrated for cloud save even before it implements a dedicated cover-export path.

getCloudCover

If the host already has a way to export the current canvas preview but does not want to restructure the entire getCloudSaveOptions return value, use the dedicated cover hook instead:

ts
workbench.config = {
  cloudEnabled: true,
  autoSaveEnabled: true,
  getCloudSaveOptions(context) {
    return {
      title: 'My Generator Draft',
      snapshot: context.state,
      // no cover here — handled by getCloudCover below
    }
  },
  async getCloudCover(context) {
    // context exposes sdk, runtime, and the current state
    return await context.runtime.export({ action: 'cover', format: 'png' })
  },
}

The shell calls getCloudCover only when the cover field is absent from the result of getCloudSaveOptions. Old projects that already return cover inside getCloudSaveOptions are completely unaffected.

If getCloudCover throws or returns empty, the shell logs a warning and falls back to the cached thumbnail from params_change.data.cover (or saves without a cover if that is also unavailable).

For runtime-owned edits that need an explicit save payload, the runtime can dispatch runtime-cloud-save-request with sdk.cloud.save(...)-compatible detail. The workbench debounces that request for 2 seconds and injects the current route gid as options.id before saving.

Embed Adaptation Levels

When ?mode=embed is active, the workbench lowers its shell surface. But the runtime still needs to decide how much of its own UI to show. These levels give you a shared vocabulary so that skill, documentation, and validation checklists all refer to the same thing.

Start with embed-basic by default. Only upgrade to embed-canvas-panel when the template page needs to render an editable parameter panel from the generator's own PanelSchema.

embed-basic

The minimal embed integration. The workbench hides shell chrome, starts the iframe bridge, and the runtime accepts template or snapshot data. This does not require any special layout change inside the runtime; it only requires that the runtime correctly responds to state injection and does not render platform-owned chrome in its own UI.

Acceptance criteria:

  • ?mode=embed hides the top bar and export area
  • The runtime can receive a template payload through the bridge and restore state through setState() / patchState()
  • The runtime does not render platform-owned login, credits, or export buttons when routeMode === 'embed'

embed-canvas-only

The runtime renders only its canvas output and nothing else. This is the expected level for template preview pages where the host owns the parameter panel or shows no editing controls at all.

Acceptance criteria:

  • Everything in embed-basic
  • The runtime hides its sidebar, template list, and any left-column navigation when routeMode === 'embed'
  • Mobile layout: only the canvas content is visible
  • The runtime mounts correctly with target: 'canvas' and responds to container size

embed-canvas-panel

The runtime supports independent canvas and panel mounting, so a host can split them into separate DOM containers. This level is required when a template page renders a filtered parameter panel next to the canvas.

Acceptance criteria:

  • Everything in embed-basic
  • The runtime responds to target: 'canvas' and target: 'panel' independently
  • getPanelSchema() returns a filterable schema and the runtime respects panelFilter passed at mount time
  • When mounted with target: 'canvas' only, the runtime does not show any panel UI

Use embed-canvas-only as the default when a generator is first introduced to template pages. Upgrade to embed-canvas-panel only when the template page needs to render an editable parameter panel from the generator's own PanelSchema.

When documenting, testing, or delivering a generator that needs template support, always state which embed level it targets. Do not use the term "embed adaptation" without specifying the level.

Template Publishing Flow

When the top bar template action is used, generator-workbench now:

  1. ensures the current user is logged in before opening the publish flow
  2. resolves the exportable bind.path set from runtime.getPanelSchema(), still respecting config.getTemplateFieldPaths()
  3. builds the standard template JSON through sdk.template.build(...)
  4. resolves the cover image from config.embedBridge.getExportData('cover', ...) first, then falls back to the SDK export provider when available
  5. lazy-mounts @atomm/atomm-pro's PublishTemplateModal
  6. calls PublishTemplateModal.open(...) with:
    • generatorImage: runtime template_publish_media_change.data.generatorImage first; if absent, originImageUrl; otherwise the resolved cover data URL
    • generatorTag: the latest runtime select_template event data.name first; if no such event has arrived yet, fall back to config.getTemplateMeta()?.generatorTag
    • initialData.style: from config.style; use this when the host wants to bridge a runtime-resolved style mode into the publish modal
    • initialData.summary: config.getTemplateMeta()?.summary
    • initialData.contentTags: config.getTemplateMeta()?.contentTags
    • initialData.cover: runtime template_publish_media_change.data.cover first; otherwise the resolved cover data URL
    • initialData.generatorInfo: { appKey, generatorCode, generatorName, info, template, cover, originImageUrl }
  7. before the shared atomm-pro modal submits its create/update request, any local image data still present in top-level cover or generatorInfo.cover is uploaded to OSS and replaced with the returned URL
  8. on the first publish open, caches the resolved generatorImage / generatorTag; if that first generatorImage is uploaded to OSS, later opens reuse the cached OSS URL instead of re-uploading the original local image payload
  9. when the user closes and reopens the shared modal, keeps the existing form draft by default instead of resetting the modal automatically

Where:

  • generatorInfo.generatorCode is the same value as generatorInfo.appKey
  • generatorInfo.generatorName comes from config.getTemplateMeta()?.title, then falls back to config.title
  • generatorInfo.info is built from the latest runtime state using the same info shape that sdk.cloud.save(...) submits to /ai/v5/artimind/generator: { version: '1.0.0', ...snapshot, originImageUrl }

Field source guide:

FieldCurrent default sourceNotes
generatorImagetemplate_publish_media_change.data.generatorImage -> originImageUrl -> resolved cover data URLRuntime or host may provide a more accurate current preview
generatorTaglatest select_template event data.name -> config.getTemplateMeta()?.generatorTagIf the generator has no template-selection concept, this may stay empty
initialData.covertemplate_publish_media_change.data.cover -> resolved cover data URLOptional enhancement, usually a preview thumbnail
generatorInfo.generatorNameconfig.getTemplateMeta()?.title -> config.titleThe shell already provides a safe fallback
generatorInfo.infolatest runtime state mapped to the cloud info shapeBuilt by the shell from runtime state
generatorInfo.templatesdk.template.build(...)Required for template-authoring flows

This means the integration work is usually about identifying the runtime-owned fields that have no universal fallback, not about re-implementing the full publish flow from scratch.

getTemplatePublishPayload

If the generator needs to supply generatorImage, generatorTag, cover, or originImageUrl but the default resolution chain does not reach them automatically, use the dedicated aggregated hook:

ts
workbench.config = {
  templateEnabled: true,
  isAdminPublishTemplate: false,
  async getTemplatePublishPayload(context) {
    // context exposes sdk, runtime, and the workbench element
    const coverDataUrl = await context.runtime.export({ action: 'cover', format: 'png' })
    const state = context.runtime.getState()

    return {
      // current canvas preview used as the template preview image
      generatorImage: coverDataUrl,
      // name of the currently selected template style; omit if the generator has no template list
      generatorTag: state.selectedTemplateName ?? '',
      // thumbnail shown in the publish modal
      cover: coverDataUrl,
      // URL of the original source asset that can restore the template's origin media
      originImageUrl: state.originImageUrl ?? '',
    }
  },
}

isAdminPublishTemplate

If template publishing should be available only to community admins, enable:

ts
workbench.config = {
  templateEnabled: true,
  isAdminPublishTemplate: true,
}

When enabled, the shell shows the Publish as Template entry only when auth.userInfo.isCommunityAdmin === true. The visibility is updated automatically on auth state changes (login/logout/account switch) through the built-in auth subscription. If omitted (default false), behavior remains exactly the same as existing integrations.

The shell uses the returned object as an override layer on top of the existing resolution chain. Only the fields you explicitly return are overridden; any field you leave out continues to fall back to the shell's built-in logic. Old projects that do not configure this hook are completely unaffected.

If getTemplatePublishPayload throws, the shell logs a warning and proceeds with the original default resolution for all fields.

This keeps template field discovery and standard template serialization inside the shell, while delegating the final business publish UX to the shared atomm-pro modal.

Debug Configuration

During development, the shell can log its internal payloads to the console so you can verify the integration without opening DevTools network tabs or adding breakpoints inside the workbench source.

ts
workbench.config = {
  debug: {
    // print the full cloud save payload each time autosave or an explicit save runs
    logCloudPayload: true,
    // print the full template publish payload when the publish modal opens
    logTemplatePublishPayload: true,
    // warn when required publish fields such as generatorImage or template are missing or empty
    warnOnMissingTemplateFields: true,
  },
}

All options default to false. The debug config has no effect on production behavior; it only adds console.debug and console.warn output. Remove or set all options to false before shipping.

Typical workflow:

  1. Enable logCloudPayload when verifying that the cloud save snapshot and cover match what you expect.
  2. Enable logTemplatePublishPayload when verifying that generatorImage, generatorTag, and generatorInfo.template are populated correctly before the publish modal opens.
  3. Enable warnOnMissingTemplateFields during initial integration to catch empty or missing fields that will silently degrade the publish preview.
  • Use it for new standard generators that need a unified platform shell.
  • Prefer shell mode for AI-generated or rapid-integration generators where the runtime should own the entire workspace layout.
  • Combine it with starter-html-runtime or starter-vue-runtime so the runtime stays focused on business rendering.
  • Use full only when you intentionally want the built-in right sidebar split layout.
  • Keep manual page layout customization outside the workbench only when you intentionally need a non-standard shell.
  • Treat complex iframe-host setups, legacy bridge compatibility, and DOM-selector-based media extraction as advanced integration patterns rather than the default path for every generator.

Installation Note

CDN loading behavior, Vue auto-loading, and atomm-ui script/style notes now live in Installation so those details do not need to be repeated here.

MIT Licensed