@supaku/agentfactory-dashboard 0.5.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/components.json +16 -0
- package/package.json +47 -0
- package/src/components/fleet/agent-card.tsx +62 -0
- package/src/components/fleet/fleet-overview.tsx +89 -0
- package/src/components/fleet/provider-icon.tsx +61 -0
- package/src/components/fleet/stat-card.tsx +43 -0
- package/src/components/fleet/status-dot.tsx +32 -0
- package/src/components/layout/bottom-bar.tsx +50 -0
- package/src/components/layout/shell.tsx +57 -0
- package/src/components/layout/sidebar.tsx +81 -0
- package/src/components/layout/top-bar.tsx +55 -0
- package/src/components/pipeline/pipeline-card.tsx +42 -0
- package/src/components/pipeline/pipeline-column.tsx +40 -0
- package/src/components/pipeline/pipeline-view.tsx +75 -0
- package/src/components/sessions/session-detail.tsx +132 -0
- package/src/components/sessions/session-list.tsx +109 -0
- package/src/components/sessions/session-timeline.tsx +54 -0
- package/src/components/sessions/token-chart.tsx +51 -0
- package/src/components/settings/settings-view.tsx +155 -0
- package/src/components/shared/empty-state.tsx +29 -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 +50 -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 +12 -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 +58 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/work-type-config.ts +53 -0
- package/src/pages/dashboard-page.tsx +7 -0
- package/src/pages/pipeline-page.tsx +7 -0
- package/src/pages/session-page.tsx +29 -0
- package/src/pages/settings-page.tsx +7 -0
- package/src/styles/globals.css +38 -0
- package/src/types/api.ts +45 -0
- package/tailwind.config.ts +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Supaku
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/components.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "tailwind.config.ts",
|
|
8
|
+
"css": "src/styles/globals.css",
|
|
9
|
+
"baseColor": "zinc",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"aliases": {
|
|
13
|
+
"components": "@/components",
|
|
14
|
+
"utils": "@/lib/utils"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@supaku/agentfactory-dashboard",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Premium dashboard UI components for AgentFactory",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./styles": "./src/styles/globals.css",
|
|
13
|
+
"./tailwind-preset": "./tailwind.config.ts"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"tailwind.config.ts",
|
|
18
|
+
"components.json"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@radix-ui/react-dialog": "^1.1.0",
|
|
22
|
+
"@radix-ui/react-dropdown-menu": "^2.1.0",
|
|
23
|
+
"@radix-ui/react-scroll-area": "^1.2.0",
|
|
24
|
+
"@radix-ui/react-separator": "^1.1.0",
|
|
25
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
26
|
+
"@radix-ui/react-tabs": "^1.1.0",
|
|
27
|
+
"@radix-ui/react-tooltip": "^1.1.0",
|
|
28
|
+
"class-variance-authority": "^0.7.0",
|
|
29
|
+
"clsx": "^2.1.0",
|
|
30
|
+
"lucide-react": "^0.400.0",
|
|
31
|
+
"swr": "^2.2.0",
|
|
32
|
+
"tailwind-merge": "^2.2.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"next": ">=14.0.0",
|
|
36
|
+
"react": ">=18.0.0",
|
|
37
|
+
"react-dom": ">=18.0.0",
|
|
38
|
+
"tailwindcss": ">=3.4.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "^19",
|
|
42
|
+
"typescript": "^5"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { Card } from '../../components/ui/card'
|
|
5
|
+
import { Badge } from '../../components/ui/badge'
|
|
6
|
+
import { StatusDot } from './status-dot'
|
|
7
|
+
import { ProviderIcon } from './provider-icon'
|
|
8
|
+
import { formatDuration, formatCost } from '../../lib/format'
|
|
9
|
+
import { getWorkTypeConfig } from '../../lib/work-type-config'
|
|
10
|
+
import { getStatusConfig } from '../../lib/status-config'
|
|
11
|
+
import type { PublicSessionResponse } from '../../types/api'
|
|
12
|
+
|
|
13
|
+
interface AgentCardProps {
|
|
14
|
+
session: PublicSessionResponse
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function AgentCard({ session, className }: AgentCardProps) {
|
|
19
|
+
const statusConfig = getStatusConfig(session.status)
|
|
20
|
+
const workTypeConfig = getWorkTypeConfig(session.workType)
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Card
|
|
24
|
+
className={cn(
|
|
25
|
+
'relative p-4 transition-colors hover:border-af-surface-border/80',
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
<div className="flex items-start justify-between">
|
|
30
|
+
<div className="flex items-center gap-2">
|
|
31
|
+
<StatusDot status={session.status} showHeartbeat={session.status === 'working'} />
|
|
32
|
+
<span className="text-sm font-medium text-af-text-primary font-mono">
|
|
33
|
+
{session.identifier}
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
<ProviderIcon size={14} />
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="mt-3 flex items-center gap-2">
|
|
40
|
+
<Badge
|
|
41
|
+
variant="outline"
|
|
42
|
+
className={cn('text-xs', workTypeConfig.bgColor, workTypeConfig.color, 'border-transparent')}
|
|
43
|
+
>
|
|
44
|
+
{workTypeConfig.label}
|
|
45
|
+
</Badge>
|
|
46
|
+
<Badge
|
|
47
|
+
variant="outline"
|
|
48
|
+
className={cn('text-xs', statusConfig.bgColor, statusConfig.textColor, 'border-transparent')}
|
|
49
|
+
>
|
|
50
|
+
{statusConfig.label}
|
|
51
|
+
</Badge>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="mt-3 flex items-center justify-between text-xs text-af-text-secondary">
|
|
55
|
+
<span className="tabular-nums">{formatDuration(session.duration)}</span>
|
|
56
|
+
{session.costUsd != null && (
|
|
57
|
+
<span className="tabular-nums font-mono">{formatCost(session.costUsd)}</span>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</Card>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { useStats } from '../../hooks/use-stats'
|
|
5
|
+
import { useSessions } from '../../hooks/use-sessions'
|
|
6
|
+
import { StatCard } from './stat-card'
|
|
7
|
+
import { AgentCard } from './agent-card'
|
|
8
|
+
import { EmptyState } from '../../components/shared/empty-state'
|
|
9
|
+
import { Skeleton } from '../../components/ui/skeleton'
|
|
10
|
+
import { formatCost } from '../../lib/format'
|
|
11
|
+
import { Users, Cpu, ListTodo, CheckCircle2, Gauge, DollarSign } from 'lucide-react'
|
|
12
|
+
|
|
13
|
+
interface FleetOverviewProps {
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function FleetOverview({ className }: FleetOverviewProps) {
|
|
18
|
+
const { data: stats, isLoading: statsLoading } = useStats()
|
|
19
|
+
const { data: sessionsData, isLoading: sessionsLoading } = useSessions()
|
|
20
|
+
|
|
21
|
+
const sessions = sessionsData?.sessions ?? []
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn('space-y-6 p-5', className)}>
|
|
25
|
+
{/* Stats grid */}
|
|
26
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
|
|
27
|
+
<StatCard
|
|
28
|
+
label="Workers Online"
|
|
29
|
+
value={stats?.workersOnline ?? 0}
|
|
30
|
+
icon={<Users className="h-3.5 w-3.5" />}
|
|
31
|
+
loading={statsLoading}
|
|
32
|
+
/>
|
|
33
|
+
<StatCard
|
|
34
|
+
label="Agents Working"
|
|
35
|
+
value={stats?.agentsWorking ?? 0}
|
|
36
|
+
icon={<Cpu className="h-3.5 w-3.5" />}
|
|
37
|
+
loading={statsLoading}
|
|
38
|
+
/>
|
|
39
|
+
<StatCard
|
|
40
|
+
label="Queue Depth"
|
|
41
|
+
value={stats?.queueDepth ?? 0}
|
|
42
|
+
icon={<ListTodo className="h-3.5 w-3.5" />}
|
|
43
|
+
loading={statsLoading}
|
|
44
|
+
/>
|
|
45
|
+
<StatCard
|
|
46
|
+
label="Completed Today"
|
|
47
|
+
value={stats?.completedToday ?? 0}
|
|
48
|
+
icon={<CheckCircle2 className="h-3.5 w-3.5" />}
|
|
49
|
+
loading={statsLoading}
|
|
50
|
+
/>
|
|
51
|
+
<StatCard
|
|
52
|
+
label="Capacity"
|
|
53
|
+
value={stats?.availableCapacity ?? 0}
|
|
54
|
+
icon={<Gauge className="h-3.5 w-3.5" />}
|
|
55
|
+
loading={statsLoading}
|
|
56
|
+
/>
|
|
57
|
+
<StatCard
|
|
58
|
+
label="Cost Today"
|
|
59
|
+
value={formatCost(stats?.totalCostToday)}
|
|
60
|
+
icon={<DollarSign className="h-3.5 w-3.5" />}
|
|
61
|
+
loading={statsLoading}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Agent cards */}
|
|
66
|
+
<div>
|
|
67
|
+
<h2 className="mb-3 text-sm font-medium text-af-text-secondary">Active Sessions</h2>
|
|
68
|
+
{sessionsLoading ? (
|
|
69
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
70
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
71
|
+
<Skeleton key={i} className="h-32 rounded-lg" />
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
) : sessions.length === 0 ? (
|
|
75
|
+
<EmptyState
|
|
76
|
+
title="No active sessions"
|
|
77
|
+
description="Sessions will appear here when agents start working."
|
|
78
|
+
/>
|
|
79
|
+
) : (
|
|
80
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
81
|
+
{sessions.map((session) => (
|
|
82
|
+
<AgentCard key={session.id} session={session} />
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
|
|
3
|
+
interface ProviderIconProps {
|
|
4
|
+
provider?: string
|
|
5
|
+
className?: string
|
|
6
|
+
size?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ProviderIcon({ provider = 'claude', className, size = 16 }: ProviderIconProps) {
|
|
10
|
+
const name = provider.toLowerCase()
|
|
11
|
+
|
|
12
|
+
if (name === 'codex' || name === 'openai') {
|
|
13
|
+
return (
|
|
14
|
+
<svg
|
|
15
|
+
width={size}
|
|
16
|
+
height={size}
|
|
17
|
+
viewBox="0 0 24 24"
|
|
18
|
+
fill="none"
|
|
19
|
+
className={cn('text-emerald-400', className)}
|
|
20
|
+
>
|
|
21
|
+
<path
|
|
22
|
+
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
|
|
23
|
+
fill="currentColor"
|
|
24
|
+
/>
|
|
25
|
+
</svg>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (name === 'amp') {
|
|
30
|
+
return (
|
|
31
|
+
<svg
|
|
32
|
+
width={size}
|
|
33
|
+
height={size}
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
fill="none"
|
|
36
|
+
className={cn('text-yellow-400', className)}
|
|
37
|
+
>
|
|
38
|
+
<path d="M11 21h-1l1-7H7.5c-.88 0-.33-.75-.31-.78C8.48 10.94 10.42 7.54 13.01 3h1l-1 7h3.51c.4 0 .62.19.4.66C12.97 17.55 11 21 11 21z" fill="currentColor" />
|
|
39
|
+
</svg>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Default: Claude
|
|
44
|
+
return (
|
|
45
|
+
<svg
|
|
46
|
+
width={size}
|
|
47
|
+
height={size}
|
|
48
|
+
viewBox="0 0 24 24"
|
|
49
|
+
fill="none"
|
|
50
|
+
className={cn('text-af-accent', className)}
|
|
51
|
+
>
|
|
52
|
+
<path
|
|
53
|
+
d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
strokeWidth="2"
|
|
56
|
+
strokeLinecap="round"
|
|
57
|
+
strokeLinejoin="round"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
import { Card } from '../../components/ui/card'
|
|
3
|
+
import { Skeleton } from '../../components/ui/skeleton'
|
|
4
|
+
|
|
5
|
+
interface StatCardProps {
|
|
6
|
+
label: string
|
|
7
|
+
value: string | number
|
|
8
|
+
detail?: string
|
|
9
|
+
icon?: React.ReactNode
|
|
10
|
+
trend?: 'up' | 'down' | 'neutral'
|
|
11
|
+
loading?: boolean
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function StatCard({ label, value, detail, icon, loading, className }: StatCardProps) {
|
|
16
|
+
if (loading) {
|
|
17
|
+
return (
|
|
18
|
+
<Card className={cn('p-4', className)}>
|
|
19
|
+
<div className="flex items-center justify-between">
|
|
20
|
+
<Skeleton className="h-3 w-16" />
|
|
21
|
+
<Skeleton className="h-4 w-4 rounded" />
|
|
22
|
+
</div>
|
|
23
|
+
<Skeleton className="mt-2 h-7 w-12" />
|
|
24
|
+
<Skeleton className="mt-1 h-3 w-20" />
|
|
25
|
+
</Card>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Card className={cn('p-4', className)}>
|
|
31
|
+
<div className="flex items-center justify-between">
|
|
32
|
+
<span className="text-xs font-medium text-af-text-secondary">{label}</span>
|
|
33
|
+
{icon && <span className="text-af-text-secondary">{icon}</span>}
|
|
34
|
+
</div>
|
|
35
|
+
<div className="mt-1.5 text-2xl font-bold tabular-nums text-af-text-primary">
|
|
36
|
+
{value}
|
|
37
|
+
</div>
|
|
38
|
+
{detail && (
|
|
39
|
+
<p className="mt-0.5 text-xs text-af-text-secondary">{detail}</p>
|
|
40
|
+
)}
|
|
41
|
+
</Card>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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.5 w-2.5', className)}>
|
|
15
|
+
{config.animate && showHeartbeat && (
|
|
16
|
+
<span
|
|
17
|
+
className={cn(
|
|
18
|
+
'absolute inset-0 rounded-full animate-heartbeat',
|
|
19
|
+
config.dotColor
|
|
20
|
+
)}
|
|
21
|
+
/>
|
|
22
|
+
)}
|
|
23
|
+
<span
|
|
24
|
+
className={cn(
|
|
25
|
+
'relative inline-flex h-2.5 w-2.5 rounded-full',
|
|
26
|
+
config.dotColor,
|
|
27
|
+
config.animate && 'animate-pulse-dot'
|
|
28
|
+
)}
|
|
29
|
+
/>
|
|
30
|
+
</span>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { useStats } from '../../hooks/use-stats'
|
|
5
|
+
import { formatCost } from '../../lib/format'
|
|
6
|
+
|
|
7
|
+
interface BottomBarProps {
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function BottomBar({ className }: BottomBarProps) {
|
|
12
|
+
const { data } = useStats()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<footer
|
|
16
|
+
className={cn(
|
|
17
|
+
'flex h-8 items-center justify-between border-t border-af-surface-border bg-af-bg-secondary px-5',
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
<div className="flex items-center gap-4 text-xs text-af-text-secondary">
|
|
22
|
+
<span>
|
|
23
|
+
Completed today:{' '}
|
|
24
|
+
<span className="font-medium text-af-text-primary">{data?.completedToday ?? 0}</span>
|
|
25
|
+
</span>
|
|
26
|
+
<span>
|
|
27
|
+
Capacity:{' '}
|
|
28
|
+
<span className="font-medium text-af-text-primary">{data?.availableCapacity ?? 0}</span>
|
|
29
|
+
</span>
|
|
30
|
+
{data?.totalCostToday != null && (
|
|
31
|
+
<span>
|
|
32
|
+
Cost today:{' '}
|
|
33
|
+
<span className="font-medium text-af-text-primary">
|
|
34
|
+
{formatCost(data.totalCostToday)}
|
|
35
|
+
</span>
|
|
36
|
+
</span>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<a
|
|
41
|
+
href="https://github.com/supaku/agentfactory"
|
|
42
|
+
target="_blank"
|
|
43
|
+
rel="noopener noreferrer"
|
|
44
|
+
className="text-xs text-af-text-secondary hover:text-af-text-primary transition-colors"
|
|
45
|
+
>
|
|
46
|
+
Powered by AgentFactory
|
|
47
|
+
</a>
|
|
48
|
+
</footer>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
|
|
13
|
+
interface DashboardShellProps {
|
|
14
|
+
children: React.ReactNode
|
|
15
|
+
currentPath?: string
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function DashboardShell({ children, currentPath = '/', className }: DashboardShellProps) {
|
|
20
|
+
return (
|
|
21
|
+
<TooltipProvider>
|
|
22
|
+
<div className={cn('flex h-screen bg-af-bg-primary', className)}>
|
|
23
|
+
{/* Desktop sidebar */}
|
|
24
|
+
<div className="hidden md:block">
|
|
25
|
+
<Sidebar currentPath={currentPath} />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{/* Main content area */}
|
|
29
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
30
|
+
{/* Mobile header with hamburger */}
|
|
31
|
+
<div className="flex items-center gap-2 border-b border-af-surface-border bg-af-bg-secondary px-3 py-2 md:hidden">
|
|
32
|
+
<Sheet>
|
|
33
|
+
<SheetTrigger asChild>
|
|
34
|
+
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
35
|
+
<Menu className="h-4 w-4" />
|
|
36
|
+
</Button>
|
|
37
|
+
</SheetTrigger>
|
|
38
|
+
<SheetContent side="left" className="w-56 p-0">
|
|
39
|
+
<SheetTitle className="sr-only">Navigation</SheetTitle>
|
|
40
|
+
<Sidebar currentPath={currentPath} />
|
|
41
|
+
</SheetContent>
|
|
42
|
+
</Sheet>
|
|
43
|
+
<span className="text-sm font-semibold text-af-text-primary">AgentFactory</span>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<TopBar className="hidden md:flex" />
|
|
47
|
+
|
|
48
|
+
<main className="flex-1 overflow-auto">
|
|
49
|
+
{children}
|
|
50
|
+
</main>
|
|
51
|
+
|
|
52
|
+
<BottomBar />
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</TooltipProvider>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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 } 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-56 flex-col border-r border-af-surface-border bg-af-bg-secondary',
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<div className="flex items-center gap-2.5 px-5 py-4">
|
|
41
|
+
<Logo size={24} />
|
|
42
|
+
<span className="text-sm font-semibold text-af-text-primary tracking-tight">
|
|
43
|
+
AgentFactory
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<Separator />
|
|
48
|
+
|
|
49
|
+
<nav className="flex-1 space-y-0.5 px-3 py-3">
|
|
50
|
+
{navItems.map((item) => (
|
|
51
|
+
<a
|
|
52
|
+
key={item.href}
|
|
53
|
+
href={item.href}
|
|
54
|
+
className={cn(
|
|
55
|
+
'flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors',
|
|
56
|
+
isActive(item.href)
|
|
57
|
+
? 'bg-af-surface text-af-text-primary'
|
|
58
|
+
: 'text-af-text-secondary hover:bg-af-surface/50 hover:text-af-text-primary'
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{item.icon}
|
|
62
|
+
{item.label}
|
|
63
|
+
</a>
|
|
64
|
+
))}
|
|
65
|
+
</nav>
|
|
66
|
+
|
|
67
|
+
<Separator />
|
|
68
|
+
|
|
69
|
+
<div className="px-5 py-3">
|
|
70
|
+
<a
|
|
71
|
+
href="https://github.com/supaku/agentfactory"
|
|
72
|
+
target="_blank"
|
|
73
|
+
rel="noopener noreferrer"
|
|
74
|
+
className="text-xs text-af-text-secondary hover:text-af-text-primary transition-colors"
|
|
75
|
+
>
|
|
76
|
+
AgentFactory
|
|
77
|
+
</a>
|
|
78
|
+
</div>
|
|
79
|
+
</aside>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
|
|
8
|
+
interface TopBarProps {
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function TopBar({ className }: TopBarProps) {
|
|
13
|
+
const { data, isLoading } = useStats()
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<header
|
|
17
|
+
className={cn(
|
|
18
|
+
'flex h-11 items-center justify-between border-b border-af-surface-border bg-af-bg-secondary px-5',
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
<div className="flex items-center gap-4">
|
|
23
|
+
{isLoading ? (
|
|
24
|
+
<>
|
|
25
|
+
<Skeleton className="h-4 w-24" />
|
|
26
|
+
<Skeleton className="h-4 w-20" />
|
|
27
|
+
<Skeleton className="h-4 w-20" />
|
|
28
|
+
</>
|
|
29
|
+
) : (
|
|
30
|
+
<>
|
|
31
|
+
<div className="flex items-center gap-1.5">
|
|
32
|
+
<StatusDot status={data?.workersOnline ? 'working' : 'stopped'} showHeartbeat />
|
|
33
|
+
<span className="text-xs text-af-text-secondary">
|
|
34
|
+
<span className="font-medium text-af-text-primary">{data?.workersOnline ?? 0}</span>{' '}
|
|
35
|
+
{data?.workersOnline === 1 ? 'worker' : 'workers'} online
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="text-xs text-af-text-secondary">
|
|
39
|
+
<span className="font-medium text-af-text-primary">{data?.agentsWorking ?? 0}</span> working
|
|
40
|
+
</div>
|
|
41
|
+
<div className="text-xs text-af-text-secondary">
|
|
42
|
+
<span className="font-medium text-af-text-primary">{data?.queueDepth ?? 0}</span> queued
|
|
43
|
+
</div>
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="text-xs text-af-text-secondary">
|
|
49
|
+
{data?.timestamp && (
|
|
50
|
+
<span>Updated {new Date(data.timestamp).toLocaleTimeString()}</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</header>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
|
|
8
|
+
interface PipelineCardProps {
|
|
9
|
+
session: PublicSessionResponse
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PipelineCard({ session, className }: PipelineCardProps) {
|
|
14
|
+
const workTypeConfig = getWorkTypeConfig(session.workType)
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={cn(
|
|
19
|
+
'rounded-md border border-af-surface-border bg-af-surface p-3 transition-colors hover:border-af-surface-border/80',
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
<div className="flex items-center gap-2">
|
|
24
|
+
<StatusDot status={session.status} />
|
|
25
|
+
<span className="text-xs font-medium text-af-text-primary font-mono truncate">
|
|
26
|
+
{session.identifier}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="mt-2 flex items-center justify-between">
|
|
30
|
+
<Badge
|
|
31
|
+
variant="outline"
|
|
32
|
+
className={cn('text-xs', workTypeConfig.bgColor, workTypeConfig.color, 'border-transparent')}
|
|
33
|
+
>
|
|
34
|
+
{workTypeConfig.label}
|
|
35
|
+
</Badge>
|
|
36
|
+
<span className="text-xs text-af-text-secondary tabular-nums">
|
|
37
|
+
{formatDuration(session.duration)}
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|