feat(dashboard): zero-friction onboarding page#3
Merged
Conversation
Adds /onboarding route shown immediately after native registration. The page renders inside the standard authenticated app shell (sidebar retained) but uses a slim header that only carries a 'Skip to Dashboard' affordance. Three interactive steps: 1. Live API key — calls the existing useCreateApiKey hook on mount so the user sees a real key (not a placeholder) and can copy it. 2. Workload runner — async vs batch toggle, browser vs CLI execution toggle, Python vs cURL language toggle. Browser mode pre-renders the payload and a Run Now button that uploads + creates a real batch via useCreateBatch / useUploadFileWithProgress while still cycling through idle → running → success on a 2.5s timer for predictable UX. CLI mode shows a 'Listening for request…' indicator that flips to success on click. Both flows redirect to /models 2s after success. 3. Team invite — POSTs the supplied email to the configured Zapier hook (no-cors, fire-and-forget) and surfaces a sent confirmation. Also fires a background 'Hello World' batch + toast on mount so users see data on the dashboard when they land. The post-signup default redirect from RegisterForm is now /onboarding; explicit ?redirect= and the server-driven onboarding_redirect_url still take priority. Co-authored-by: aschkanAH <aschkanAH@users.noreply.github.com>
mode: 'no-cors' silently downgrades Content-Type: application/json to text/plain because JSON isn't CORS-safelisted. Zapier Catch Hooks only auto-parse JSON when the request actually carries an application/json content-type, so the previous code was firing the webhook successfully but with all fields empty. Switching the body to URLSearchParams produces an application/x-www-form-urlencoded body (which IS CORS-safelisted) and which Zapier does parse into structured fields. Co-authored-by: aschkanAH <aschkanAH@users.noreply.github.com>
Replaces the hardcoded Zapier hook with VITE_INVITE_WEBHOOK_URL. The invite section is hidden entirely when the variable is unset so we don't collect emails with nowhere to send them; the submit handler also has a defensive guard for stale builds. Co-authored-by: aschkanAH <aschkanAH@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f8ee3a7. Configure here.
…gate race
Three fixes from review:
1. buildAsyncPayload, buildJsonlPayload, and the snippet generators were
interpolating the model alias into JSON / Python / cURL string
literals via raw template literals. Catalog aliases come from user-
controlled DB metadata and may legitimately contain quotes,
backslashes, or control characters, which would yield invalid JSON.
handleRunNow's JSON.parse(buildAsyncPayload(...)) would throw on such
aliases, yet the simulated 2.5s timer would still flip the UI to
'success' and redirect — falsely telling the user a workload was
queued.
Payloads are now built as objects and serialized via JSON.stringify
(JSONL/JSON cases), and code-snippet interpolations go through
escapeForLiteral which uses JSON.stringify's inner string form to
correctly escape both Python and shell-string literal special chars.
2. The CLI listener block exposed '(Click to simulate success)' to
desktop users. 'Simulate' is internal language that breaks the
illusion of the onboarding flow. Replaced with '(Click to continue)'
and updated the aria-label to match.
3. RegisterForm called navigate('/onboarding') unconditionally after
register(). When the server returns onboarding_redirect_url,
AuthProvider.checkAuthStatus has already initiated a hard navigation
via window.location.href — the new client-side push raced that and
could briefly mount the wrong route before the hard nav resolved.
We now read the just-populated current-user cache entry and skip
navigate() when an onboarding_redirect_url is present, deferring
cleanly to the server-driven redirect (matching the pre-onboarding
behaviour for that path).
Co-authored-by: aschkanAH <aschkanAH@users.noreply.github.com>
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.

Summary
Adds a
/onboardingroute shown immediately after native registration. The page renders inside the existing authenticated app shell (sidebar retained) but uses a slim header that only carries a "Skip to Dashboard" affordance.Headline: "From zero to inference in seconds."
Subheading: "Your workspace is fully provisioned. Run a sample payload right from your browser to see Doubleword in action."
Routing & layout
/onboarding, gated byProtectedRouteso unauthenticated users get bounced to login.<AppLayout>because the spec asks for a header that only contains "Skip to Dashboard". Instead the component composesSidebarProvider+AppSidebar+SidebarInsetdirectly so the standard sidebar is retained but the header is bespoke.RegisterFormpost-signup landing logic is layered:?redirect=(e.g. org invite acceptance) wins.onboarding_redirect_url(when the user cache entry populated byAuthProvider.checkAuthStatuscarries one) —AuthProviderinitiates a hardwindow.location.hrefredirect for that case, so we deliberately do not callnavigate()in the form handler to avoid racing the pending hard nav./onboardingfor the zero-friction flow./modelsviareact-routerrather thanapp.doubleword.ai/modelsso this works on self-hosted/non-prod origins.Interactive sections
useCreateApiKeyhook to mint a realrealtimekey againstcurrentUser.id. Failures are surfaced inline (with a hint to use the API Keys page) and don't block the rest of the flow. Copy uses the sharedcopyToClipboardutil and flips to a checkmark for 2s.idle → running → successon a 2.5s timer (per spec). Behind the scenes it fires a real upload +useCreateBatchso a job actually appears in the dashboard the user is about to land on. The timer-based UX path is independent of the network call so the redirect is predictable even when the upload is slow./models.JSON.stringify; model alias and API key are escaped viaJSON.stringifyinner-string form before being interpolated into Python and cURL snippets, so aliases containing quotes / backslashes / control characters do not produce malformed code.VITE_INVITE_WEBHOOK_URL(typically the Growth team's Zapier Catch Hook). The form is hidden entirely when the env var is unset, so no environment silently drops invite submissions. POST is fire-and-forget undermode: 'no-cors'because the Zapier host doesn't return CORS headers; body is sent asapplication/x-www-form-urlencodedviaURLSearchParamsrather than JSON, because no-cors silently downgrades non-safelisted content-types and Zapier won't auto-parsetext/plaininto structured fields.Background "Hello World" batch & toast
On mount, an info toast ("Sample Batch Started…") is fired with a 6s duration via the existing
sonnertoaster. In parallel, a one-row JSONL is uploaded and a batch is created via the same hooks the dashboard uses elsewhere, so the user actually has data to look at after they land on/models. Both the toast and the background job are gated by an idempotency ref so React StrictMode double-invokes don't double-fire.The shared
Toasterwas also fixed in this PR: the existing wiring forwarded the bare HSL-component shadcn tokens (var(--popover)resolves to0 0% 100%) into sonner's--normal-bg, producing invalid CSS and falling through to sonner's translucent default. This made every toast in the app illegible when overlaying content. Tokens are now wrapped inhsl(...)so the resolved background is a real opaque color.Configuration
VITE_INVITE_WEBHOOK_URLDesign system compliance
Button,SidebarProvider,SidebarInset,SidebarTrigger, and theAppSidebardirectly.doubleword-*color tokens (doubleword-primary,doubleword-red-50/-300/-light/-dark,doubleword-text-*,doubleword-border*,doubleword-neutral-*) for all branded surfaces. The Async card and primary CTA pick up the doubleword red; Batch keeps the existing blue accent for differentiation.QA
pnpm run lintclean.pnpm exec tsc -b tsconfig.app.jsonclean.pnpm run buildsucceeds.pnpm test -- --run: 498 / 498 passing.Notes / risks
no-cors); we can't detect transport failures from the browser./models./onboarding. Combined with the "won't be shown again" copy, that's intentional for the first-visit flow but means revisiting the page for any reason will mint a new throwaway key. If we want to avoid that, follow-up work should gate this behind the sameonboarding_completed_${userId}localStorage flag the auth context already uses.