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:
@@ -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 />} />
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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 PO→QA handoff,
|
||||||
|
and watch <Link to="/analytics" className="text-primary underline">Analytics</Link> for human edit distance.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AppShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user