Summary
Show the feedback composer's quality verdict while the user is writing, so a submission that will be refused is caught in the form and a thin-but-real one gets a nudge instead of silently reaching triage looking like a good report.
Problem
FeedbackSubmitForm.tsx has three outcomes today — accepted, rejected, error — and all of them arrive after the user presses submit. There is no state between "published" and "refused".
tinyhumansai/backend#1241 adds a deterministic quality gate ahead of moderation with three tiers:
- block — placeholder bodies (
test), bodies under two words or with no letters, keyboard mashes, and a repeat of the same user's own recent submission
- warn — a one-word title, a body under 40 characters, or a bug report with no reproduction signal
- pass — everything else
Without app-side work the user experience of that is worse, not better: a block becomes a 400 after they have written and sent the thing, and a warn is invisible — the submission is accepted and the reason the backend attached is dropped on the floor. The nudge only helps if it arrives while the text can still be changed.
This is also distinct from moderation. A quality verdict means "we could not act on this"; it must not read like "you were flagged", which is what the existing rejected state says.
Solution (optional)
Scope: app only (app/src/components/feedback/).
Live hint. Debounced POST /feedback/validate as the user types — { type, title, body } → { success: true, data: { tier, reason } }. The endpoint is deliberately cheap for this: deterministic and local, no moderation model call, nothing written, and it does not consume the daily submission limit. The one exception is the self-repeat check, which does hit the database.
On submit. POST /feedback applies the same rules, so the form is a convenience, not the enforcement point:
block → the request 400s with the reason as its message
warn → accepted, with quality: { tier, reason } on the response body
pass → quality.tier === 'pass', reason empty
States. Add a warn state to SubmitStatus (currently 'idle' | 'loading' | 'accepted' | 'rejected' | 'error'). Warn is advisory — it shows the reason and still allows submitting. Block disables submit and explains why. Neither should be styled as the moderation rejected state.
Strings go through the existing t('feedback.submit.*') namespace; the backend reasons are English-only and are a fallback, not the copy.
Acceptance criteria
Related
- tinyhumansai/backend#1241 — the quality gate,
POST /feedback/validate, and quality on the submit response. Must deploy first: the endpoint is not in the published spec until then, so the SDK sync also follows the deploy.
- tinyhumansai/backend#1236 — the backend feature issue this completes
- tinyhumansai/backend#1133 — the parent feedback-to-GitHub effort, §7
Summary
Show the feedback composer's quality verdict while the user is writing, so a submission that will be refused is caught in the form and a thin-but-real one gets a nudge instead of silently reaching triage looking like a good report.
Problem
FeedbackSubmitForm.tsxhas three outcomes today —accepted,rejected,error— and all of them arrive after the user presses submit. There is no state between "published" and "refused".tinyhumansai/backend#1241 adds a deterministic quality gate ahead of moderation with three tiers:
test), bodies under two words or with no letters, keyboard mashes, and a repeat of the same user's own recent submissionWithout app-side work the user experience of that is worse, not better: a
blockbecomes a 400 after they have written and sent the thing, and awarnis invisible — the submission is accepted and the reason the backend attached is dropped on the floor. The nudge only helps if it arrives while the text can still be changed.This is also distinct from moderation. A quality verdict means "we could not act on this"; it must not read like "you were flagged", which is what the existing
rejectedstate says.Solution (optional)
Scope: app only (
app/src/components/feedback/).Live hint. Debounced
POST /feedback/validateas the user types —{ type, title, body }→{ success: true, data: { tier, reason } }. The endpoint is deliberately cheap for this: deterministic and local, no moderation model call, nothing written, and it does not consume the daily submission limit. The one exception is the self-repeat check, which does hit the database.On submit.
POST /feedbackapplies the same rules, so the form is a convenience, not the enforcement point:block→ the request 400s with the reason as its messagewarn→ accepted, withquality: { tier, reason }on the response bodypass→quality.tier === 'pass', reason emptyStates. Add a warn state to
SubmitStatus(currently'idle' | 'loading' | 'accepted' | 'rejected' | 'error'). Warn is advisory — it shows the reason and still allows submitting. Block disables submit and explains why. Neither should be styled as the moderationrejectedstate.Strings go through the existing
t('feedback.submit.*')namespace; the backend reasons are English-only and are a fallback, not the copy.Acceptance criteria
POST /feedback/validateas the user types (debounced) and shows the returned reason forblockandwarnblock, and a block that only the server catches surfaces its message rather than a generic errorrejected(moderation) state.github/workflows/ci-lite.yml).Related
POST /feedback/validate, andqualityon the submit response. Must deploy first: the endpoint is not in the published spec until then, so the SDK sync also follows the deploy.