Use this for fal-studio-style apps where users compare many image/video models on mobile and need to launch multiple jobs without losing references.
generating spinner blocks exploration[object Object]Do not use a single global result + generating pair if the user may launch another request before the first finishes.
Use frontend job state like:
- jobQueue: [{ id, modelId, modelName, prompt, status, result, error, createdAt }]
- activeJobId
- derived activeJob
- derived activeResult
Statuses:
- queued
- running
- completed
- failed
Flow:
1. Build FormData from the current prompt/model/uploads
2. Push a new job into jobQueue
3. Fire the request in an async closure without blocking model switching
4. On success: attach result, mark completed, refresh gallery/credits
5. On error: attach normalized error text, mark failed
6. Let the user tap job chips to inspect past/current jobs
This is enough for real multi-request concurrency in a standard Express + fal app without needing backend queues.
Users hate losing uploaded references when comparing nearby models.
Use upload buckets keyed by semantic purpose, not model-specific IDs:
- generalReferences
- firstFrame
- lastFrame
- storyboardReference
On model switch:
1. Compute old upload sections and new upload sections
2. Keep only keys that exist in the next model
3. Trim preserved files/previews to the new section's maxFiles
4. Clear only truly incompatible buckets
This works well for: - image edit → image edit - first-frame video → first-frame video - first/last-frame video → another first/last-frame video - storyboard-capable models with the same section key
fal responses may include nested objects, arrays, or structured validation payloads. If you directly surface them, the UI may show [object Object] or object object object.
Implement a shared formatter that unwraps, in order:
- error
- detail
- message
- msg
- reason
- arrays of issues joined with •
- validation items with loc + type
- final JSON.stringify fallback
Recommended mapping:
- 401 → invalid key or missing access
- 403 → likely safety/policy/provider restriction
- 422 → invalid parameters or blocked request
- 429 → rate limited
- 5xx → provider-side failure
Use the same formatter on: - backend fal proxy route - frontend fetch wrapper / generation handler
A bottom action bar that is directly position: fixed and also changes height/state can appear to jump.
Use:
- outer fixed shell with safe-area padding
- inner constrained bar with stable padding
- gradient backdrop
- enough .content bottom padding so content never collides with the CTA
- touch-friendly min-height: 48px buttons on mobile
Recommended structure:
- .generate-bar-shell → fixed full width + safe area + backdrop
- .generate-bar → centered constrained content width
For fal-studio-like apps:
- generation response can include previewUrl from fal
- saved asset can be a local fallback URL or S3 URL
- gallery should use the saved asset URL
- result preview can prefer previewUrl
So keep helpers like:
- getPrimaryResult(result) → prefer previewUrl, then saved URL
- getAssetUrl(item) → prefer gallery-safe local/S3 URL
- getResultKind(result) → trust explicit resultKind first
After changes:
1. Launch one job, switch models, launch another — both must complete
2. Switch between two compatible reference-based models — uploads should remain
3. Trigger a provider rejection/moderation-ish failure — error should be readable, not object spam
4. Confirm generated assets still auto-save to gallery
5. Check mobile layout on a narrow viewport / iPhone screenshot
6. Build, restart PM2, verify dist/ timestamps, and spot-check other VPS apps
This pattern was used successfully on fal-studio when Alex wanted:
- automatic gallery saves
- multiple image/video jobs at once
- preserved references while comparing similar models
- cleaner mobile-first behavior
- clearer Kling/fal error messages instead of object object object