Get started: an A-to-Z onboarding checklist

A new "Get started" page (top of the sidebar) that detects setup progress from real data
and guides the full flow: model the org -> product identity -> connect a model (BYOK) ->
staff an AI seat -> fill the backlog -> review the first agent output. Each step shows
done/todo with a deep link, plus an overall progress bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-16 07:56:32 +03:30
parent 13d01391da
commit 8a033a2a6f
3 changed files with 140 additions and 0 deletions
+2
View File
@@ -4,6 +4,7 @@ import { AgentProfilesPage } from '@/pages/AgentProfilesPage'
import { AnalyticsPage } from '@/pages/AnalyticsPage' import { AnalyticsPage } from '@/pages/AnalyticsPage'
import { BoardPage } from '@/pages/BoardPage' import { BoardPage } from '@/pages/BoardPage'
import { CartablePage } from '@/pages/CartablePage' import { CartablePage } from '@/pages/CartablePage'
import { GetStartedPage } from '@/pages/GetStartedPage'
import { LoginPage } from '@/pages/LoginPage' import { LoginPage } from '@/pages/LoginPage'
import { MembersPage } from '@/pages/MembersPage' import { MembersPage } from '@/pages/MembersPage'
import { OrgChartPage } from '@/pages/OrgChartPage' import { OrgChartPage } from '@/pages/OrgChartPage'
@@ -24,6 +25,7 @@ export default function App() {
<Routes> <Routes>
<Route path="/login" element={token ? <Navigate to="/" replace /> : <LoginPage />} /> <Route path="/login" element={token ? <Navigate to="/" replace /> : <LoginPage />} />
<Route path="/" element={token ? <BoardPage /> : <Navigate to="/login" replace />} /> <Route path="/" element={token ? <BoardPage /> : <Navigate to="/login" replace />} />
<Route path="/start" element={token ? <GetStartedPage /> : <Navigate to="/login" replace />} />
<Route path="/team" element={token ? <TeamPage /> : <Navigate to="/login" replace />} /> <Route path="/team" element={token ? <TeamPage /> : <Navigate to="/login" replace />} />
<Route path="/seats" element={token ? <SeatsPage /> : <Navigate to="/login" replace />} /> <Route path="/seats" element={token ? <SeatsPage /> : <Navigate to="/login" replace />} />
<Route path="/reviews" element={token ? <ReviewsPage /> : <Navigate to="/login" replace />} /> <Route path="/reviews" element={token ? <ReviewsPage /> : <Navigate to="/login" replace />} />
+2
View File
@@ -13,6 +13,7 @@ import {
LogOut, LogOut,
Network, Network,
Package, Package,
Rocket,
ShieldCheck, ShieldCheck,
Sparkles, Sparkles,
Users, Users,
@@ -45,6 +46,7 @@ export function AppShell({ children }: { children: ReactNode }) {
<Separator className="bg-sidebar-border" /> <Separator className="bg-sidebar-border" />
<nav className="flex flex-1 flex-col gap-1 p-3"> <nav className="flex flex-1 flex-col gap-1 p-3">
<NavItem icon={Rocket} label="Get started" to="/start" />
<NavItem icon={LayoutDashboard} label="Board" to="/" /> <NavItem icon={LayoutDashboard} label="Board" to="/" />
<NavItem icon={Sparkles} label="Team" to="/team" /> <NavItem icon={Sparkles} label="Team" to="/team" />
<NavItem icon={Inbox} label="Cartable" to="/cartable" /> <NavItem icon={Inbox} label="Cartable" to="/cartable" />
+136
View File
@@ -0,0 +1,136 @@
import { useCallback, useEffect, useState } from 'react'
import { Link } from 'react-router'
import { ArrowRight, Check, Circle, Rocket } from 'lucide-react'
import { toast } from 'sonner'
import { AppShell } from '@/components/AppShell'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { api } from '@/lib/api'
import { cn } from '@/lib/utils'
import { useAuth } from '@/store/auth'
interface Step {
title: string
desc: string
done: boolean
to: string
cta: string
}
export function GetStartedPage() {
const organizationId = useAuth((s) => s.organizationId)
const [steps, setSteps] = useState<Step[] | null>(null)
const load = useCallback(async () => {
if (!organizationId) return
try {
const [products, teams, configs] = await Promise.all([
api.get<{ id: string }[]>(`/api/orgboard/products?organizationId=${organizationId}`),
api.get<{ id: string; productId: string | null }[]>(`/api/orgboard/teams?organizationId=${organizationId}`),
api.get<{ id: string }[]>(`/api/integrations/api-configs?organizationId=${organizationId}`),
])
// Walk each team once for seats + board; cap the fan-out for big orgs.
const teamsToScan = teams.slice(0, 12)
const seatsByTeam = await Promise.all(
teamsToScan.map((t) => api.get<{ state: string }[]>(`/api/orgboard/seats?teamId=${t.id}`).catch(() => [])),
)
const boards = await Promise.all(
teamsToScan.map((t) =>
api.get<{ columns: { items: unknown[] }[] }>(`/api/orgboard/board?teamId=${t.id}`).catch(() => ({ columns: [] })),
),
)
const identities = await Promise.all(
products.slice(0, 12).map((p) =>
api.get<{ identity: string | null }>(`/api/orgboard/products/${p.id}/identity`).catch(() => ({ identity: null })),
),
)
const reviews = await api
.get<unknown[]>(`/api/governance/reviews?organizationId=${organizationId}`)
.catch(() => [])
const hasAiSeat = seatsByTeam.some((seats) => seats.some((s) => s.state === 'Ai'))
const hasTask = boards.some((b) => b.columns.some((c) => c.items.length > 0))
const hasIdentity = identities.some((i) => !!i.identity)
setSteps([
{ title: 'Workspace ready', desc: 'Your organization exists and you are signed in as its owner.', done: true, to: '/', cta: 'Open board' },
{ title: 'Model the org', desc: 'Create divisions → products/services → teams. A team runs a board.', done: teams.length > 0, to: '/structure', cta: 'Structure' },
{ title: 'Give the product an identity', desc: 'Write a PRODUCT.md brief — shared by every agent on the product.', done: hasIdentity, to: '/structure', cta: 'Set identity' },
{ title: 'Connect a model (BYOK)', desc: 'Add an API key (OpenAI-compatible). Owner-only, encrypted, never returned.', done: configs.length > 0, to: '/seats', cta: 'Add connection' },
{ title: 'Staff a seat with an AI agent', desc: 'Pick a role-seat, choose skills + autonomy + the model, and turn it AI.', done: hasAiSeat, to: '/seats', cta: 'Staff a seat' },
{ title: 'Fill the backlog', desc: 'Create tasks on the board — assign them to humans or AI agents.', done: hasTask, to: '/', cta: 'Create tasks' },
{ title: 'Review the first agent output', desc: 'Assign a task to an agent; its output waits in the review inbox to approve.', done: reviews.length > 0, to: '/reviews', cta: 'Review inbox' },
])
} catch (err) {
toast.error((err as Error).message)
}
}, [organizationId])
useEffect(() => {
void load()
}, [load])
const doneCount = steps?.filter((s) => s.done).length ?? 0
const total = steps?.length ?? 7
return (
<AppShell>
<div className="mx-auto max-w-2xl p-6">
<header className="mb-6">
<h1 className="flex items-center gap-2 text-2xl font-semibold tracking-tight">
<Rocket className="size-6" /> Get started
</h1>
<p className="text-sm text-muted-foreground">
Build a human + AI team from A to Z. {doneCount} of {total} steps done.
</p>
<div className="mt-3 h-2 w-full overflow-hidden rounded-full bg-muted/60">
<div
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${(doneCount / total) * 100}%` }}
/>
</div>
</header>
<div className="flex flex-col gap-3">
{steps?.map((step, i) => (
<Card key={i}>
<CardContent className="flex items-center gap-4 py-4">
<span
className={cn(
'grid size-8 shrink-0 place-items-center rounded-full text-sm font-semibold',
step.done ? 'bg-approved/20 text-approved' : 'bg-muted text-muted-foreground',
)}
>
{step.done ? <Check className="size-4" /> : <Circle className="size-3.5" />}
</span>
<div className="min-w-0 flex-1">
<p className={cn('text-sm font-medium', step.done && 'text-muted-foreground line-through')}>
{i + 1}. {step.title}
</p>
<p className="text-xs text-muted-foreground">{step.desc}</p>
</div>
<Button asChild variant={step.done ? 'ghost' : 'outline'} size="sm" className="shrink-0">
<Link to={step.to}>
{step.done ? 'View' : step.cta}
<ArrowRight data-icon="inline-end" />
</Link>
</Button>
</CardContent>
</Card>
))}
{steps === null && <p className="text-sm text-muted-foreground">Checking your setup</p>}
</div>
{steps && doneCount === total && (
<Card className="mt-4">
<CardContent className="py-5 text-center text-sm">
🎉 Your human + AI team is running end to end. Mark a story <b>Done</b> to fire the POQA handoff,
and watch <Link to="/analytics" className="text-primary underline">Analytics</Link> for human edit distance.
</CardContent>
</Card>
)}
</div>
</AppShell>
)
}