{"slug": "why-should-a-completed-ai-video-job-enter-a-review-state", "title": "Why should a completed AI video job enter a review state?", "summary": "A developer working on marketing for AI Pop Video published a teaching example of a pure JavaScript state reducer that separates server-side completion of an AI video generation job from human approval of the output, using phases idle, submitting, processing, review, ready and error. The reducer guards against stale asynchronous events by comparing request IDs and job IDs before applying transitions, and ships with ten executable assertions run locally via Node.js. The author notes the example is independent of the product's internal implementation and makes no network request.", "body_md": "If a generation job reports success, should your UI immediately present its video as ready to publish?\n\nA useful UI separates server completion from a person approving the output. This post shows a small reducer for that distinction, including stale-event checks and ten executable assertions. It makes no network request.\n\nDisclosure: I work on marketing and communications for AI Pop Video. This post and example were prepared with AI assistance. The assertions below were run locally; they are not an integration test of the product or its generation service.\n\nConsider [this two-photo video interface](https://aipopvideo.com/hotel-lobby-ai-video-generator). Its default template currently asks for two portraits and lists a 15-second output and 150-credit cost. A user has several distinct decisions: confirm the selected template and cost, submit a request, wait for processing, inspect the result, then decide whether it is suitable to share. Recheck the live interface before using any current template values.\n\nThe reducer below is an independent teaching example. It does not describe that product’s internal implementation. Its phases are `idle`, `submitting`, `processing`, `review`, `ready` and `error`. A successful server event moves to `review`. Only a separate review action moves to `ready`.\n\nA late response from an older request should not change the active request. Compare the request ID before handling asynchronous events. Once the server has accepted a job, also compare the job ID on completion. These are local correlation checks; they do not authenticate the caller or replace server authorization.\n\n``` python\nimport assert from 'node:assert/strict';\n\nexport const initialState = { phase: 'idle', requestId: null, jobId: null, error: null };\nexport function transition(state, event = {}) {\n  if (event.type === 'START') {\n    if (!['idle', 'error'].includes(state.phase) || !event.requestId) return state;\n    return { phase: 'submitting', requestId: event.requestId, jobId: null, error: null };\n  }\n  if (event.requestId !== state.requestId) return state;\n  if (event.type === 'ACCEPTED' && state.phase === 'submitting' && event.jobId) {\n    return { ...state, phase: 'processing', jobId: event.jobId };\n  }\n  if (event.type === 'COMPLETE' && state.phase === 'processing' && event.jobId === state.jobId) {\n    return { ...state, phase: 'review' };\n  }\n  if (event.type === 'APPROVE_REVIEW' && state.phase === 'review') {\n    return { ...state, phase: 'ready' };\n  }\n  if (event.type === 'FAIL' && ['submitting', 'processing'].includes(state.phase)) {\n    return { ...state, phase: 'error', error: event.message || 'Request failed' };\n  }\n  return state;\n}\n\nconst start = transition(initialState, { type: 'START', requestId: 'r1' });\nassert.equal(start.phase, 'submitting');\nassert.equal(transition(start, { type: 'START', requestId: 'r2' }), start);\nassert.equal(transition(start, { type: 'COMPLETE', requestId: 'r1', jobId: 'j1' }), start);\nconst processing = transition(start, { type: 'ACCEPTED', requestId: 'r1', jobId: 'j1' });\nassert.equal(processing.phase, 'processing');\nassert.equal(transition(processing, { type: 'COMPLETE', requestId: 'old', jobId: 'j1' }), processing);\nassert.equal(transition(processing, { type: 'COMPLETE', requestId: 'r1', jobId: 'wrong' }), processing);\nconst review = transition(processing, { type: 'COMPLETE', requestId: 'r1', jobId: 'j1' });\nassert.equal(review.phase, 'review');\nassert.equal(transition(review, { type: 'APPROVE_REVIEW', requestId: 'r1' }).phase, 'ready');\nassert.equal(transition(initialState, { type: 'APPROVE_REVIEW' }), initialState);\nassert.equal(transition(processing, { type: 'FAIL', requestId: 'r1' }).phase, 'error');\nconsole.log('10 assertions passed. Pure state transitions; no network or generation request.');\n```\n\nSave the complete code as `review-state.mjs` and run `node review-state.mjs` in Node.js. The local run produced `10 assertions passed`. The tests cover the happy path, a second START while submitting, completion before acceptance, an old request, a wrong job, review approval in the wrong phase and failure during processing.\n\nThe test fixtures are synthetic IDs. No user photos, credentials, credit balance or generation API is involved. `ready` means the UI recorded a review action in this example. It is not proof of a successful public upload.\n\nWhile `submitting` or `processing`, disable the generate control and show the current task. In `review`, show the result alongside a clear checklist for faces, hands, framing and audio. Keep the review action distinct from download or public publishing. In `error`, display the actual known failure and decide whether a retry is appropriate.\n\nA disabled button alone cannot prevent duplicate requests: another tab or a modified client can still send them. The server needs its own request validation, ownership checks and idempotency strategy. This reducer deliberately leaves those backend concerns unimplemented.\n\nIf a connection drops, do not automatically assume the server rejected the job. In a real integration, retrieve the authoritative task status before deciding how the UI should recover or whether a retry could create another charged task. The example has no status-recovery or charging logic.\n\nFinally, review the source-photo and audio permissions and the destination platform’s AI disclosure controls separately. A completed generation task, a reviewed video and a published post are different outcomes. Representing them as separate states makes that distinction visible in the interface.", "url": "https://wpnews.pro/news/why-should-a-completed-ai-video-job-enter-a-review-state", "canonical_source": "https://dev.to/zhi_liu_3c86a766f3bc41d00/why-should-a-completed-ai-video-job-enter-a-review-state-40do", "published_at": "2026-10-23 11:09:35+00:00", "updated_at": "2026-10-23 11:13:15.174431+00:00", "lang": "en", "topics": ["ai-products", "developer-tools", "generative-ai"], "entities": ["AI Pop Video", "Node.js"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/why-should-a-completed-ai-video-job-enter-a-review-state", "markdown": "https://wpnews.pro/news/why-should-a-completed-ai-video-job-enter-a-review-state.md", "text": "https://wpnews.pro/news/why-should-a-completed-ai-video-job-enter-a-review-state.txt", "jsonld": "https://wpnews.pro/news/why-should-a-completed-ai-video-job-enter-a-review-state.jsonld"}}