DEV Community

Cover image for Designing One UI for Many AI Photo Editing Workflows
EthanCole
EthanCole

Posted on

Designing One UI for Many AI Photo Editing Workflows

A single-purpose image tool can get surprisingly far with a file input, one button, and a result panel. The architecture changes when the same surface needs to handle background replacement, object cleanup, enhancement, canvas extension, restoration, and style changes.

The visible steps may still look identical: upload, describe, configure, generate, review, and download. Underneath, however, each model and mode can accept a different number of source images, expose different aspect ratios and resolutions, require authentication, cost a different number of credits, or become temporarily unavailable.

Disclosure: I used AI assistance to help structure and edit this article, then checked the product-specific facts and limitations against the current application evidence.

While working on the ImgPhotoEditor workflow, the useful design question became less about adding another feature card and more about representing a changing capability space without turning the UI into nested conditionals. This article describes the engineering pattern I would use for that problem. The sample types and functions are illustrative, not a copy of a private implementation.

Start with a capability contract, not a model picker

A model dropdown is easy to render, but it is the wrong abstraction if the rest of the form depends on the selected value. The client needs a contract that describes valid combinations.

One simplified shape might look like this:

type Capability = {
  model: string;
  mode: "single" | "fusion";
  sourceImages: { min: number; max: number };
  aspectRatios: string[];
  resolutions: string[];
  authRequired: boolean;
  available: boolean;
};
Enter fullscreen mode Exit fullscreen mode

The important idea is not the property names. It is that the server describes what can be submitted now, and the browser derives the available controls from that response.

This prevents three common forms of drift:

  • the UI offers a resolution that the selected model does not accept;
  • a multi-image control appears for a single-image mode;
  • marketing copy implies that one high-end option applies to every workflow.

The public ImgPhotoEditor flow, for example, accepts JPG, PNG, and WebP files up to 24 MB in the reviewed UI. Single Edit accepts exactly one source image, while multi-image behavior depends on the selected model and mode. Output options, authentication requirements, costs, and availability are validated through live capabilities. Those are constraints the interface should represent, not facts it should scatter across components.

Derive form state instead of patching it

Once capability data is available, the form can be treated as a projection of three inputs:

  1. the latest capability set;
  2. the user's current selection;
  3. the uploaded source images.

A small resolver can produce both normalized state and user-facing reasons:

function resolveEditorState(capability: Capability, files: File[]) {
  const count = files.length;
  const validCount =
    count >= capability.sourceImages.min &&
    count <= capability.sourceImages.max;

  return {
    canGenerate: capability.available && validCount,
    needsSignIn: capability.authRequired,
    sourceCountMessage: validCount
      ? null
      : `Choose ${capability.sourceImages.min}${capability.sourceImages.max} images.`,
    ratios: capability.aspectRatios,
    resolutions: capability.resolutions,
  };
}
Enter fullscreen mode Exit fullscreen mode

In production, validation still belongs on the server. The value of client derivation is immediate feedback and a single explainable path through the form.

It also makes state transitions easier to reason about. If a user switches from a fusion mode to a single-image mode, the resolver can identify that the existing selection is invalid. The UI can then ask which image to keep instead of silently submitting an impossible combination.

Treat capability refresh as data migration

Capabilities can change while a tab is open. A model may become unavailable, an output option may be removed, or a sign-in requirement may change. Replacing the capability response is therefore not just a refetch; it can invalidate user state.

A safe refresh sequence is:

  1. fetch and validate the new capability document;
  2. preserve the current selection when it is still valid;
  3. choose a documented fallback only when necessary;
  4. show what changed before generation;
  5. validate the final request again on the server.

The worst fallback is a quiet one. If a requested 4K option is no longer available for the selected combination, silently downgrading the output makes the interface look unreliable. Disable the invalid choice and explain the boundary instead.

This is also why broad claims such as “every edit supports 4K” are dangerous. A capability-driven product should use capability-driven copy: higher resolutions may be available on eligible model, ratio, and task combinations.

Separate workflow state from editor configuration

Configuration answers “what does the user want?” Workflow state answers “what is happening now?” Mixing them creates components with flags such as isUploading, isGenerating, hasError, and isDone that can accidentally describe impossible combinations.

A small state machine is easier to audit:

idle
  -> validating
  -> uploading
  -> ready
  -> submitting
  -> processing
  -> succeeded | failed | canceled
Enter fullscreen mode Exit fullscreen mode

Each state should define the actions it permits. Upload progress should not masquerade as generation progress. A failed generation should not erase the valid source selection. A succeeded task should lead to review and download, not immediately reset the editor.

The review step matters because “request completed” and “result is useful” are different statements. Generated images can miss subjective expectations even when the task succeeds technically. A credible interface gives the user space to inspect the result and decide whether to download it or revise the prompt without promising perfection.

Make invalid combinations explain themselves

Disabling a button prevents an invalid request, but it does not teach the user how to recover. Every blocked action should have a reason close to the control:

  • unsupported file type or file larger than the current limit;
  • wrong number of source images for the selected mode;
  • output ratio or resolution unavailable for this combination;
  • sign-in required for this model;
  • capability temporarily unavailable.

These messages are part of the contract. They should be derived from the same validation result used to enable submission, otherwise the interface can display one rule while enforcing another.

This approach also improves testing. Instead of clicking through every visual branch, unit tests can feed capability fixtures and user selections into a pure resolver, then assert the allowed options and recovery message.

The broader lesson

An online AI photo editor is not one feature repeated several times. It is a constrained state space presented as one coherent workflow.

The maintainable approach is to keep volatile model rules in a live contract, derive the editor from that contract, migrate user state deliberately when capabilities change, and keep asynchronous task state separate from configuration. That structure makes it easier to add workflows without adding another layer of hidden assumptions.

For context, the public editor that prompted these design notes is available at https://imgphotoeditor.ai/.

The product link is incidental to the pattern. Capability-driven interfaces are useful anywhere one front end must stay truthful while the services behind it evolve.

Top comments (0)