@renseiai/agentfactory-dashboard 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/components.json +16 -0
- package/package.json +78 -0
- package/src/components/fleet/agent-card.tsx +97 -0
- package/src/components/fleet/fleet-overview.tsx +139 -0
- package/src/components/fleet/provider-icon.tsx +61 -0
- package/src/components/fleet/stat-card.tsx +77 -0
- package/src/components/fleet/status-dot.tsx +35 -0
- package/src/components/layout/bottom-bar.tsx +58 -0
- package/src/components/layout/shell.tsx +65 -0
- package/src/components/layout/sidebar.tsx +97 -0
- package/src/components/layout/top-bar.tsx +63 -0
- package/src/components/pipeline/pipeline-card.tsx +65 -0
- package/src/components/pipeline/pipeline-column.tsx +44 -0
- package/src/components/pipeline/pipeline-view.tsx +85 -0
- package/src/components/sessions/session-detail.tsx +153 -0
- package/src/components/sessions/session-list.tsx +125 -0
- package/src/components/sessions/session-timeline.tsx +76 -0
- package/src/components/sessions/token-chart.tsx +51 -0
- package/src/components/settings/settings-view.tsx +175 -0
- package/src/components/shared/empty-state.tsx +34 -0
- package/src/components/shared/logo.tsx +37 -0
- package/src/components/ui/badge.tsx +33 -0
- package/src/components/ui/button.tsx +54 -0
- package/src/components/ui/card.tsx +57 -0
- package/src/components/ui/dropdown-menu.tsx +77 -0
- package/src/components/ui/scroll-area.tsx +45 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +88 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/tabs.tsx +54 -0
- package/src/components/ui/tooltip.tsx +29 -0
- package/src/hooks/use-sessions.ts +13 -0
- package/src/hooks/use-stats.ts +13 -0
- package/src/hooks/use-workers.ts +17 -0
- package/src/index.ts +82 -0
- package/src/lib/format.ts +36 -0
- package/src/lib/status-config.ts +72 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/work-type-config.ts +116 -0
- package/src/pages/dashboard-page.tsx +11 -0
- package/src/pages/pipeline-page.tsx +11 -0
- package/src/pages/session-page.tsx +29 -0
- package/src/pages/settings-page.tsx +7 -0
- package/src/styles/globals.css +218 -0
- package/src/types/api.ts +48 -0
- package/tailwind.config.ts +139 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
import { Skeleton } from '../../components/ui/skeleton'
|
|
3
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/ui/tooltip'
|
|
4
|
+
|
|
5
|
+
interface StatCardProps {
|
|
6
|
+
label: string
|
|
7
|
+
value: string | number
|
|
8
|
+
detail?: string
|
|
9
|
+
icon?: React.ReactNode
|
|
10
|
+
trend?: 'up' | 'down' | 'neutral'
|
|
11
|
+
accent?: boolean
|
|
12
|
+
loading?: boolean
|
|
13
|
+
className?: string
|
|
14
|
+
tooltip?: React.ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function StatCard({ label, value, detail, icon, accent, loading, className, tooltip }: StatCardProps) {
|
|
18
|
+
if (loading) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn(
|
|
21
|
+
'rounded-xl border border-af-surface-border/50 bg-af-surface/50 p-4',
|
|
22
|
+
className
|
|
23
|
+
)}>
|
|
24
|
+
<div className="flex items-center justify-between">
|
|
25
|
+
<Skeleton className="h-3 w-16" />
|
|
26
|
+
<Skeleton className="h-4 w-4 rounded" />
|
|
27
|
+
</div>
|
|
28
|
+
<Skeleton className="mt-3 h-8 w-16" />
|
|
29
|
+
<Skeleton className="mt-1.5 h-3 w-20" />
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const card = (
|
|
35
|
+
<div className={cn(
|
|
36
|
+
'group rounded-xl border border-af-surface-border/50 bg-af-surface/40 p-4 transition-all duration-300 hover-glow',
|
|
37
|
+
accent && 'border-af-accent/15 bg-af-accent/[0.03]',
|
|
38
|
+
className
|
|
39
|
+
)}>
|
|
40
|
+
<div className="flex items-center justify-between">
|
|
41
|
+
<span className="text-2xs font-body font-medium uppercase tracking-wider text-af-text-tertiary">
|
|
42
|
+
{label}
|
|
43
|
+
</span>
|
|
44
|
+
{icon && (
|
|
45
|
+
<span className={cn(
|
|
46
|
+
'text-af-text-tertiary transition-colors duration-300 group-hover:text-af-text-secondary',
|
|
47
|
+
accent && 'text-af-accent/40 group-hover:text-af-accent/70'
|
|
48
|
+
)}>
|
|
49
|
+
{icon}
|
|
50
|
+
</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
<div className={cn(
|
|
54
|
+
'mt-2 font-display text-2xl font-bold tabular-nums tracking-tight',
|
|
55
|
+
accent ? 'text-af-accent' : 'text-af-text-primary'
|
|
56
|
+
)}>
|
|
57
|
+
{value}
|
|
58
|
+
</div>
|
|
59
|
+
{detail && (
|
|
60
|
+
<p className="mt-1 text-2xs font-body text-af-text-tertiary">{detail}</p>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (tooltip) {
|
|
66
|
+
return (
|
|
67
|
+
<Tooltip>
|
|
68
|
+
<TooltipTrigger asChild>{card}</TooltipTrigger>
|
|
69
|
+
<TooltipContent side="bottom" className="max-w-xs">
|
|
70
|
+
{tooltip}
|
|
71
|
+
</TooltipContent>
|
|
72
|
+
</Tooltip>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return card
|
|
77
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
import { getStatusConfig, type SessionStatus } from '../../lib/status-config'
|
|
3
|
+
|
|
4
|
+
interface StatusDotProps {
|
|
5
|
+
status: SessionStatus
|
|
6
|
+
className?: string
|
|
7
|
+
showHeartbeat?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function StatusDot({ status, className, showHeartbeat = false }: StatusDotProps) {
|
|
11
|
+
const config = getStatusConfig(status)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<span className={cn('relative inline-flex h-2 w-2', className)}>
|
|
15
|
+
{/* Outer glow ring for active states */}
|
|
16
|
+
{config.animate && showHeartbeat && (
|
|
17
|
+
<span
|
|
18
|
+
className={cn(
|
|
19
|
+
'absolute inset-0 rounded-full animate-heartbeat',
|
|
20
|
+
config.dotColor
|
|
21
|
+
)}
|
|
22
|
+
/>
|
|
23
|
+
)}
|
|
24
|
+
{/* Core dot */}
|
|
25
|
+
<span
|
|
26
|
+
className={cn(
|
|
27
|
+
'relative inline-flex h-2 w-2 rounded-full',
|
|
28
|
+
config.dotColor,
|
|
29
|
+
config.animate && 'animate-pulse-dot',
|
|
30
|
+
config.glowClass
|
|
31
|
+
)}
|
|
32
|
+
/>
|
|
33
|
+
</span>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { useStats } from '../../hooks/use-stats'
|
|
5
|
+
import { formatCost } from '../../lib/format'
|
|
6
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/ui/tooltip'
|
|
7
|
+
import { Zap } from 'lucide-react'
|
|
8
|
+
|
|
9
|
+
interface BottomBarProps {
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function BottomBar({ className }: BottomBarProps) {
|
|
14
|
+
const { data } = useStats()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<footer
|
|
18
|
+
className={cn(
|
|
19
|
+
'flex h-8 items-center justify-between border-t border-af-surface-border/40 bg-af-bg-secondary/30 backdrop-blur-sm px-5',
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
<div className="flex items-center gap-5 text-2xs font-body text-af-text-tertiary">
|
|
24
|
+
<span>
|
|
25
|
+
<span className="text-af-text-secondary tabular-nums">{data?.completedToday ?? 0}</span> completed
|
|
26
|
+
</span>
|
|
27
|
+
<span>
|
|
28
|
+
<span className="text-af-text-secondary tabular-nums">{data?.availableCapacity ?? 0}</span> capacity
|
|
29
|
+
</span>
|
|
30
|
+
{data?.totalCostToday != null && (
|
|
31
|
+
<Tooltip>
|
|
32
|
+
<TooltipTrigger asChild>
|
|
33
|
+
<span className="cursor-default">
|
|
34
|
+
<span className="text-af-accent font-mono tabular-nums">{formatCost(data.totalCostToday)}</span> today
|
|
35
|
+
{data.totalCostAllTime != null && data.totalCostAllTime !== data.totalCostToday && (
|
|
36
|
+
<span className="text-af-text-tertiary"> · <span className="font-mono tabular-nums">{formatCost(data.totalCostAllTime)}</span> total</span>
|
|
37
|
+
)}
|
|
38
|
+
</span>
|
|
39
|
+
</TooltipTrigger>
|
|
40
|
+
<TooltipContent side="top">
|
|
41
|
+
Cost from {data.sessionCountToday ?? 0} session{(data.sessionCountToday ?? 0) !== 1 ? 's' : ''} updated today (UTC)
|
|
42
|
+
</TooltipContent>
|
|
43
|
+
</Tooltip>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<a
|
|
48
|
+
href="https://github.com/renseiai/agentfactory"
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noopener noreferrer"
|
|
51
|
+
className="flex items-center gap-1 text-2xs font-body text-af-text-tertiary hover:text-af-text-secondary transition-colors"
|
|
52
|
+
>
|
|
53
|
+
<Zap className="h-2.5 w-2.5" />
|
|
54
|
+
AgentFactory
|
|
55
|
+
</a>
|
|
56
|
+
</footer>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
import { Sidebar } from './sidebar'
|
|
6
|
+
import { TopBar } from './top-bar'
|
|
7
|
+
import { BottomBar } from './bottom-bar'
|
|
8
|
+
import { Sheet, SheetContent, SheetTrigger, SheetTitle } from '../../components/ui/sheet'
|
|
9
|
+
import { Button } from '../../components/ui/button'
|
|
10
|
+
import { TooltipProvider } from '../../components/ui/tooltip'
|
|
11
|
+
import { Menu } from 'lucide-react'
|
|
12
|
+
import { Logo } from '../../components/shared/logo'
|
|
13
|
+
|
|
14
|
+
interface DashboardShellProps {
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
currentPath?: string
|
|
17
|
+
className?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function DashboardShell({ children, currentPath = '/', className }: DashboardShellProps) {
|
|
21
|
+
return (
|
|
22
|
+
<TooltipProvider>
|
|
23
|
+
<div className={cn('flex h-screen bg-af-bg-primary overflow-hidden', className)}>
|
|
24
|
+
{/* Background effects layer */}
|
|
25
|
+
<div className="fixed inset-0 mesh-gradient pointer-events-none" />
|
|
26
|
+
<div className="fixed inset-0 grid-bg pointer-events-none opacity-40" />
|
|
27
|
+
|
|
28
|
+
{/* Desktop sidebar */}
|
|
29
|
+
<div className="hidden md:block relative z-10">
|
|
30
|
+
<Sidebar currentPath={currentPath} />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Main content area */}
|
|
34
|
+
<div className="flex flex-1 flex-col overflow-hidden relative z-10">
|
|
35
|
+
{/* Mobile header with hamburger */}
|
|
36
|
+
<div className="flex items-center gap-3 border-b border-af-surface-border bg-af-bg-secondary/80 backdrop-blur-md px-4 py-2.5 md:hidden">
|
|
37
|
+
<Sheet>
|
|
38
|
+
<SheetTrigger asChild>
|
|
39
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 text-af-text-secondary hover:text-af-text-primary">
|
|
40
|
+
<Menu className="h-4 w-4" />
|
|
41
|
+
</Button>
|
|
42
|
+
</SheetTrigger>
|
|
43
|
+
<SheetContent side="left" className="w-60 p-0 border-af-surface-border bg-af-bg-secondary">
|
|
44
|
+
<SheetTitle className="sr-only">Navigation</SheetTitle>
|
|
45
|
+
<Sidebar currentPath={currentPath} />
|
|
46
|
+
</SheetContent>
|
|
47
|
+
</Sheet>
|
|
48
|
+
<Logo size={20} />
|
|
49
|
+
<span className="text-sm font-display font-semibold text-af-text-primary tracking-tight">
|
|
50
|
+
AgentFactory
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<TopBar className="hidden md:flex" />
|
|
55
|
+
|
|
56
|
+
<main className="flex-1 overflow-auto">
|
|
57
|
+
{children}
|
|
58
|
+
</main>
|
|
59
|
+
|
|
60
|
+
<BottomBar />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</TooltipProvider>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { Logo } from '../../components/shared/logo'
|
|
5
|
+
import { Separator } from '../../components/ui/separator'
|
|
6
|
+
import { LayoutDashboard, Columns3, Activity, Settings, ExternalLink } from 'lucide-react'
|
|
7
|
+
|
|
8
|
+
export interface NavItem {
|
|
9
|
+
label: string
|
|
10
|
+
href: string
|
|
11
|
+
icon: React.ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const defaultNavItems: NavItem[] = [
|
|
15
|
+
{ label: 'Fleet', href: '/', icon: <LayoutDashboard className="h-4 w-4" /> },
|
|
16
|
+
{ label: 'Pipeline', href: '/pipeline', icon: <Columns3 className="h-4 w-4" /> },
|
|
17
|
+
{ label: 'Sessions', href: '/sessions', icon: <Activity className="h-4 w-4" /> },
|
|
18
|
+
{ label: 'Settings', href: '/settings', icon: <Settings className="h-4 w-4" /> },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
interface SidebarProps {
|
|
22
|
+
currentPath?: string
|
|
23
|
+
navItems?: NavItem[]
|
|
24
|
+
className?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Sidebar({ currentPath = '/', navItems = defaultNavItems, className }: SidebarProps) {
|
|
28
|
+
const isActive = (href: string) => {
|
|
29
|
+
if (href === '/') return currentPath === '/'
|
|
30
|
+
return currentPath.startsWith(href)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<aside
|
|
35
|
+
className={cn(
|
|
36
|
+
'flex h-full w-[220px] flex-col border-r border-af-surface-border/60 bg-af-bg-secondary/60 backdrop-blur-xl',
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
{/* Logo area */}
|
|
41
|
+
<div className="flex items-center gap-2.5 px-5 py-4">
|
|
42
|
+
<Logo size={22} />
|
|
43
|
+
<span className="font-display text-sm font-bold text-af-text-primary tracking-tight">
|
|
44
|
+
AgentFactory
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="px-5">
|
|
49
|
+
<Separator className="bg-af-surface-border/60" />
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Navigation */}
|
|
53
|
+
<nav className="flex-1 space-y-0.5 px-3 py-4">
|
|
54
|
+
{navItems.map((item) => {
|
|
55
|
+
const active = isActive(item.href)
|
|
56
|
+
return (
|
|
57
|
+
<a
|
|
58
|
+
key={item.href}
|
|
59
|
+
href={item.href}
|
|
60
|
+
className={cn(
|
|
61
|
+
'flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-body transition-all duration-200',
|
|
62
|
+
active
|
|
63
|
+
? 'nav-active-indicator bg-af-surface/80 text-af-text-primary font-medium'
|
|
64
|
+
: 'text-af-text-secondary hover:bg-af-surface/40 hover:text-af-text-primary'
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<span className={cn(
|
|
68
|
+
'transition-colors duration-200',
|
|
69
|
+
active ? 'text-af-accent' : ''
|
|
70
|
+
)}>
|
|
71
|
+
{item.icon}
|
|
72
|
+
</span>
|
|
73
|
+
{item.label}
|
|
74
|
+
</a>
|
|
75
|
+
)
|
|
76
|
+
})}
|
|
77
|
+
</nav>
|
|
78
|
+
|
|
79
|
+
<div className="px-5">
|
|
80
|
+
<Separator className="bg-af-surface-border/60" />
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Footer */}
|
|
84
|
+
<div className="px-5 py-3">
|
|
85
|
+
<a
|
|
86
|
+
href="https://github.com/renseiai/agentfactory"
|
|
87
|
+
target="_blank"
|
|
88
|
+
rel="noopener noreferrer"
|
|
89
|
+
className="group flex items-center gap-1.5 text-2xs font-body text-af-text-tertiary hover:text-af-text-secondary transition-colors"
|
|
90
|
+
>
|
|
91
|
+
AgentFactory
|
|
92
|
+
<ExternalLink className="h-2.5 w-2.5 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
93
|
+
</a>
|
|
94
|
+
</div>
|
|
95
|
+
</aside>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { StatusDot } from '../../components/fleet/status-dot'
|
|
5
|
+
import { useStats } from '../../hooks/use-stats'
|
|
6
|
+
import { Skeleton } from '../../components/ui/skeleton'
|
|
7
|
+
import { Radio } from 'lucide-react'
|
|
8
|
+
|
|
9
|
+
interface TopBarProps {
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function TopBar({ className }: TopBarProps) {
|
|
14
|
+
const { data, isLoading } = useStats()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<header
|
|
18
|
+
className={cn(
|
|
19
|
+
'flex h-11 items-center justify-between border-b border-af-surface-border/50 bg-af-bg-secondary/40 backdrop-blur-md px-5',
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
<div className="flex items-center gap-5">
|
|
24
|
+
{isLoading ? (
|
|
25
|
+
<>
|
|
26
|
+
<Skeleton className="h-4 w-28" />
|
|
27
|
+
<Skeleton className="h-4 w-20" />
|
|
28
|
+
<Skeleton className="h-4 w-20" />
|
|
29
|
+
</>
|
|
30
|
+
) : (
|
|
31
|
+
<>
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
<StatusDot status={data?.workersOnline ? 'working' : 'stopped'} showHeartbeat />
|
|
34
|
+
<span className="text-xs font-body text-af-text-secondary">
|
|
35
|
+
<span className="font-semibold text-af-text-primary tabular-nums">{data?.workersOnline ?? 0}</span>{' '}
|
|
36
|
+
{data?.workersOnline === 1 ? 'worker' : 'workers'}
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div className="h-3 w-px bg-af-surface-border" />
|
|
41
|
+
|
|
42
|
+
<span className="text-xs font-body text-af-text-secondary">
|
|
43
|
+
<span className="font-semibold text-af-teal tabular-nums">{data?.agentsWorking ?? 0}</span> active
|
|
44
|
+
</span>
|
|
45
|
+
|
|
46
|
+
<span className="text-xs font-body text-af-text-secondary">
|
|
47
|
+
<span className="font-semibold text-af-text-primary tabular-nums">{data?.queueDepth ?? 0}</span> queued
|
|
48
|
+
</span>
|
|
49
|
+
</>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="flex items-center gap-2 text-xs font-body text-af-text-tertiary">
|
|
54
|
+
{data?.timestamp && (
|
|
55
|
+
<>
|
|
56
|
+
<Radio className="h-3 w-3 text-af-teal animate-pulse-dot" />
|
|
57
|
+
<span>{new Date(data.timestamp).toLocaleTimeString()}</span>
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</header>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
import { StatusDot } from '../../components/fleet/status-dot'
|
|
3
|
+
import { Badge } from '../../components/ui/badge'
|
|
4
|
+
import { formatDuration } from '../../lib/format'
|
|
5
|
+
import { getWorkTypeConfig } from '../../lib/work-type-config'
|
|
6
|
+
import type { PublicSessionResponse } from '../../types/api'
|
|
7
|
+
import { Clock } from 'lucide-react'
|
|
8
|
+
|
|
9
|
+
interface PipelineCardProps {
|
|
10
|
+
session: PublicSessionResponse
|
|
11
|
+
className?: string
|
|
12
|
+
onSelect?: (sessionId: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function PipelineCard({ session, className, onSelect }: PipelineCardProps) {
|
|
16
|
+
const workTypeConfig = getWorkTypeConfig(session.workType)
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={cn(
|
|
21
|
+
'rounded-lg border border-af-surface-border/40 bg-af-surface/50 p-3 transition-all duration-200 hover-glow',
|
|
22
|
+
onSelect && 'cursor-pointer',
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...(onSelect && {
|
|
26
|
+
role: 'button',
|
|
27
|
+
tabIndex: 0,
|
|
28
|
+
onClick: () => onSelect(session.id),
|
|
29
|
+
onKeyDown: (e: React.KeyboardEvent) => {
|
|
30
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
31
|
+
e.preventDefault()
|
|
32
|
+
onSelect(session.id)
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
})}
|
|
36
|
+
>
|
|
37
|
+
<div className="flex items-center gap-2">
|
|
38
|
+
<StatusDot status={session.status} />
|
|
39
|
+
<span className={cn(
|
|
40
|
+
'text-xs font-mono font-medium truncate',
|
|
41
|
+
onSelect
|
|
42
|
+
? 'text-af-teal hover:underline underline-offset-2'
|
|
43
|
+
: 'text-af-text-primary'
|
|
44
|
+
)}>
|
|
45
|
+
{session.identifier}
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="mt-2.5 flex items-center justify-between">
|
|
49
|
+
<Badge
|
|
50
|
+
variant="outline"
|
|
51
|
+
className={cn(
|
|
52
|
+
'text-2xs border',
|
|
53
|
+
workTypeConfig.bgColor, workTypeConfig.color, workTypeConfig.borderColor
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{workTypeConfig.label}
|
|
57
|
+
</Badge>
|
|
58
|
+
<span className="flex items-center gap-1 text-2xs font-body text-af-text-tertiary tabular-nums">
|
|
59
|
+
<Clock className="h-2.5 w-2.5" />
|
|
60
|
+
{formatDuration(session.duration)}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
import { ScrollArea } from '../../components/ui/scroll-area'
|
|
3
|
+
import { PipelineCard } from './pipeline-card'
|
|
4
|
+
import type { PublicSessionResponse } from '../../types/api'
|
|
5
|
+
|
|
6
|
+
interface PipelineColumnProps {
|
|
7
|
+
title: string
|
|
8
|
+
sessions: PublicSessionResponse[]
|
|
9
|
+
count: number
|
|
10
|
+
accentClass?: string
|
|
11
|
+
className?: string
|
|
12
|
+
onSessionSelect?: (sessionId: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function PipelineColumn({ title, sessions, count, accentClass, className, onSessionSelect }: PipelineColumnProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={cn(
|
|
19
|
+
'flex w-72 shrink-0 flex-col rounded-xl border border-af-surface-border/40 bg-af-bg-secondary/50 backdrop-blur-sm',
|
|
20
|
+
accentClass,
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
{/* Column header */}
|
|
25
|
+
<div className="flex items-center justify-between px-3.5 py-3">
|
|
26
|
+
<span className="text-xs font-display font-semibold text-af-text-primary tracking-tight">
|
|
27
|
+
{title}
|
|
28
|
+
</span>
|
|
29
|
+
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-af-surface/60 px-1.5 text-2xs font-body font-medium tabular-nums text-af-text-secondary">
|
|
30
|
+
{count}
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* Card list */}
|
|
35
|
+
<ScrollArea className="flex-1 px-2 pb-2">
|
|
36
|
+
<div className="space-y-2">
|
|
37
|
+
{sessions.map((session) => (
|
|
38
|
+
<PipelineCard key={session.id} session={session} onSelect={onSessionSelect} />
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</ScrollArea>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { useSessions } from '../../hooks/use-sessions'
|
|
5
|
+
import { PipelineColumn } from './pipeline-column'
|
|
6
|
+
import { EmptyState } from '../../components/shared/empty-state'
|
|
7
|
+
import { Skeleton } from '../../components/ui/skeleton'
|
|
8
|
+
import type { PublicSessionResponse, SessionStatus } from '../../types/api'
|
|
9
|
+
|
|
10
|
+
interface ColumnDef {
|
|
11
|
+
title: string
|
|
12
|
+
statuses: SessionStatus[]
|
|
13
|
+
accentClass: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const columns: ColumnDef[] = [
|
|
17
|
+
{ title: 'Backlog', statuses: ['queued', 'parked'], accentClass: 'column-accent column-accent-gray' },
|
|
18
|
+
{ title: 'Started', statuses: ['working'], accentClass: 'column-accent column-accent-green' },
|
|
19
|
+
{ title: 'Finished', statuses: ['completed'], accentClass: 'column-accent column-accent-blue' },
|
|
20
|
+
{ title: 'Failed', statuses: ['failed'], accentClass: 'column-accent column-accent-red' },
|
|
21
|
+
{ title: 'Stopped', statuses: ['stopped'], accentClass: 'column-accent column-accent-yellow' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function groupByColumn(sessions: PublicSessionResponse[]) {
|
|
25
|
+
return columns.map((col) => ({
|
|
26
|
+
...col,
|
|
27
|
+
sessions: sessions.filter((s) => col.statuses.includes(s.status)),
|
|
28
|
+
}))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PipelineViewProps {
|
|
32
|
+
className?: string
|
|
33
|
+
onSessionSelect?: (sessionId: string) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function PipelineView({ className, onSessionSelect }: PipelineViewProps) {
|
|
37
|
+
const { data, isLoading } = useSessions()
|
|
38
|
+
const sessions = data?.sessions ?? []
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={cn('p-6', className)}>
|
|
42
|
+
<div className="mb-5 flex items-center gap-3">
|
|
43
|
+
<h2 className="font-display text-lg font-bold text-af-text-primary tracking-tight">
|
|
44
|
+
Pipeline
|
|
45
|
+
</h2>
|
|
46
|
+
{!isLoading && (
|
|
47
|
+
<span className="rounded-full bg-af-surface/60 px-2 py-0.5 text-2xs font-body font-medium tabular-nums text-af-text-secondary">
|
|
48
|
+
{sessions.length}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{isLoading ? (
|
|
54
|
+
<div className="flex gap-3 overflow-x-auto pb-2">
|
|
55
|
+
{columns.map((col) => (
|
|
56
|
+
<Skeleton key={col.title} className="h-96 w-72 shrink-0 rounded-xl" />
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
) : sessions.length === 0 ? (
|
|
60
|
+
<EmptyState
|
|
61
|
+
title="No pipeline data"
|
|
62
|
+
description="Sessions will populate the pipeline as agents work on issues."
|
|
63
|
+
/>
|
|
64
|
+
) : (
|
|
65
|
+
<div className="flex gap-3 overflow-x-auto pb-2">
|
|
66
|
+
{groupByColumn(sessions).map((col, i) => (
|
|
67
|
+
<div
|
|
68
|
+
key={col.title}
|
|
69
|
+
className="animate-fade-in"
|
|
70
|
+
style={{ animationDelay: `${i * 60}ms` }}
|
|
71
|
+
>
|
|
72
|
+
<PipelineColumn
|
|
73
|
+
title={col.title}
|
|
74
|
+
sessions={col.sessions}
|
|
75
|
+
count={col.sessions.length}
|
|
76
|
+
accentClass={col.accentClass}
|
|
77
|
+
onSessionSelect={onSessionSelect}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|