AI video products look simple from the outside: a prompt box, a Generate button, and a result. The implementation becomes much more complicated as soon as multiple models, reference images, different aspect ratios, asynchronous jobs, credit costs, and failures enter the picture.
While building Flux 3 AI, I wanted the user-facing workflow to stay consistent even when the underlying model capabilities changed. The current implementation is therefore organized around a capability-driven workbench rather than a separate form for every model.
1. Start with a normalized workbench state
The central state is deliberately small:
type WorkbenchState = {
scene: string;
model: string;
referenceImages: ReferenceImage[];
params: {
aspectRatio: string;
duration: string;
resolution: string;
generateAudio: boolean;
contentFilter?: boolean;
webSearch?: boolean;
};
};
Each model declares its supported scenes, resolutions, durations, aspect ratios, defaults, and maximum number of reference images. The interface renders controls from those declarations.
This matters when a user changes models. A duration or resolution that was valid for the previous model may not be valid for the next one. Instead of letting incompatible values leak into the request, the normalization step checks every field against the new capability set and falls back to that model's defaults.
That single decision removed a large class of provider-specific conditionals from the UI.
2. Keep product language separate from provider configuration
The workbench uses stable product-level model names while keeping the provider configuration behind a small mapping layer. The rest of the interface does not need to know which provider powers a particular option.
This separation is useful for more than branding. It means model routing can change without rewriting the editor, history cards, settings controls, or saved drafts. The UI depends on capabilities and normalized state, not on provider-specific branches.
The practical lesson was simple: provider names are infrastructure details; scenes, controls, and outputs are product concepts.
3. Treat reference uploads as a pipeline, not a file input
Reference images created more edge cases than the prompt editor.
The upload flow now performs several steps:
- Validate file type and source size.
- Decode the image and apply model-specific pixel and edge limits.
- Resize and re-encode when the original asset is too large.
- Request a direct upload session from the server.
- Upload the transformed blob directly to storage.
- Confirm the upload so the server can attach metadata to the asset.
- Fall back to a multipart API upload for smaller files if direct upload is unavailable.
Every item has an explicit state such as uploading, uploaded, or error. Each active upload also owns an AbortController, so removing a reference cancels its in-flight request instead of letting a stale upload finish in the background. Temporary object URLs are revoked after the hosted asset replaces the local preview.
This made the feature feel much more reliable than treating upload as one opaque request.
4. Model generation as an asynchronous job
Video generation is not a request that should keep a browser connection open until completion. The client creates a generation job, stores the returned item, and reads recent history through the same media-specific endpoint.
The history query only polls while at least one item is still generating. Once every item reaches a terminal state, polling stops. Creating or removing a job invalidates both the generation history and the user's credit balance so those two views do not drift apart.
Conditional polling is a small optimization, but it prevents every idle editor tab from continuously requesting history.
5. Show cost before the irreversible action
Different combinations of model, duration, resolution, audio, and processing options consume different numbers of credits. The workbench calculates a visible estimate as the user changes controls.
That estimate belongs next to the Generate action. Users should not have to discover the cost only after submitting a job. I treat the client calculation as presentation logic; billing still needs authoritative validation on the server.
The same capability object that drives the controls also drives the estimate, which keeps the displayed options and displayed cost aligned.
6. Make failure and recovery part of the design
The interface distinguishes between empty history, loading history, failed generation, failed upload, and completed work. Generated items can be removed, prompts can be copied back into the editor, and previous configurations can be reused.
The important change was to stop treating errors as exceptional UI. In an AI generation workflow, provider failures, upload limits, and long-running jobs are normal states. Designing them explicitly produces a calmer experience and a simpler state model.
What I would keep for the next model integration
The reusable parts are now clear:
- Define model capabilities as data.
- Normalize state whenever the selected model changes.
- Keep user-facing model names separate from provider routing.
- Upload large assets directly, but maintain a bounded fallback path.
- Poll only while work is active.
- Put cost estimates beside the action that spends credits.
- Represent failures as first-class states with a recovery path.
The architecture is not finished, but it has kept the editor from becoming a collection of one-off model forms. Adding a model is primarily a capability-definition task rather than a redesign of the entire workspace.
If you want to inspect the live workflow, it is available at Flux 3 AI.
Disclosure: I used AI assistance to organize and edit this article. The technical details were checked against the live implementation before publication.
Top comments (0)