A convincing interface creates an obligation
UGC Forge explores a workflow from campaign brief to generated scripts and video renders. Its public interface lets someone try that workflow with demo campaigns. The engineering question is what that experience promises when the video backend is absent.
An interface can look complete while its output is entirely simulated. That is useful for developing interactions, but only if the person using it knows what they are seeing. Here, the source exposes a clear adapter boundary worth examining: endpoint methods use a live API or fall back to a demo implementation with the same response types.
Centralize the choice of data source
The request helper derives its base URL from VITE_API_URL, with a localhost default. VITE_DEMO_MODE can bypass live requests. A module-level apiUnavailable flag also stops subsequent network attempts after a network TypeError. Endpoint wrappers catch the API_UNAVAILABLE sentinel and call the corresponding demo method.
This keeps campaign creation, statistics, scripts, and renders behind a consistent interface. Components do not each invent a different fallback. But the decision is sticky for the lifetime of the loaded module: one network failure can move later requests into demo mode until reload. A recovery control would make that behavior easier to understand.
UI → endpoint wrapper → request()
response OK → live data
HTTP error → surface error
API_UNAVAILABLE → demoApiAn HTTP error should stay an error
The helper throws an explicit error when a response is not OK. That error is not converted into demo data. The fallback instead activates for the sentinel or a network TypeError. This is a valuable distinction: an authorization failure or a rejected request should not quietly become a successful fake campaign.
A TypeError still covers more than “the backend is down.” A blocked cross-origin request or other fetch failure can look similar to the application. A production version should explain the transition and offer a retry, rather than suggesting it has diagnosed the infrastructure.
Shared types do not establish shared meaning
The demo implementation stores campaigns in an array and scripts and renders in maps. Delays make actions feel asynchronous. A timer advances render progress by ten points every half-second. Completion supplies an example.com video URL; generated scores are random values. None of those values demonstrates an actual rendered asset or a measured likelihood of success.
The state disappears when the page’s module is recreated. Calling this a database, persistent campaign history, or a working video-generation pipeline would overstate what the demo does. The strongest presentation is to label it as an interactive prototype and explain the real engineering beneath that prototype.
A change I would make at the contract level is to include provenance in the result: a discriminated live/demo field, rather than relying on surrounding UI text alone. That makes the distinction available to exports, analytics, and any component that consumes the response.
type Result<T> =
| { mode: "live"; data: T }
| { mode: "demo"; data: T; simulated: true }
// Proposed contract, not the existing implementation.What I would finish before calling it live
The live render watcher currently points at a localhost WebSocket URL. Deployment needs an environment-aware WebSocket endpoint and secure transport. It also needs a real job lifecycle that survives reconnects and returns an actual downloadable asset.
I would test the boundary with four cases: explicit demo mode, an unavailable network, a rejected HTTP request, and a completed real render. Each case should have a distinct user-visible outcome. Tests that only prove the demo progress bar reaches 100 percent miss the most important behavior.
The point of a prototype is to make a product decision easier. In this case, it can test whether the campaign-to-script-to-render workflow makes sense. It cannot yet tell us about output quality, generation latency, or campaign performance. Making that distinction part of the design is itself product work.