feat(ai-create): show the draft's viral potential on the review screen#192
Open
dantaspaulo wants to merge 2 commits into
Open
feat(ai-create): show the draft's viral potential on the review screen#192dantaspaulo wants to merge 2 commits into
dantaspaulo wants to merge 2 commits 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).
Reading a draft cold gives no sense of whether it will actually perform. This adds an instant, credit-free gauge above the editable blocks that reads the real signals of the copy — hook length and tension, a call to action, word count, scannability, hashtags, attached media and carousel length — and recalculates live as the user edits. It is deterministic: the same content always scores the same, so the number never looks random, and no AI call is involved. IMPORTANT, please read before merging: the score is deliberately mapped into an 80-97 band with only positive labels (good / great / exceptional). It is a motivational device, not an objective measurement — the weakest possible draft still scores 80 and reads as "good". That is a product choice, and one you may well not want in your product. The factors and their weights are honest; only the output range is compressed. Changing VIRAL_MIN to 0 and adding a weak band turns it into a straight 0-100 measurement, and I am happy to send that instead. Also note the CTA detector recognises English, Portuguese and Spanish cues only, so the `cta` factor never fires for the other twelve locales.
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
On the review screen a user reads their draft cold. There is no signal for whether the hook actually hooks, whether a call to action is present, or whether the post is the right length — the things that decide if it performs.
Change
An instant gauge above the editable blocks, reading the real signals of the copy: hook length and tension (a question or a number), a call to action, word count, scannability (paragraph breaks), hashtags, attached media, carousel length and cross-posting reach. It recalculates live as the user edits, costs no credits and makes no AI call.
It is deterministic — the same content always scores the same, so the number never looks random between renders.
Please read before merging
The score is deliberately compressed into an 80–97 band with only positive labels (
good/great/exceptional). It is a motivational device, not an objective measurement: the weakest possible draft still scores 80 and reads as "good". There is also a small stable per-content jitter so the number does not land on round values.That is a product decision from where this came from, and one you may well not want in yours. The factors and weights are honest — only the output range is squeezed. Setting
VIRAL_MINto 0 and adding a weak band turns it into a straight 0–100 measurement that can genuinely say "this draft is weak". Say the word and I will send that version instead — I would rather you decide this knowingly than inherit a number that looks like analysis and is partly encouragement.Known limitation
The CTA detector matches English, Portuguese and Spanish cues only, so the
ctafactor (weight 4 of 30) never fires for the other twelve locales. Worth either extending per locale or moving the cue list into the translation files — happy to do either.Tests
None here: the project has no JavaScript test runner.
viralScore.tsis pure, deterministic logic and is exactly the kind of thing worth unit testing — #130 introduces Vitest, and as soon as that lands I will send the tests for this. If you prefer, I can bring the Vitest setup into this PR instead.Pint, ESLint and
vue-tscare clean on the touched files.