import { useCallback, useEffect, useMemo, useState } from 'react' import { Sparkles } from 'lucide-react' import { toast } from 'sonner' import { AppShell } from '@/components/AppShell' import type { FaceState } from '@/components/AgentFace' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { api } from '@/lib/api' import { useAgentActivity } from '@/lib/useAgentActivity' import { useAuth } from '@/store/auth' import './team.css' interface Product { id: string name: string kind: string } interface Team { id: string name: string productId: string | null } interface SeatRow { id: string teamId: string roleName: string state: string agentId: string | null } interface Agent { id: string name: string monogram: string | null autonomy: string skillKeys: string[] } interface AgentCard { seatId: string role: string team: string agent: Agent } /** Deterministic gradient + avatar ink per role family. Gradients are a deliberate exception to the * app's flat house style — used only on this showcase team view. */ function styleFor(role: string): { bg: string; ink: string } { const n = role.toLowerCase() if (/(product|owner|\bpo\b|\bpm\b)/.test(n)) return { bg: 'linear-gradient(135deg,#6366f1,#8b5cf6)', ink: '#5b21b6' } if (/(analyst|analysis|business)/.test(n)) return { bg: 'linear-gradient(135deg,#3b82f6,#06b6d4)', ink: '#0e7490' } if (/(backend|\bapi\b|server)/.test(n)) return { bg: 'linear-gradient(135deg,#4f46e5,#2563eb)', ink: '#3730a3' } if (/(frontend|front|web|client)/.test(n)) return { bg: 'linear-gradient(135deg,#7c3aed,#db2777)', ink: '#9d174d' } if (/(design|ux|ui)/.test(n)) return { bg: 'linear-gradient(135deg,#c026d3,#f43f5e)', ink: '#9d174d' } if (/(qa|test|quality)/.test(n)) return { bg: 'linear-gradient(135deg,#0d9488,#10b981)', ink: '#0f766e' } return { bg: 'linear-gradient(135deg,#475569,#6366f1)', ink: '#334155' } } const STATUS_LABEL: Record = { idle: 'idle · awaiting work', thinking: 'queued', working: 'working…', review: 'awaiting review', done: 'just delivered', failed: 'run failed', } function summaryOf(identity: string | null): string { if (!identity) return 'No product identity yet — set a PRODUCT.md to give the team shared context.' const m = identity.match(/^summary:\s*(.+)$/m) return m ? m[1].trim() : 'Shared PRODUCT.md identity is set for this product.' } /** A gradient-card overview of a product and its AI team — the product, its agents, and live status. */ export function TeamPage() { const organizationId = useAuth((s) => s.organizationId) const [products, setProducts] = useState([]) const [productId, setProductId] = useState(null) const [summary, setSummary] = useState('') const [cards, setCards] = useState([]) const [teamCount, setTeamCount] = useState(0) useEffect(() => { if (!organizationId) return void (async () => { try { const list = await api.get(`/api/orgboard/products?organizationId=${organizationId}`) setProducts(list) setProductId((cur) => cur ?? list[0]?.id ?? null) } catch (err) { toast.error((err as Error).message) } })() }, [organizationId]) const loadProduct = useCallback(async (pid: string) => { try { const [teams, identity] = await Promise.all([ api.get(`/api/orgboard/teams?organizationId=${organizationId}`), api.get<{ identity: string | null }>(`/api/orgboard/products/${pid}/identity`).catch(() => ({ identity: null })), ]) const productTeams = teams.filter((t) => t.productId === pid) setTeamCount(productTeams.length) setSummary(summaryOf(identity.identity)) const built: AgentCard[] = [] for (const team of productTeams) { const seats = await api.get(`/api/orgboard/seats?teamId=${team.id}`) for (const seat of seats.filter((s) => s.state === 'Ai' && s.agentId)) { const agent = await api.get(`/api/orgboard/seats/${seat.id}/agent`).catch(() => null) if (agent) built.push({ seatId: seat.id, role: seat.roleName, team: team.name, agent }) } } setCards(built) } catch (err) { toast.error((err as Error).message) } }, [organizationId]) useEffect(() => { if (productId) void loadProduct(productId) }, [productId, loadProduct]) const product = products.find((p) => p.id === productId) ?? null const stateFor = useAgentActivity(organizationId, useMemo(() => cards.map((c) => c.agent.id), [cards])) return (

Team

{products.length > 0 && ( )}
{product && (
Product · shared identity

{product.name}

{summary}

{teamCount}teams
{cards.length}AI agents
)}
{cards.map((c) => { const s = styleFor(c.role) const face = stateFor(c.agent.id) const active = face === 'working' || face === 'thinking' return (
{c.agent.monogram || c.agent.name.slice(0, 2).toUpperCase()}
{c.agent.autonomy}
{c.agent.name}
{c.role} · {c.team}
{c.agent.skillKeys.slice(0, 3).map((k) => {k})} {c.agent.skillKeys.length === 0 && no skills yet}
{STATUS_LABEL[face]}
) })}
{cards.length === 0 && product && (

No AI agents on {product.name} yet — staff its seats on the AI seats page.

)}
) }