Skip to content

feat(web): add an "other" answer to the post-export survey - #7074

Closed
open-design-crew[bot] wants to merge 3 commits into
mainfrom
feat/survey-other-option
Closed

feat(web): add an "other" answer to the post-export survey#7074
open-design-crew[bot] wants to merge 3 commits into
mainfrom
feat/survey-other-option

Conversation

@open-design-crew

Copy link
Copy Markdown
Contributor

Why

The post-export survey (#6999) asks which one thing to fix first, and offers eight choices. Those eight came from what users already report most often in the per-run feedback β€” so by construction they cover what we have already heard, and nothing else.

The people whose problem is not on that list are the ones worth hearing from, and right now they have two options: pick the least-wrong answer, or skip. Both destroy the signal.

What users will see

The second question gains a ninth option at the end: Something else.

Picking it does not finish the survey β€” a text box opens underneath, already focused, and the Skip link is replaced by Send. Everyone whose answer is on the list still finishes in one tap; the extra step is paid for only by the people who need it.

Picking Something else and sending nothing still counts as an answer. "None of these fit" is information, and treating it as a skip would quietly overstate how well the eight choices cover the ground.

Surface area

  • UI β€” see note below
  • UI β€” new option and text field in the existing survey card in apps/web
  • Keyboard shortcut
  • CLI / env var
  • API / contract
  • Extension point
  • i18n keys β€” 3 new keys across all 19 locales
  • New top-level dependency
  • Default behavior change
  • None

Screenshots

Attached in the review thread: the second question with the new option, and the state after picking it.

To see it without waiting for a real export, run the web app and open /?survey=preview (dev builds only).

Notes for review

The wire shape follows PostHog's open-choice convention: when a respondent picks the open choice, the recorded response is the text they typed, not the word "Other" with the text stored beside it. tests/experience-survey-response.test.ts pins this, including the empty-text case and the case where a stale choice index is present alongside the text.

The PostHog survey definition was updated in the same breath β€” the question now carries a ninth choice with hasOpenChoice: true. As the contract file warns, editing questions in PostHog regenerates nothing in the repo, so the two have to move together.

One consequence for reporting: free-text answers each become their own value in the improvement distribution, so that tile will grow a long tail of single-count rows once this ships. That is the point of the option, but the tile will want a "known choice vs free text" split before the tail gets long. Not in this PR.

The eight choices cover what users already tell us most often, but they cannot
cover what we have not heard yet β€” and the people whose problem is not on the
list are exactly the ones worth hearing from. Picking "Something else" opens a
text field instead of finishing the survey.

The cost is paid only by the people who need it: everyone whose answer is on
the list still finishes in one tap. This is the open-ended question we cut
earlier, put back behind a door rather than in everyone way.

Wire shape follows PostHog open-choice convention β€” the response recorded is
the text they typed. Picking "Something else" and typing nothing still reports
the choice itself, because "none of these fit" is an answer and dropping it
would turn those people into non-responders.

Skip disappears once the choice is made; it would only have been a second way
to submit.
@lefarcen
lefarcen requested a review from PerishCode August 18, 2026 12:07
@lefarcen lefarcen added size/M PR changes 100-300 lines risk/medium Medium risk: regular code changes type/feature New feature needs-validation Runtime change detected; needs human or /explore agent validation. labels Aug 18, 2026
@lefarcen

Copy link
Copy Markdown
Contributor

Quick heads-up: this PR has user-visible survey flow changes, so it needs a manual QA pass before merge β€” please hold off merging it until it's merge-ready and QA has been looped in.

Also, could you add a Validation section to the PR body with what you ran or checked for this change? That makes the QA handoff a lot easier.

@PerishCode PerishCode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one merge-safe layout regression in the new Other-answer submit state. The analytics wire behavior and locale coverage otherwise look consistent, and the focused tests, web typecheck, and repository guard pass.

πŸ” Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos.

would only be a second way to submit. Submit replaces it, and an
empty field still reports "none of these fit". */}
{picked === OTHER_INDEX ? (
<Button variant="primary" className={styles.submit} onClick={submitOther}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep Send in the right-aligned action slot that Skip occupied. This Button uses styles.submit, but that rule only changes sizing; the replaced styles.skip rule is what supplied margin-left: auto. Consequently, choosing Something else moves the primary action next to the 2/2 counter instead of replacing Skip at the right edge, producing a visible layout jump in the new state. Add margin-left: auto to .submit (or put the auto margin on the footer's action slot) so both branches retain the same alignment.

πŸ” Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Visual regression review

Head: 7510852 Β· Base: f1d6c5c

0 changed Β· 49 unchanged Β· 0 new without baseline Β· 0 failed

Unchanged cases
Case Main PR Diff
visual-avatar-local-agent-list
0 px (0.00%)
main pr diff
visual-avatar-local-agent-list-panel
0 px (0.00%)
main pr diff
visual-avatar-menu
0 px (0.00%)
main pr diff
visual-avatar-menu-panel
0 px (0.00%)
main pr diff
visual-avatar-open-design-model-picker
0 px (0.00%)
main pr diff
visual-critical-settings
0 px (0.00%)
main pr diff
visual-critical-workspace
0 px (0.00%)
main pr diff
visual-critical-workspace-preview
0 px (0.00%)
main pr diff
visual-design-system-detail
0 px (0.00%)
main pr diff
visual-design-systems
0 px (0.00%)
main pr diff
visual-home
0 px (0.00%)
main pr diff
visual-home-catalog
0 px (0.00%)
main pr diff
visual-home-context-picker
0 px (0.00%)
main pr diff
visual-home-context-picker-popover
0 px (0.00%)
main pr diff
visual-home-plugin-filter
0 px (0.00%)
main pr diff
visual-home-plugin-use-staged
0 px (0.00%)
main pr diff
visual-home-plugin-use-with-query
0 px (0.00%)
main pr diff
visual-home-staged-attachment
0 px (0.00%)
main pr diff
visual-integrations
0 px (0.00%)
main pr diff
visual-integrations-mcp
0 px (0.00%)
main pr diff

Visual diff is advisory only and does not block merging.

The green ring competed with the brand fill on the selected chip directly
above it β€” two green things one under the other, neither reading as primary.
A neutral border marks focus without joining that fight, and the inset fill
still separates the field from the card.

@PerishCode PerishCode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The open-choice analytics behavior, locale coverage, focused tests, web typecheck, and repository guard all check out. One merge-safe layout regression from the prior head remains unresolved on this head.

πŸ” Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos.

would only be a second way to submit. Submit replaces it, and an
empty field still reports "none of these fit". */}
{picked === OTHER_INDEX ? (
<Button variant="primary" className={styles.submit} onClick={submitOther}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep Send in the same right-aligned footer action slot as Skip. This branch replaces the element carrying styles.skip, whose CSS supplies margin-left: auto, with styles.submit, which only controls sizing. As a result, selecting Something else moves the action beside the 2/2 counter instead of replacing Skip at the right edge, causing a visible layout jump. Add margin-left: auto to .submit, or move that margin to a shared footer action wrapper so both branches align identically.

πŸ” Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos.

@lefarcen
lefarcen requested a review from PerishCode August 18, 2026 12:28
Submit replaces Skip in the footer, but only Skip carried the margin-left:auto
that pushes it right β€” so the moment the text field opened, the primary action
collapsed back against the step counter and jumped from the right edge to the
left. Measured from the card edge, both now sit at the same 17px gap.

@PerishCode PerishCode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The open-choice analytics shape, empty-answer fallback, locale coverage, and footer-alignment follow-up are consistent on this head. I found one merge-safe accessibility gap in the newly added free-text interaction. Local focused tests and typecheck could not be rerun because this prepared worktree has no installed workspace dependencies.

πŸ” Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos.

className={styles.textarea}
value={otherText}
onChange={(event) => setOtherText(event.target.value)}
placeholder={t('experienceSurvey.otherPlaceholder')}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give the new free-text control a programmatic accessible name. The textarea is auto-focused as soon as Something else is selected, but its only description is placeholder; placeholder text disappears once typing starts and is not a persistent label for assistive technology, so screen-reader users can land on an unnamed multiline edit control without knowing what response it accepts. Associate a visible <label> via htmlFor/id, or at minimum add an aria-label/aria-labelledby using the localized prompt, and add a focused component assertion that the textbox has that accessible name.

πŸ” Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos.

@xxiaoxiong xxiaoxiong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-executed "Other" escape hatch. The PostHog open-choice convention is the right call β€” reporting the typed text as the response itself rather than "Other" + sidecar text, and falling back to "Other" for empty input preserves "none of these fit" as a real answer instead of silently understating it. Three tests nail the three cases (typed-text, empty-fallback, typed-over-stale-index), and all 13 i18n locales + the Dict type are kept in lockstep. Approved.

pull Bot pushed a commit to HSKIMRobert/open-design that referenced this pull request Aug 19, 2026
…add an "other" answer (nexu-io#7117)

* feat(web): add an "other" answer with a free-text field to the survey

The eight choices cover what users already tell us most often, but they cannot
cover what we have not heard yet β€” and the people whose problem is not on the
list are exactly the ones worth hearing from. Picking "Something else" opens a
text field instead of finishing the survey.

The cost is paid only by the people who need it: everyone whose answer is on
the list still finishes in one tap. This is the open-ended question we cut
earlier, put back behind a door rather than in everyone way.

Wire shape follows PostHog open-choice convention β€” the response recorded is
the text they typed. Picking "Something else" and typing nothing still reports
the choice itself, because "none of these fit" is an answer and dropping it
would turn those people into non-responders.

Skip disappears once the choice is made; it would only have been a second way
to submit.

* style(web): use a neutral focus border on the survey text field

The green ring competed with the brand fill on the selected chip directly
above it β€” two green things one under the other, neither reading as primary.
A neutral border marks focus without joining that fight, and the inset fill
still separates the field from the card.

* fix(web): keep the survey submit button on the footer right edge

Submit replaces Skip in the footer, but only Skip carried the margin-left:auto
that pushes it right β€” so the moment the text field opened, the primary action
collapsed back against the step counter and jumped from the right edge to the
left. Measured from the card edge, both now sit at the same 17px gap.

* feat(web): ask the experience survey after a delivered artifact, not an export

The post-export survey could only reach people who exported. Over 30 days
~13k users exported while ~37k produced an artifact, so roughly two thirds
of the people who got real work out of the product were never asked β€” and
the ones who tried it, got something, and left without exporting are the
ones worth hearing from most.

The card is now armed by a delivered design run (resolveDesignDeliveryOutcome
=== 'delivered'), from the second such delivery on. The first delivery
measures a first impression rather than an experience, and lands at the one
moment the user most wants to look at what they just got; waiting for the
second costs almost no reach because deliveries repeat (~13 per user per 30
days).

Because a delivery is usually mid-session rather than end-of-session, the
3s delay now bails out if the user starts writing their next prompt.
'beforeinput' rather than 'keydown' so IME composition counts as typing.
A dropped chance is taken by the next delivery, so the card lands after the
turn the user actually stopped on.

Export no longer arms anything; the three FileViewer call sites are gone.
Every survey event carries trigger: 'post_generation' so the two regimes
stay distinguishable in the events table.

* feat(web): arm the experience survey on the first delivered artifact

Drops the two-delivery threshold to one. Waiting for a second run bought a
more considered opinion at the cost of everyone who produces one artifact
and leaves β€” the users we understand least, and the ones this survey was
widened to reach in the first place.

Asking on the first delivery is affordable because the card is not an
interruption: the component still waits out the delay and still drops the
chance entirely if the user starts typing, so an early ask that lands badly
costs a dropped chance rather than a derailed session.

The delivery counter stays. It is the seam the policy is made of, and it is
what makes an unwritable store read as "not yet qualified" β€” a store that
cannot write cannot record a dismissal either, so without it the card would
come back after every run with no way to stop it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): retire the experience survey when it is shown, not when it is answered

One ask per user was the intent, but `retireSurvey()` only ran from `finish`
and `close`. Ignoring the card β€” the most common way to decline a prompt β€”
left the flag unset, so the card returned after the user's next artifact, and
the one after that. `exposedRef` did not cover this: it is a ref, so it resets
on the next page load.

Retiring on exposure makes the rule what it claims to be: a user sees this
card at most once, whatever they do with it. A chance dropped by the typing
bail-out is still retried, because a card the user never saw did not spend
their one ask.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(web): drop the typing bail-out from the experience survey

The card no longer cancels itself when the user starts typing during the
three-second delay. With one ask per user, a dropped chance is the whole ask
for anyone whose habit is to type straight into the next prompt β€” and that
habit belongs to the engaged users this survey most wants to hear from.

Removes the `beforeinput` listener and the IME reasoning that went with it.
The delay stays: it keeps the card from animating in on top of the artifact,
which was always its other job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): give the survey's free-text field an accessible name

Review finding from nexu-io#7074, folded in with that branch. The textarea is
auto-focused the instant "Something else" is picked, so its accessible name
is the only thing telling a screen-reader user what the field is for β€” and it
had none. A placeholder is not a label and disappears on the first keystroke.

Reuses the placeholder string rather than adding a twentieth translation of
the same sentence, so the visible and announced names cannot drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: ensure PR CI

---------

Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
Co-authored-by: Eli <qiongyu1999@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: open-design-crew[bot] <4197780+open-design-crew[bot]@users.noreply.github.com>
open-design-release-bot Bot added a commit that referenced this pull request Aug 19, 2026
…add an "other" answer (#7117) (#7127)

* feat(web): add an "other" answer with a free-text field to the survey

The eight choices cover what users already tell us most often, but they cannot
cover what we have not heard yet β€” and the people whose problem is not on the
list are exactly the ones worth hearing from. Picking "Something else" opens a
text field instead of finishing the survey.

The cost is paid only by the people who need it: everyone whose answer is on
the list still finishes in one tap. This is the open-ended question we cut
earlier, put back behind a door rather than in everyone way.

Wire shape follows PostHog open-choice convention β€” the response recorded is
the text they typed. Picking "Something else" and typing nothing still reports
the choice itself, because "none of these fit" is an answer and dropping it
would turn those people into non-responders.

Skip disappears once the choice is made; it would only have been a second way
to submit.

* style(web): use a neutral focus border on the survey text field

The green ring competed with the brand fill on the selected chip directly
above it β€” two green things one under the other, neither reading as primary.
A neutral border marks focus without joining that fight, and the inset fill
still separates the field from the card.

* fix(web): keep the survey submit button on the footer right edge

Submit replaces Skip in the footer, but only Skip carried the margin-left:auto
that pushes it right β€” so the moment the text field opened, the primary action
collapsed back against the step counter and jumped from the right edge to the
left. Measured from the card edge, both now sit at the same 17px gap.

* feat(web): ask the experience survey after a delivered artifact, not an export

The post-export survey could only reach people who exported. Over 30 days
~13k users exported while ~37k produced an artifact, so roughly two thirds
of the people who got real work out of the product were never asked β€” and
the ones who tried it, got something, and left without exporting are the
ones worth hearing from most.

The card is now armed by a delivered design run (resolveDesignDeliveryOutcome
=== 'delivered'), from the second such delivery on. The first delivery
measures a first impression rather than an experience, and lands at the one
moment the user most wants to look at what they just got; waiting for the
second costs almost no reach because deliveries repeat (~13 per user per 30
days).

Because a delivery is usually mid-session rather than end-of-session, the
3s delay now bails out if the user starts writing their next prompt.
'beforeinput' rather than 'keydown' so IME composition counts as typing.
A dropped chance is taken by the next delivery, so the card lands after the
turn the user actually stopped on.

Export no longer arms anything; the three FileViewer call sites are gone.
Every survey event carries trigger: 'post_generation' so the two regimes
stay distinguishable in the events table.

* feat(web): arm the experience survey on the first delivered artifact

Drops the two-delivery threshold to one. Waiting for a second run bought a
more considered opinion at the cost of everyone who produces one artifact
and leaves β€” the users we understand least, and the ones this survey was
widened to reach in the first place.

Asking on the first delivery is affordable because the card is not an
interruption: the component still waits out the delay and still drops the
chance entirely if the user starts typing, so an early ask that lands badly
costs a dropped chance rather than a derailed session.

The delivery counter stays. It is the seam the policy is made of, and it is
what makes an unwritable store read as "not yet qualified" β€” a store that
cannot write cannot record a dismissal either, so without it the card would
come back after every run with no way to stop it.



* fix(web): retire the experience survey when it is shown, not when it is answered

One ask per user was the intent, but `retireSurvey()` only ran from `finish`
and `close`. Ignoring the card β€” the most common way to decline a prompt β€”
left the flag unset, so the card returned after the user's next artifact, and
the one after that. `exposedRef` did not cover this: it is a ref, so it resets
on the next page load.

Retiring on exposure makes the rule what it claims to be: a user sees this
card at most once, whatever they do with it. A chance dropped by the typing
bail-out is still retried, because a card the user never saw did not spend
their one ask.



* refactor(web): drop the typing bail-out from the experience survey

The card no longer cancels itself when the user starts typing during the
three-second delay. With one ask per user, a dropped chance is the whole ask
for anyone whose habit is to type straight into the next prompt β€” and that
habit belongs to the engaged users this survey most wants to hear from.

Removes the `beforeinput` listener and the IME reasoning that went with it.
The delay stays: it keeps the card from animating in on top of the artifact,
which was always its other job.



* fix(web): give the survey's free-text field an accessible name

Review finding from #7074, folded in with that branch. The textarea is
auto-focused the instant "Something else" is picked, so its accessible name
is the only thing telling a screen-reader user what the field is for β€” and it
had none. A placeholder is not a label and disappears on the first keystroke.

Reuses the placeholder string rather than adding a twentieth translation of
the same sentence, so the visible and announced names cannot drift.



* chore: ensure PR CI

---------





(cherry picked from commit 713caf0)

Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
Co-authored-by: Eli <qiongyu1999@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: open-design-crew[bot] <4197780+open-design-crew[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-validation Runtime change detected; needs human or /explore agent validation. risk/medium Medium risk: regular code changes size/M PR changes 100-300 lines type/feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants