Commit 4f5bdee
feat(web): new-user personalized onboarding — Home recommendation + Studio first-gen hint (#5111)
* feat(web): personalized first-run recommendation on Home + Studio handoff
After a new user finishes the first-run questionnaire, Home shows one
compact, personalized starting point (role/use-case -> product path via a
static mapping) instead of the generic empty entry. Clicking it opens
Studio with the recommended first request pre-filled in the composer
(not auto-sent); the generic quick-start cards are suppressed while the
composer carries content. Session-only, no persistence: the recommendation
is memory-scoped and gone on refresh or for returning users.
Adds the first-generation funnel events (onboarding_first_prompt_sent,
onboarding_first_generation_completed) plus the recommendation
surface_view / ui_click, all via the v2 analytics convention. i18n keys
across all 19 locales.
Spec: OD new-user personalized onboarding SS6-7, S11.
* feat(web): one-time first-generation hint in Studio
When a new user's first previewable artifact appears in Studio, show a
single lightweight, dismissible note pointing them at view / edit /
export. Shown at most once ever (localStorage), non-modal, in the preview
corner so it never stacks with the post-turn next-step card (spec S8.5,
one guide at a time). Fires onboarding_first_artifact_hint surface_view /
ui_click via the v2 analytics convention.
Spec: OD new-user personalized onboarding S8.3.
* style(web): use real tokens in onboarding CSS modules
--accent-contrast and --shadow don't exist in the token set; the fallbacks
were silently in effect. Use the repo's plain-white-on-accent convention and
the real --shadow-md elevation token (light/dark adaptive) instead.
* feat(web): onboarding_prompt_prefilled event + is_first_session super property
Completes the spec S11.1 funnel middle step: fire
onboarding_prompt_prefilled once when Studio mounts with the recommended
first request actually seeded in the composer (confirming the Home ->
Studio handoff), carrying role / use_cases / product_type /
recommendation_id through the session-only entry slot. Register
is_first_session as a PostHog super property, pinned to the install's
first-ever session id in localStorage.
* fix(web): recommendation strip hover states + give browse-all a destination
The global button:hover:not(:disabled) rule in primitives.css out-specifies
plain .class:hover module rules, repainting the primary CTA to --bg-subtle
(white label on near-white) and forcing a gray wash on the text actions.
Match its specificity per the existing onboarding-cloud pattern: primary
keeps its accent fill on hover; the text actions get a soft white wash
instead of gray, and the underline is dropped.
Browse-all previously just dismissed the strip with no feedback. It now
also opens the New-project template picker — the concrete 'all types'
catalogue the label promises (spec S7.4 通用选择).
* fix(web): unify hover treatment of the two text actions on the recommendation strip
换一个 hovered orange while 浏览全部类型 stayed neutral — as peers with the
same pill wash they now share one hover behavior (white wash + text
darkens); only their resting text color carries the hierarchy.
* fix(web): recommendation strip — card width, placed under the composer
Full-page width below the template section read as an auxiliary notice bar
at the bottom of Home — the one surface a brand-new user is meant to see
first was the hardest to find. HomeHero gains an optional recommendationSlot
rendered directly under the composer card (before the template section);
the strip is constrained to the composer column (720px, centered) so it
reads as a card in the same visual stack.
* feat(web): path-scoped starter templates in Studio for recommendation projects
The empty conversation above a pre-filled composer hid the quick-start
cards entirely, leaving dead space; and the generic three cards are
unrelated to what the user just told us they want. A recommendation-started
project now shows its own product path's starters (e.g. prototype /
component / low-fi for product_ui) as one-click composer replacements —
clicking overwrites the draft. Generic projects and the general fallback
keep the existing set. Supersedes the hide-when-prefilled gate from spec
S8.5: with replace-on-click semantics the cards complement the pre-filled
request instead of competing with it, so onDraftEmptyChange plumbing and
its regression test are removed. Starter copy moves to a shared module
(onboarding/starter-copy.ts) reused by the Home strip.
* fix(web): icon-only cycle button on the recommendation strip
The text 换一个试试 button crowded the 720px card and pushed 浏览全部类型
onto its own wrapped line. It is now an icon-only refresh button with the
label in title/aria-label (zh copy shortened to 换一换); all three actions
fit one row.
* fix(web): tighten recommendation strip copy across locales
Primary CTA drops the Studio jargon (在 Studio 中开始 -> 开始创作 / Start
creating) and the fallback shortens to 全部类型 / All types; both updated
across all 19 locales.
* fix(web): primary CTA sits rightmost on the recommendation strip
Action order is now cycle icon, 全部类型, then 开始创作 — the standard
primary-on-the-right reading order.
* feat(web): attention entrance for the recommendation card
The old 200ms fade rode in with the page load and read as furniture. The
card now enters 350ms after mount — motion-after-stillness, the only thing
moving once Home settles — then a one-time accent glint sweeps it twice,
mirroring HomeHero's attention-sheen invitation language. Both animations
respect prefers-reduced-motion.
* fix(web): recommendation card motion + hierarchy — push-down reveal, CTA glint, tinted primary
Entrance is now the repo's canonical accordion reveal: the slot stays
collapsed for 600ms, then expands (grid-rows 0fr -> 1fr) and pushes the
template section down — displacement as the attention cue, replacing the
card-wide sheen that read oddly on an optional surface. A one-time tinted
glint then sweeps the CTA (the same click-me-next language home-hero puts
on the send button).
开始创作 demotes from a solid accent fill to the tinted accent-secondary
(NextStepActions Share vocabulary): the composer's 发送 stays the screen's
one filled primary; first-time attention is carried by motion, not fill
weight. The shown->clicked funnel can re-promote it later if data says so.
* fix(web): drop the hover state on starter-card return glyph
The accent-filled circle on hover reads as 'this will send' — the cards
only fill the composer. Keep the glyph static; the card hover itself is
the affordance.
* fix(web): spend the first-artifact hint budget on dismiss, not on show
Marking seen at mount meant any parent-gate flicker (files refresh /
streaming blip unmounting and remounting the component) burned the
once-ever hint before anyone could read it — reproduced live in the
desktop app. The budget is now spent by the user's own close action;
remounts before dismissal simply show the hint again, matching the spec's
'visible until closed or used'.
* fix(web): anchor the first-artifact hint under the preview toolbar
Bottom-right corner had zero directionality (and sat on the pet widget) —
the hint talks about view/edit/export but pointed at nothing. It now sits
directly below the artifact toolbar with an upward caret aimed at the
share/download cluster, and slides down from it on entry. Offsets track
the stable chrome heights, not preview internals.
* feat(web): settle-then-bob entrance for the first-artifact hint
600ms delay lets the freshly-opened preview settle; the slide-down then
ends in two decaying bobs (4px, 2px) — a nudge that says 'I appeared'
without a glint, sized for a persistent hint rather than a toast.
* feat(web): motion-driven drop-and-knock entrance for the first-artifact hint
Fully opaque throughout — the 600ms settle window is a delayed mount, not
an opacity fade (a see-through frame made the copy unreadable). The card
then drops 28px from the toolbar and sets down (easeOut), holds a beat,
and gives two equal 4px knocks with a short pause between them (easeInOut
segments). Driven by motion/react (already a dependency; ProjectView uses
AnimatePresence) with useReducedMotion opting the whole sequence out.
* test(web): export useReducedMotion from the motion/react mock
FirstArtifactHint is the first component to call it; without the export
every jsdom test whose project fixtures include an .html file crashed the
whole ProjectView render.
* fix(web): first-artifact hint must never occlude or intercept the viewer toolbar
The hardcoded top offset landed the card ON the preview's action toolbar in
layouts with taller tab chrome, blocking comment/zoom clicks (caught by the
Playwright critical suite). Two independent fixes: the card now anchors
below the lowest visible .viewer-toolbar, measured imperatively via ref
(state-driven repositioning re-rendered mid-flight and froze the motion
keyframes) with re-measures while the project chrome settles; and the card
is pointer-events: none (dismiss button excepted), so even a transient
overlap can never swallow toolbar clicks.
* fix(web): first-artifact hint copy leads with editing
At the first-artifact moment the loop's next step is modify, not deliver
(spec S8.3 orders 查看 -> 修改 -> 导出; export already has the next-step
card's Share and the export-ready nudge). Body copy now leads with edit /
annotate and closes with export-when-happy, matching the caret that points
at the view/edit toolbar. All 19 locales.
* fix(web): address onboarding review feedback (PR #5111)
- contracts: re-add `amr_auth_result` / `file_version_restore_result` to the
`AnalyticsEventName` union (they were present in `AnalyticsEventPayload` but
missing from the name union) and add a compile-time drift guard so the two
can never diverge again.
- studio: gate the one-time first-generation hint on the consumed
`onboardingEntryRef` so it only shows for projects started from the Home
recommendation, not for returning users opening an existing HTML project.
- analytics: make `isFirstSession()` a pure read and defer pinning the
first-session marker to the consented analytics-init path
(`pinFirstSessionForCapture`), so an install that boots with analytics off
and opts in later still records its real first analytics session as first.
- onboarding: stash the Home→Studio handoff entry only for a successful
create/navigation; clear it when the create fails or is aborted so a later
unrelated project mount can't consume the stale slot and mis-attribute
itself as recommendation-started.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): address onboarding review follow-ups (PR #5111)
- analytics: centralize the first-analytics-session pin in `getAnalyticsClient`
(the single consent gate every caller funnels through), so the opt-in-later
path — booting with metrics off then enabling them mid-session via
`setConsent(true)` — also records the real first analytics session as first.
Adds `analytics-first-session-consent` regression coverage.
- onboarding: canonicalize `useCases` against questionnaire display order in
`resolveProductType` / `buildRecommendation`, so a set of answers resolves to
the same product bucket (and echoes the same telemetry) regardless of the
chip click order. Adds selection-order-independence tests.
- onboarding: create the project FIRST in `handleRecommendationStart` and only
dismiss the Home recommendation once it actually opened; a failed/aborted
create now keeps the recommendation mounted so the user can retry instead of
dropping to a dead-end generic Home. Adds `RecommendedStartRegion.handoff`
reject-path coverage.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): report has_prefilled_prompt from real send state (PR #5111)
The onboarding_first_prompt_sent funnel event hard-coded has_prefilled_prompt
to true for every recommendation-started project, but the user can edit, clear,
replace, or starter-swap the prefilled composer before sending. Snapshot the
seed prompt at mount and compare it against the actually-sent prompt via a new
`sentPrefilledPrompt` invariant helper, so the send-through split reflects
whether the suggestion was really used. Adds unit coverage for the
unmodified / edited / cleared / no-seed cases.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): pin first analytics session only after client init succeeds
pinFirstSessionForCapture() ran right after the config check but before
import('posthog-js') / posthog.init(), so a boot where init threw returned null
from the catch while the marker was already burned — a later real session would
then report is_first_session=false despite never capturing. Move the pin to the
point where init is known to have succeeded (after client = posthog, before
return), and add a "config enabled but client init fails" regression case.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): key onboarding handoff by project id; gate first-gen on artifact
Address two review findings on PR #5111:
- The Home→Studio onboarding entry was stashed in a single global
sessionStorage slot before create resolved, racing any concurrent
project navigation: an unrelated project mounting mid-create could
consume the slot and steal the personalized starter/hint/funnel
context. Key the handoff by the created project id and move the stash
into the create success path (App.handleCreateProject), where the id is
finally known. A failed/aborted create now leaves nothing behind, and a
concurrent mount reads its own (absent) key.
- onboarding_first_generation_completed fired on any succeeded run, even
one producing only text or a clarifying question, overstating the funnel
and diverging from the first-artifact hint. Gate it on the same
artifact-producing condition the hint uses (a produced previewable
.html), via a new producedPreviewableArtifact invariant.
Tests: keyed per-project isolation (race regression), handoff no longer
touches sessionStorage, and producedPreviewableArtifact incl. a succeeded
run with no artifact.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): StrictMode-safe onboarding handoff, visible start failure, gate hint impression
Address three review findings on PR #5111:
- consumeOnboardingEntryForProject is a destructive read called from
ProjectView render; under reactStrictMode the throwaway first mount
consumed the sessionStorage slot and the committed remount saw null,
dropping every recommendation-only behavior in dev. Cache the first
parsed entry per project id in the helper so both StrictMode passes (and
later remounts) get the value while the slot is still cleared on first
read. Adds a double-consume regression test.
- EntryShell.handleRecommendationStart swallowed create failures in a
catch and RecommendedStartRegion discarded the promise, so a failed
"Start creating" was a silent no-op. Let the failure propagate; HomeView
now routes it into the same error channel other entry actions use
(setError + new home.recommendation.startFailed string in all locales),
and RecommendedStartRegion awaits with a pending state that re-enables
for retry on failure.
- FirstArtifactHint fired its surface_view impression before `settled`,
while the card still rendered null, counting mounts removed during the
600ms settle window as impressions the user never saw. Gate the effect
on settled. Adds an impression-timing regression test.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): make onboarding funnel guards project-scoped and gate first-prompt after pre-run gates
Address two review findings on PR #5111:
- The once-per-project funnel guards (first_prompt_sent /
first_generation_completed) were mount-local refs. Now that the
onboarding entry survives ProjectView remounts via the project-id cache,
reopening a project reset the refs while the entry kept returning, so
both events could re-fire on a later conversation/run. Move the guards
into project-keyed module flags beside the entry cache so they hold
across remounts (within the session, which is the entry's lifetime).
- onboarding_first_prompt_sent was emitted before the queue-only / busy /
AMR balance pre-run gates, which can abort the send without creating a
run. A hard AMR gate then showed the sign-in/recharge dialog while the
event had already fired and the once-only guard suppressed the real
retry. Move the tracking below those gates, to the point the send is
committed to creating a run.
Adds regression coverage for the project-scoped guards (survive remount,
per-id isolation).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): cache onboarding seed prompt with the entry so it survives reopen-before-send
Address review on PR #5111: onboardingSeedPromptRef was rebuilt only from
project.pendingPrompt, which onClearPendingPrompt wipes on the first mount.
Since the onboarding entry now survives ProjectView remounts via the
project-id cache, a user who accepts a recommendation, opens Studio, leaves
before sending, then reopens the same project in-session kept the entry but
lost the seed — so onboarding_first_prompt_sent reported
has_prefilled_prompt=false even when they sent the original recommended draft,
undercounting the accepted-recommendation path.
Cache the seed prompt WITH the entry (new OnboardingEntry.seedPrompt, stashed
at create time in App.handleCreateProject). ProjectView prefers the cached
seed and falls back to pendingPrompt. Adds a reopen-before-send regression.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>1 parent 38c20ef commit 4f5bdee
56 files changed
Lines changed: 3361 additions & 14 deletions
File tree
- apps/web
- src
- analytics
- components
- i18n
- locales
- onboarding
- styles/viewer
- tests
- components
- helpers
- packages/contracts/src/analytics/events
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
12 | 16 | | |
13 | 17 | | |
14 | 18 | | |
| |||
140 | 144 | | |
141 | 145 | | |
142 | 146 | | |
| 147 | + | |
143 | 148 | | |
144 | 149 | | |
145 | 150 | | |
| |||
1576 | 1581 | | |
1577 | 1582 | | |
1578 | 1583 | | |
| 1584 | + | |
| 1585 | + | |
| 1586 | + | |
| 1587 | + | |
| 1588 | + | |
| 1589 | + | |
| 1590 | + | |
| 1591 | + | |
| 1592 | + | |
| 1593 | + | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
| 1600 | + | |
1579 | 1601 | | |
1580 | 1602 | | |
1581 | 1603 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
25 | 30 | | |
26 | 31 | | |
27 | 32 | | |
| |||
337 | 342 | | |
338 | 343 | | |
339 | 344 | | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
340 | 349 | | |
341 | 350 | | |
342 | 351 | | |
| |||
363 | 372 | | |
364 | 373 | | |
365 | 374 | | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
366 | 384 | | |
367 | 385 | | |
368 | 386 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
76 | 80 | | |
77 | 81 | | |
78 | 82 | | |
| |||
131 | 135 | | |
132 | 136 | | |
133 | 137 | | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
134 | 141 | | |
135 | 142 | | |
136 | 143 | | |
| |||
258 | 265 | | |
259 | 266 | | |
260 | 267 | | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
261 | 298 | | |
262 | 299 | | |
263 | 300 | | |
| |||
1082 | 1119 | | |
1083 | 1120 | | |
1084 | 1121 | | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
| 1128 | + | |
| 1129 | + | |
| 1130 | + | |
| 1131 | + | |
| 1132 | + | |
| 1133 | + | |
| 1134 | + | |
| 1135 | + | |
| 1136 | + | |
| 1137 | + | |
| 1138 | + | |
| 1139 | + | |
| 1140 | + | |
| 1141 | + | |
| 1142 | + | |
| 1143 | + | |
1085 | 1144 | | |
1086 | 1145 | | |
1087 | 1146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
59 | 104 | | |
60 | 105 | | |
61 | 106 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| |||
166 | 167 | | |
167 | 168 | | |
168 | 169 | | |
| 170 | + | |
| 171 | + | |
169 | 172 | | |
170 | 173 | | |
171 | 174 | | |
| |||
188 | 191 | | |
189 | 192 | | |
190 | 193 | | |
| 194 | + | |
191 | 195 | | |
192 | 196 | | |
193 | 197 | | |
194 | 198 | | |
195 | 199 | | |
196 | 200 | | |
197 | 201 | | |
| 202 | + | |
198 | 203 | | |
199 | 204 | | |
200 | 205 | | |
| |||
252 | 257 | | |
253 | 258 | | |
254 | 259 | | |
| 260 | + | |
255 | 261 | | |
256 | 262 | | |
257 | 263 | | |
| |||
310 | 316 | | |
311 | 317 | | |
312 | 318 | | |
| 319 | + | |
313 | 320 | | |
314 | 321 | | |
315 | 322 | | |
| |||
350 | 357 | | |
351 | 358 | | |
352 | 359 | | |
| 360 | + | |
353 | 361 | | |
354 | 362 | | |
355 | 363 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
| |||
95 | 97 | | |
96 | 98 | | |
97 | 99 | | |
| 100 | + | |
98 | 101 | | |
99 | 102 | | |
100 | 103 | | |
| |||
510 | 513 | | |
511 | 514 | | |
512 | 515 | | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
513 | 520 | | |
514 | 521 | | |
515 | 522 | | |
| |||
796 | 803 | | |
797 | 804 | | |
798 | 805 | | |
| 806 | + | |
799 | 807 | | |
800 | 808 | | |
801 | 809 | | |
| |||
1029 | 1037 | | |
1030 | 1038 | | |
1031 | 1039 | | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
1032 | 1053 | | |
1033 | 1054 | | |
1034 | 1055 | | |
| |||
2209 | 2230 | | |
2210 | 2231 | | |
2211 | 2232 | | |
2212 | | - | |
| 2233 | + | |
2213 | 2234 | | |
2214 | 2235 | | |
2215 | 2236 | | |
| |||
2232 | 2253 | | |
2233 | 2254 | | |
2234 | 2255 | | |
2235 | | - | |
| 2256 | + | |
| 2257 | + | |
| 2258 | + | |
2236 | 2259 | | |
2237 | 2260 | | |
2238 | 2261 | | |
| |||
0 commit comments