fix: onboarding and dashboard UX improvements - #1676
Conversation
- Use free model (auto + free_models_only) in Quick Start snippets to match "Try it now" section - Replace empty dashboard chart with Get Started card when user has < 5 API calls - Delay invite banner until 7+ days, 50+ calls, or credits purchased - Acknowledge onboarding test call in dashboard state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WalkthroughModifies dashboard UI to display a Get Started card for new users with totalRequests below 5, introduces activity tracking via localStorage, restructures Quick Actions component rendering with Button/Link composition, updates sidebar CTA eligibility logic based on organization age and activity flags, and changes Quick Start code snippets to use "auto" model with free_models_only configuration. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.
In `@apps/ui/src/components/dashboard/dashboard-client.tsx`:
- Line 441: The UI shows the "Get Started" card when totalRequests < 5 but
totalRequests is derived from activityData filtered by the selected from/to
range, so users with lifetime usage outside that range still see the card;
update the logic to use a lifetime request count or persisted flag instead of
the range-filtered totalRequests: fetch or read a lifetimeRequests (or reuse the
existing user_has_50_plus_calls-style flag) and change the conditional in the
dashboard rendering (the {!isLoading && totalRequests < 5 ? ...} branch) to
check lifetimeRequests or the persisted flag so the card is only shown for truly
new users.
- Around line 146-150: The effect currently sets localStorage key
"user_has_50_plus_calls" based on date-scoped totalRequests, which misses users
with 50+ lifetime calls when they view a narrow range; change the check inside
the useEffect to use the lifetime (unfiltered) request count instead of the
date-filtered totalRequests — e.g., use a prop/state like lifetimeRequests,
totalRequestsAllTime, or compute from the unfiltered requests array and fall
back to totalRequests if the lifetime value is not available; update the
useEffect (and its dependency list) to reference that lifetime identifier so the
flag reflects 50+ API calls across all time rather than only the selected date
window.
- Around line 490-519: The Quick Actions JSX is duplicated; extract it into a
small local React component or variable (e.g., QuickActionsCard) that renders
the Card with CardHeader/CardContent and maps quickActions to the Button+Link
elements, reusing helpers buildOrgUrl and buildUrl and action.icon/action.label;
then replace the repeated blocks in both branches with a single
{QuickActionsCard} reference to eliminate duplication.
In `@apps/ui/src/components/dashboard/dashboard-sidebar.tsx`:
- Around line 649-686: The hook useInviteBannerEligible currently only depends
on selectedOrganization so it never re-checks when the localStorage flag
"user_has_50_plus_calls" is set later; update the hook to listen for changes to
that flag (e.g., add a storage event listener or read the flag into React state
and include it in the effect dependency array) so the eligibility recalculates
in-session when dashboard-client.tsx writes the flag, and stabilize the
selectedOrganization dependency by depending only on the specific fields used
(e.g., selectedOrganization.id, selectedOrganization.createdAt,
selectedOrganization.credits) or derive a memoized object to avoid unnecessary
re-runs.
- Around line 674-680: The code reads
localStorage.getItem("user_has_50_plus_calls") inside the useEffect to
setEligible, which violates the guideline to use cookies for non-DB user
settings; update the logic to read the "user_has_50_plus_calls" cookie instead
of localStorage in the useEffect (and ensure any code that previously wrote to
localStorage now sets the same cookie), keeping the same boolean check and
calling setEligible(true) when the cookie equals "true"; reference the existing
symbols localStorage -> cookie, "user_has_50_plus_calls", useEffect,
setEligible, and eligible so you can locate and replace the storage reads/writes
consistently across the codebase.
🧹 Nitpick comments (3)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/dashboard/dashboard-client.tsx`: - Around line 490-519: The Quick Actions JSX is duplicated; extract it into a small local React component or variable (e.g., QuickActionsCard) that renders the Card with CardHeader/CardContent and maps quickActions to the Button+Link elements, reusing helpers buildOrgUrl and buildUrl and action.icon/action.label; then replace the repeated blocks in both branches with a single {QuickActionsCard} reference to eliminate duplication. In `@apps/ui/src/components/dashboard/dashboard-sidebar.tsx`: - Around line 649-686: The hook useInviteBannerEligible currently only depends on selectedOrganization so it never re-checks when the localStorage flag "user_has_50_plus_calls" is set later; update the hook to listen for changes to that flag (e.g., add a storage event listener or read the flag into React state and include it in the effect dependency array) so the eligibility recalculates in-session when dashboard-client.tsx writes the flag, and stabilize the selectedOrganization dependency by depending only on the specific fields used (e.g., selectedOrganization.id, selectedOrganization.createdAt, selectedOrganization.credits) or derive a memoized object to avoid unnecessary re-runs. - Around line 674-680: The code reads localStorage.getItem("user_has_50_plus_calls") inside the useEffect to setEligible, which violates the guideline to use cookies for non-DB user settings; update the logic to read the "user_has_50_plus_calls" cookie instead of localStorage in the useEffect (and ensure any code that previously wrote to localStorage now sets the same cookie), keeping the same boolean check and calling setEligible(true) when the cookie equals "true"; reference the existing symbols localStorage -> cookie, "user_has_50_plus_calls", useEffect, setEligible, and eligible so you can locate and replace the storage reads/writes consistently across the codebase.apps/ui/src/components/dashboard/dashboard-sidebar.tsx (2)
649-686:useInviteBannerEligiblewon't react to theuser_has_50_plus_callsflag being set later in the session.The
useEffectdepends only on[selectedOrganization]. If the user reaches 50 calls during the same session (dashboard-client.tsx writes to localStorage), this hook won't re-evaluate becauseselectedOrganizationhasn't changed. The banner will only appear on the next page load / organization switch.This may be acceptable (banner appears on next visit), but if the intent is real-time eligibility, you'd need a cross-component signal (e.g., a shared context, a custom event, or polling).
Also minor:
selectedOrganizationis an object prop — if the parent re-creates it on each render the effect will re-fire unnecessarily. Consider comparing only the fields you actually read (createdAt,credits, and the org id) via a stabilized dependency or a ref-based check.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/dashboard/dashboard-sidebar.tsx` around lines 649 - 686, The hook useInviteBannerEligible currently only depends on selectedOrganization so it never re-checks when the localStorage flag "user_has_50_plus_calls" is set later; update the hook to listen for changes to that flag (e.g., add a storage event listener or read the flag into React state and include it in the effect dependency array) so the eligibility recalculates in-session when dashboard-client.tsx writes the flag, and stabilize the selectedOrganization dependency by depending only on the specific fields used (e.g., selectedOrganization.id, selectedOrganization.createdAt, selectedOrganization.credits) or derive a memoized object to avoid unnecessary re-runs.
674-680:localStorageusage instead of cookies for cross-component state.The coding guidelines state: "Use cookies for user settings which are not saved in the database to ensure SSR works." Here
localStorageis used for theuser_has_50_plus_callsflag. Since the initialeligiblestate isfalseand the check happens insideuseEffect, this won't cause an SSR hydration mismatch — the CTA simply never renders server-side and appears after hydration when eligible. This is fine for a progressive-enhancement pattern, but be aware that if you ever need SSR-aware eligibility (e.g., to avoid layout shift), you'd need to move this to a cookie.As per coding guidelines:
apps/{ui,playground}/**/*.{ts,tsx}: "Use cookies for user-settings which are not saved in the database to ensure SSR works"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/dashboard/dashboard-sidebar.tsx` around lines 674 - 680, The code reads localStorage.getItem("user_has_50_plus_calls") inside the useEffect to setEligible, which violates the guideline to use cookies for non-DB user settings; update the logic to read the "user_has_50_plus_calls" cookie instead of localStorage in the useEffect (and ensure any code that previously wrote to localStorage now sets the same cookie), keeping the same boolean check and calling setEligible(true) when the cookie equals "true"; reference the existing symbols localStorage -> cookie, "user_has_50_plus_calls", useEffect, setEligible, and eligible so you can locate and replace the storage reads/writes consistently across the codebase.apps/ui/src/components/dashboard/dashboard-client.tsx (1)
490-519: Quick Actions card is duplicated across both branches.The Quick Actions rendering (Button + Link composition for each action) is identical in the "Get Started" branch (lines 490–519) and the "Usage Overview" branch (lines 558–587). Extract it to a local component or variable to reduce duplication.
♻️ Suggested extraction
+ const QuickActionsCard = ( + <Card className="col-span-3"> + <CardHeader> + <CardTitle>Quick Actions</CardTitle> + <CardDescription> + Common tasks you might want to perform + </CardDescription> + </CardHeader> + <CardContent className="space-y-2"> + {quickActions.map((action) => ( + <Button + key={action.href} + asChild + variant="outline" + className="w-full justify-start" + > + <Link + href={ + action.href === "provider-keys" + ? buildOrgUrl("org/provider-keys") + : buildUrl(action.href) + } + prefetch={true} + > + <action.icon className="mr-2 h-4 w-4" /> + {action.label} + </Link> + </Button> + ))} + </CardContent> + </Card> + );Then use
{QuickActionsCard}in both branches instead of repeating the markup.Also applies to: 558-587
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/dashboard/dashboard-client.tsx` around lines 490 - 519, The Quick Actions JSX is duplicated; extract it into a small local React component or variable (e.g., QuickActionsCard) that renders the Card with CardHeader/CardContent and maps quickActions to the Button+Link elements, reusing helpers buildOrgUrl and buildUrl and action.icon/action.label; then replace the repeated blocks in both branches with a single {QuickActionsCard} reference to eliminate duplication.
Summary
gemini-3-flash-preview(paid) tomodel: "auto"withfree_models_only: true, matching the "Try it now" section so new users' first self-initiated API call succeeds without creditsTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Improvements