Skip to content
Documentation
Agent skills

osr-product-experience

Agent skills 9 min read

Design and build the user experience of the OpenSmartRoute web app and dashboard under platform/web - screens, user flows, navigation placement, page layouts, components and design tokens so that anyone can use the product without reading the docs. Redesign or simplify a dashboard page, the playground, the signup and login forms, the pricing page or the operator console; put the right action in the right place; write clear empty, loading and error states; keep the UI clean, consistent, responsive on a phone and accessible; make the interface prove the platform is real by showing live data and traces instead of claims. Understand user behaviour with empathy - what a first-time visitor, a developer wiring the API, a workspace admin and an operator each try to do, where they hesitate and why. Validate every change with vitest, Playwright e2e journeys, the axe accessibility audit and the responsive overflow guard. Use when changing UI, UX, screens, components, flows, copy or usability in platform/web.

Package
.claude/skills/osr-product-experience
Compatibility
OpenSmartRoute repository, Node >= 22, Next.js 16, React 19, Tailwind 4, Playwright
License
Apache-2.0
Domains
coding marketing data_analysis general
Quality prior
0.85
Tags
opensmartroute ux ui design screens flows components accessibility responsive playground dashboard nextjs

Install by copying .claude/skills/osr-product-experience/ into the skills folder of your coding assistant. To load every package as a routing target: osr --skills .claude/skills route "..." --plan.

The web app (platform/web, Next.js 16 App Router, React 19, Tailwind 4, shadcn/ui primitives, Recharts) is the place where a person decides whether OpenSmartRoute is real, whether it is for them and whether they can trust it with traffic. This skill is the playbook for changing what they see. Read osr-platform first for the build, proxy and deployment mechanics; this skill is about what goes on the screen, where, and how to prove it works. For onboarding, activation and growth measurement see osr-customer-success; for words and colours see docs/BRAND.md (voice: precise, calm, evidence-led - "routes", "decides", "learns", never "magically").

Who is on the other side of the screen

Design every page for one of these people first and name them in the PR. Each has a different question, a different first screen and a different definition of "it works".

PersonArrives atWants to know in 30 sFirst successWhere they hesitate
Evaluator (CTO, platform lead)/, /pricing, /compare, /rankings, /statusIs this real, who runs it, what does it save, what does it cost, is it upSees live numbers (LiveStats, /rankings, /models), the readiness checks on /status and a trace, not adjectivesMarketing claims without evidence, a pricing page that hides limits, no status
Developer (wires the API)/platform/signup, /docs/QUICKSTART_PLATFORM, /platform/playgroundHow do I send one request and see why it was routedPOST /api/v1/route answers and the DecisionPanel explains the choiceA key they cannot find again, an unclear error, execute failing because no provider is connected
Workspace admin/platform/dashboardAre we within quota and budget, who is using it, what did it costThe overview StatCards, /dashboard/usage, /dashboard/savings, /dashboard/governanceA form that needs the docs, a gated feature with no explanation, a number without a unit
Operator (runs the deployment)/platform/adminIs the deployment healthy, who signed up, who is stuck, what is the margin/platform/admin/analytics, /health, /finance, /activityPages that need three clicks to answer one question, tables without links to the thing

Empathy is procedural here, not a mood: before changing a page, write the one question that person brings to it, then check the page answers it above the fold, in their words, with a real number or a real action.

Where things live

src/app/(marketing)/*            public site: /, /pricing, /compare, /models, /rankings, /marketplace, /estimate,
                                 /roi, /blog, /providers, /vendors, /support, /privacy, /terms
src/app/platform/(auth)/*        /platform/login, /signup, /forgot-password, /reset-password, /verify-email,
                                 /invite/[token], /cli/authorize, /email/preferences
src/app/platform/dashboard/*     workspace: overview, activity, handoffs, usage, savings, keys, providers,
                                 integrations, listings, governance, events, learning, health, notifications,
                                 tenants, audit, members, workspace, billing, support, account
src/app/platform/admin/(console) operator console (ADMIN_NAV in components/admin/admin-shell.tsx)
src/app/platform/playground      the route tracer (components/playground/{playground,decision-panel,compare-panel}.tsx)
src/components/ui/*              primitives: button, card (Card, CardHeader, CardBody, StatCard, Alert, Badge,
                                 Skeleton, EmptyState, Trend), field (Input, Textarea, SearchInput), table
                                 (DataTable), tabs (Tabs, SegmentedControl), dialog, sheet, drawer, toast (Sonner),
                                 tooltip (Hint), empty, code (CodeBlock, CopyButton), progress, switch (SwitchField)
src/components/charts/index.tsx  CHART_COLORS / SERIES / seriesColor (CSS vars), TimeBars, TimeArea, Bars,
                                 RankedBars, ShareBar, Sparkline, ChartEmpty, ChartTooltip
src/components/layout/*          SiteHeader, SiteFooter, Logo / BrandMarkImage, ThemeToggle, NavIcon, SearchCommand
src/components/dashboard/*       DashboardShell (SECTIONS, PageHeader), GettingStarted, UsageChart, ApiError
src/components/observability/*   TraceView / TraceLoader (the proof a decision is explainable), StatusPill,
                                 SystemStatusCard, AlertsPanel, series
src/lib/config/site.ts           site, seo, install one-liners, headerNav, dashboardNav, footerNav
src/lib/api/{client,use-api,types,proxy}.ts   api.get/post, ApiError (status, upgradeRequired, keyScope,
                                 edition, budget, walletBalanceCents, requestId, retryAfter), useApi, useAdminApi
src/lib/auth/{session,operator}.tsx           useSession (me, apiKey, signIn/out), useOperator
src/app/globals.css              tokens: --ink/--paper/--snow/--mist/--line, --brand-blue/-cyan/-violet,
                                 --status-*, --chart-1..9, text-display-{xl,lg,md,sm}, .dark, .theme-dark

page.tsx files may only export default; shared UI goes into components/. Navigation is declared in one place each: dashboardNav + SECTIONS (dashboard), ADMIN_NAV (console), headerNav / footerNav (site). A new dashboard or console page also needs its row in docs/PLATFORM.md section 13 (the docs-claims test checks every /platform/dashboard/<page> and /platform/admin/<page> documented there exists, and vice versa).

Rules for a clean screen

  1. One question per page, answered above the fold. The PageHeader title says what the page is, the description says what the numbers mean and over which window ("Usage covers the last seven days"). The first row is StatCards with a unit and a hint (hint="Only executed requests incur provider cost"), never a bare number.
  2. Right action in the right place. The primary action sits in PageHeader action= (top right); the destructive one at the bottom of its card, variant="destructive", behind a Dialog confirmation. Row actions live in the row. Never two primary buttons on one screen.
  3. Progressive disclosure, not a wall. Defaults first, the advanced block in a Collapsible / Tabs. The playground shows text + Route; objective weights, constraints, plan and execute sit behind it. Gate by plan with the same Alert pattern everywhere (ApiError.upgradeRequired -> "not in your plan" with the link to /platform/dashboard/billing), never by hiding the feature silently.
  4. Every state has a design. loading && !data -> Skeleton with the final shape; error -> Alert tone="danger" with error.message and, for 5xx, the requestId so support can find it; !data.length -> EmptyState / ChartEmpty that says what will appear here and offers the action that creates it ("No requests yet - open the playground"). A page must never render a blank card or undefined.
  5. Show the evidence. A routing decision is never a label alone: link to the trace (TraceLoader in the activity drawer, DecisionPanel in the playground), show confidence, cost and the alternatives. Savings show baseline_cost_usd next to routed_cost_usd and how the baseline was chosen. Status is a live StatusPill, not a green dot drawn in CSS.
  6. Copy is plain and specific. Sentence case, no exclamation marks, numbers with units (formatUsd, formatPercent, formatMs, formatCompact from lib/utils/format.ts), dates through formatDate. Prefer "Route a first request" to "Get started!". Name the real API call next to UI actions where a developer is the audience (the getting-started list shows POST /api/v1/route).
  7. Forms. Field-labelled inputs (a label a screen reader can name), inline validation on blur, aria-invalid, the submit button loading while pending, success through toast.success, the created secret (API key) shown once in a CodeBlock with CopyButton and a sentence saying it will not be shown again. Escape - inside pattern character classes ([A-Za-z0-9._\-], the v flag). Never Date.now() in render (React Compiler lint) - useState(() => Date.now()).
  8. Tokens only. Colours come from the CSS variables (text-ink, bg-snow, border-line, text-brand-blue, CHART_COLORS.*), never hex in a component; both themes must work (.dark on <html>, always-dark panes use .theme-dark; the brand mark through BrandMarkImage, vendor logos inside .logo-tile). Display type is text-display-* (registered in tailwind-merge extendTailwindMerge - keep it there).
  9. Responsive by construction. Grid tracks are minmax(0,1fr) (a table or <pre> otherwise widens the track; .grid > * { min-width: 0 } in globals.css is the safety net); action rows wrap; tables and code panes scroll inside their card (overflow-x-auto), never the page. Two-column detail layouts collapse below lg. Check 375 px and 768 px.
  10. Accessible by default. Every icon-only button has aria-label; every image alt; focus is visible (focus-visible:ring-[3px]); dialogs trap focus (Radix); colour is never the only signal (the Badge tone carries a word too); contrast >= 4.5 on both themes (--brand-cyan-text exists because cyan on white does not pass). Motion respects prefers-reduced-motion.

Understanding behaviour before redesigning

Do not guess where people struggle - the platform records it, without prompts:

  • Funnel and time to value - GET /api/v1/admin/analytics/activation?days= (console Analytics, card Activation funnel): the share of new workspaces that confirmed the e-mail, routed, got an answer, reported success, returned; the median hours to each. A step with a steep drop is a screen to redesign.
  • Stuck before the first request - the same payload's stuck list: who signed up more than a day ago and never routed. Read their console page (/platform/admin/workspaces/{id}): did they create a key, open the playground, hit a 4xx?
  • Where errors happen - GET /api/v1/admin/activity?failed=true and the events page; a 400 on POST /api/v1/route right after signup is a form or copy problem, not a user problem.
  • What people click on the public site - GA4 events through lib/analytics.ts track() (sign_up, login, select_content), consent-gated (ConsentBanner), never on /platform/dashboard/* or /platform/admin/* (isTrackedPath). First-touch attribution (lib/attribution.ts) lands on the signup and in the funnel's by_source.
  • Support tickets and sales inquiries (/platform/admin/support, /leads) are the qualitative signal; quote them in the PR when they motivate a change.
  • Watch someone. For anything on the first-value path (signup -> key -> playground -> activity) sit with one person who has never seen it, say nothing, note every hesitation. Five people find most of what matters.

Write the finding as observation -> hypothesis -> change -> metric that will move, and put the metric in the CHANGELOG entry ("activation funnel: first_route share").

Building a page: the recipe

  1. Data first: find or add the endpoint (osr-platform), then the TypeScript type in lib/api/types.ts with a one-line doc comment naming the route.
  2. useApi<T>("/api/v1/...") (workspace) or useAdminApi<T> (console); design the four states before the happy path.
  3. Compose from primitives: PageHeader -> StatCard row -> Card grid (lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]) -> DataTable / RankedBars / TimeBars. Rows link to the thing they describe (Link to /platform/admin/workspaces/{id}, /models/{id}).
  4. Register: nav (site.ts / SECTIONS / ADMIN_NAV) + NavIcon name, the docs/PLATFORM.md row, the CHANGELOG bullet.
  5. Prove it: unit test for any pure helper (tests/*.test.ts), e2e coverage (below), then the visual pass in both themes at 375 / 768 / 1280.

Validation - what "working" means here

cd platform/web
npm run check                 # eslint + tsc + vitest (tests/*.test.ts, .tsx)
npm run build                 # the standalone build the e2e harness serves
$env:OSR_E2E_DATABASE_URL="postgresql://osr:osr@127.0.0.1:55432/osr"   # docker start osr-test-pg
npx playwright test e2e/customer-journey.spec.ts      # sign up -> dashboard -> playground -> keys -> activity
npx playwright test e2e/accessibility.spec.ts         # axe WCAG 2.1 A/AA on the first-value pages (serious+ fails)
npx playwright test e2e/responsive.spec.ts            # no sideways scroll at 375 / 768 on every page
npx playwright test e2e/dashboard-pages.spec.ts e2e/console-pages.spec.ts   # every page renders real data, no crash
npx playwright test e2e/page-quality.spec.ts          # failed requests, console errors, leaked "undefined"/NaN, one h1,
                                                      # dead internal links, unknown slugs -> not-found page (not a skeleton)
  • e2e/global-setup.ts spawns a throw-away API on 8111 and the standalone build on 3111 (OSR_E2E_WEB / OSR_E2E_API point at servers you already run - clear them afterwards, they linger in the shell).
  • Helpers in e2e/helpers.ts: signUp(tag) (unique e-mail per run - the DB is shared across runs), adoptSession(page, token), operatorToken(), setPlan, field(page, /^Label/), expectNoCrash(page). Locator lessons: Field labels with hints -> getByRole("textbox", { name: /^Code/ }); getByRole("alert") also matches Next's route announcer; Card titles are h3; a hidden streamed copy of a form exists in div#S:0 -> use :visible.
  • Add a new page to PUBLIC / DASHBOARD / ADMIN in accessibility.spec.ts and responsive.spec.ts, and to the page-walk in dashboard-pages.spec.ts / console-pages.spec.ts. A new journey gets its own spec named after what the person does ("open a ticket, get its reference and e-mail, operator answers...").
  • Theme and contrast audit: run against next dev on 3001 (OSR_API_URL=http://localhost:8080), sign in by fetch (POST /api/v1/signup -> localStorage osr.platform.apiKey), parse colours through a 1x1 canvas (getImageData; Tailwind 4 emits lab() / oklab()). Screenshots go to %TEMP%, never under platform/web.
  • Against the dev stack (docker compose up -d --build, containers run the image, no watch): python -X utf8 platform/api/scripts/smoke.py --api http://localhost:3000 --web http://localhost:3000 --operator admin:admin-dev-only walks every page and API surface.
  • Live: platform/api/scripts/live_use_cases.py --key <k> proves routing on production; open the deployed page in both themes on a phone before closing the task.

Anti-patterns seen in this codebase (do not bring them back)

  • A 1fr grid track that grew to a <pre> / install command and made the page scroll sideways on phones.
  • tailwind-merge dropping text-display-* as a colour class; shrink-0 action rows; fixed w-64 inputs.
  • A cache-only useApi page that showed stale numbers after a mutation without a reload().
  • A gated feature that disappeared on the free plan instead of explaining the gate.
  • Provider "healthy" badges that were five days old; health must show when it was checked.
  • Clicks that "worked" in the integrated browser but not on the deployed site - verify on the real origin.
  • notFound() thrown by a detail page under a route-group loading.tsx left visitors on the skeleton with a 200: every route group that streams needs its own not-found.tsx (the public one is app/(marketing)/not-found.tsx).
  • A checklist step that was "done" for the wrong reason (user is None on a key session read as verified): a step must be computed from the event it names, for every kind of principal.
  • "100% succeeded" over zero requests; raw proxy errors (platform API unavailable: fetch failed) shown to a person.