feat(ai-create): review the generated copy before spending image credits#191
Open
dantaspaulo wants to merge 1 commit into
Open
feat(ai-create): review the generated copy before spending image credits#191dantaspaulo wants to merge 1 commit into
dantaspaulo wants to merge 1 commit into
Conversation
## Problem
AI creation is one shot: the wizard sends the prompt and the job writes the
copy AND renders the images in a single pass. The user only sees the result
once everything has been generated and the post exists.
That is expensive and frustrating. Images are the costly part of a
generation, so a weak headline, an off-tone caption or a carousel whose
slides arrived in the wrong order all cost a full image render before the
user can react — and fixing it means starting over and paying again.
## Change
Generation is split into two phases with a review step in between.
**Phase A** (`PreparePostContent`) writes only the structured text — the same
generator and humanizer as before, no images — and stores it on a new
`AiPostDraft`. **The review screen** shows that text as editable blocks:
headline, body, caption, photo keywords, and for carousels a card per slide
that can be reordered by dragging. Edits autosave. **Phase B** reuses
`StreamPostCreation` with the approved structure, so it skips text generation
and renders the images from exactly what the user signed off on.
Nothing about the one-shot path changes: `StreamPostCreation` without
`preparedStructured` behaves exactly as before.
## How
- `PostContentComposer` — the "generate then humanize" step extracted from
`StreamPostCreation` verbatim, so both phases share one implementation.
Behaviour is unchanged; this is a pure extraction.
- `AiPostDraft` + `DraftStatus` (`preparing/ready/generating/completed/failed`)
hold the draft between phases. `generate` refuses a draft that is not
`ready`, so a double click cannot create a duplicate post or bill images twice.
- `PostContentPrepared` broadcasts on `user.{id}.ai-draft.{draftId}` when the
text lands; the review screen also polls every 4s so a dropped websocket
cannot strand the user on the skeleton.
- `creationStatus` gives the loading screen the same fallback for phase B.
## Tests
15 tests covering the flow end to end: draft creation and job dispatch, the
cross-workspace social account guard, rendering and authorization of the
review screen, generate persisting the reviewed structure and dispatching
phase B, the replay guard, autosave (persists while `ready`, no-op otherwise),
validation, and `PreparePostContent` marking the draft ready while preserving
the fields the humanizer does not return.
The full suite passes (2638 tests).
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
AI creation is one shot: the wizard sends the prompt and
StreamPostCreationwrites the copy AND renders the images in a single pass. The user only sees the result once everything has been generated and the post already exists.That is expensive and frustrating. Images are the costly part of a generation, so a weak headline, an off-tone caption, or a carousel whose slides arrived in the wrong order all cost a full image render before the user can react — and fixing it means starting over and paying again.
Change
Generation is split into two phases, with a review step in between.
Phase A (
PreparePostContent) writes only the structured text — the same generator and humanizer as today, no images — and stores it on a newAiPostDraft.The review screen shows that text as editable blocks: headline, body, caption, photo keywords, and for carousels one card per slide, reorderable by dragging. Edits autosave (debounced) so nothing is lost.
Phase B reuses
StreamPostCreationwith the approved structure, so it skips text generation and renders images from exactly what the user signed off on.The one-shot path is untouched:
StreamPostCreationwithoutpreparedStructuredbehaves exactly as before.How
PostContentComposer— the "generate, then humanize" step lifted out ofStreamPostCreationverbatim so both phases share one implementation. This is a pure extraction: no behaviour change.AiPostDraft+DraftStatus(preparing/ready/generating/completed/failed) hold the draft between phases.generaterefuses a draft that is notready, so a double click or a re-POST cannot create a duplicate post or bill images twice.PostContentPreparedbroadcasts onuser.{id}.ai-draft.{draftId}when the text lands. The review screen also polls every 4s, so a dropped websocket cannot strand the user on the skeleton.creationStatusgives the loading screen the same polling fallback for phase B.Tests
15 tests covering the flow end to end: draft creation and job dispatch, the cross-workspace social account guard, rendering and authorization of the review screen,
generatepersisting the reviewed structure and dispatching phase B, the replay guard, autosave (persists whileready, no-op otherwise), validation, andPreparePostContentmarking the draft ready while preserving the fields the humanizer does not return.The full suite passes locally (2638 tests), Pint is clean, and ESLint passes on the touched frontend files.
Notes for review
enandpt-BRare written properly; the other 13 locales carry the English strings as an untranslated fallback soLocalizationParityTestpasses. Happy to drop them to placeholders or take translations from you if you prefer.AiPostWizard.vue— it does not matchprintWidth: 80onmaintoday, and reformatting it would have buried the two-line change in noise. The two new components are formatted.