@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
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
accentColor?: string
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function PipelineColumn({ title, sessions, count, accentColor, className }: PipelineColumnProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className={cn(
|
|
18
|
+
'flex w-72 shrink-0 flex-col rounded-lg border border-af-surface-border bg-af-bg-secondary',
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
<div className="flex items-center justify-between px-3 py-2.5">
|
|
23
|
+
<div className="flex items-center gap-2">
|
|
24
|
+
{accentColor && (
|
|
25
|
+
<span className={cn('h-2 w-2 rounded-full', accentColor)} />
|
|
26
|
+
)}
|
|
27
|
+
<span className="text-xs font-medium text-af-text-primary">{title}</span>
|
|
28
|
+
</div>
|
|
29
|
+
<span className="text-xs tabular-nums text-af-text-secondary">{count}</span>
|
|
30
|
+
</div>
|
|
31
|
+
<ScrollArea className="flex-1 px-2 pb-2">
|
|
32
|
+
<div className="space-y-2">
|
|
33
|
+
{sessions.map((session) => (
|
|
34
|
+
<PipelineCard key={session.id} session={session} />
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
</ScrollArea>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
accentColor: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const columns: ColumnDef[] = [
|
|
17
|
+
{ title: 'Backlog', statuses: ['queued', 'parked'], accentColor: 'bg-af-text-secondary' },
|
|
18
|
+
{ title: 'Started', statuses: ['working'], accentColor: 'bg-af-status-success' },
|
|
19
|
+
{ title: 'Finished', statuses: ['completed'], accentColor: 'bg-blue-400' },
|
|
20
|
+
{ title: 'Failed', statuses: ['failed'], accentColor: 'bg-af-status-error' },
|
|
21
|
+
{ title: 'Stopped', statuses: ['stopped'], accentColor: 'bg-af-status-warning' },
|
|
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
|
+
}
|
|
34
|
+
|
|
35
|
+
export function PipelineView({ className }: PipelineViewProps) {
|
|
36
|
+
const { data, isLoading } = useSessions()
|
|
37
|
+
const sessions = data?.sessions ?? []
|
|
38
|
+
|
|
39
|
+
if (isLoading) {
|
|
40
|
+
return (
|
|
41
|
+
<div className={cn('flex gap-4 overflow-x-auto p-5', className)}>
|
|
42
|
+
{columns.map((col) => (
|
|
43
|
+
<Skeleton key={col.title} className="h-96 w-72 shrink-0 rounded-lg" />
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (sessions.length === 0) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="p-5">
|
|
52
|
+
<EmptyState
|
|
53
|
+
title="No pipeline data"
|
|
54
|
+
description="Sessions will populate the pipeline as agents work on issues."
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const grouped = groupByColumn(sessions)
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={cn('flex gap-4 overflow-x-auto p-5', className)}>
|
|
64
|
+
{grouped.map((col) => (
|
|
65
|
+
<PipelineColumn
|
|
66
|
+
key={col.title}
|
|
67
|
+
title={col.title}
|
|
68
|
+
sessions={col.sessions}
|
|
69
|
+
count={col.sessions.length}
|
|
70
|
+
accentColor={col.accentColor}
|
|
71
|
+
/>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card'
|
|
5
|
+
import { Badge } from '../../components/ui/badge'
|
|
6
|
+
import { StatusDot } from '../../components/fleet/status-dot'
|
|
7
|
+
import { SessionTimeline, type TimelineEvent } from './session-timeline'
|
|
8
|
+
import { TokenChart } from './token-chart'
|
|
9
|
+
import { formatDuration, formatCost } from '../../lib/format'
|
|
10
|
+
import { getStatusConfig } from '../../lib/status-config'
|
|
11
|
+
import { getWorkTypeConfig } from '../../lib/work-type-config'
|
|
12
|
+
import type { PublicSessionResponse } from '../../types/api'
|
|
13
|
+
import { ArrowLeft } from 'lucide-react'
|
|
14
|
+
import { Button } from '../../components/ui/button'
|
|
15
|
+
|
|
16
|
+
interface SessionDetailProps {
|
|
17
|
+
session: PublicSessionResponse
|
|
18
|
+
onBack?: () => void
|
|
19
|
+
className?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SessionDetail({ session, onBack, className }: SessionDetailProps) {
|
|
23
|
+
const statusConfig = getStatusConfig(session.status)
|
|
24
|
+
const workTypeConfig = getWorkTypeConfig(session.workType)
|
|
25
|
+
|
|
26
|
+
// Synthesize timeline from available data
|
|
27
|
+
const events: TimelineEvent[] = [
|
|
28
|
+
{
|
|
29
|
+
id: 'started',
|
|
30
|
+
label: 'Session started',
|
|
31
|
+
timestamp: session.startedAt,
|
|
32
|
+
type: 'info',
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
if (session.status === 'completed') {
|
|
37
|
+
const endTime = new Date(new Date(session.startedAt).getTime() + session.duration * 1000)
|
|
38
|
+
events.push({
|
|
39
|
+
id: 'completed',
|
|
40
|
+
label: 'Session completed',
|
|
41
|
+
timestamp: endTime.toISOString(),
|
|
42
|
+
type: 'success',
|
|
43
|
+
})
|
|
44
|
+
} else if (session.status === 'failed') {
|
|
45
|
+
const endTime = new Date(new Date(session.startedAt).getTime() + session.duration * 1000)
|
|
46
|
+
events.push({
|
|
47
|
+
id: 'failed',
|
|
48
|
+
label: 'Session failed',
|
|
49
|
+
timestamp: endTime.toISOString(),
|
|
50
|
+
type: 'error',
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={cn('space-y-4 p-5', className)}>
|
|
56
|
+
{onBack && (
|
|
57
|
+
<Button variant="ghost" size="sm" onClick={onBack} className="gap-1.5 text-af-text-secondary">
|
|
58
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
59
|
+
Back
|
|
60
|
+
</Button>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{/* Header */}
|
|
64
|
+
<div className="flex items-center gap-3">
|
|
65
|
+
<StatusDot status={session.status} showHeartbeat={session.status === 'working'} />
|
|
66
|
+
<h1 className="text-lg font-semibold font-mono text-af-text-primary">
|
|
67
|
+
{session.identifier}
|
|
68
|
+
</h1>
|
|
69
|
+
<Badge
|
|
70
|
+
variant="outline"
|
|
71
|
+
className={cn('text-xs', statusConfig.bgColor, statusConfig.textColor, 'border-transparent')}
|
|
72
|
+
>
|
|
73
|
+
{statusConfig.label}
|
|
74
|
+
</Badge>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
|
78
|
+
{/* Left: Details */}
|
|
79
|
+
<Card className="lg:col-span-2">
|
|
80
|
+
<CardHeader>
|
|
81
|
+
<CardTitle>Session Details</CardTitle>
|
|
82
|
+
</CardHeader>
|
|
83
|
+
<CardContent>
|
|
84
|
+
<dl className="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
|
85
|
+
<div>
|
|
86
|
+
<dt className="text-af-text-secondary">Work Type</dt>
|
|
87
|
+
<dd className="mt-0.5">
|
|
88
|
+
<Badge
|
|
89
|
+
variant="outline"
|
|
90
|
+
className={cn(workTypeConfig.bgColor, workTypeConfig.color, 'border-transparent')}
|
|
91
|
+
>
|
|
92
|
+
{workTypeConfig.label}
|
|
93
|
+
</Badge>
|
|
94
|
+
</dd>
|
|
95
|
+
</div>
|
|
96
|
+
<div>
|
|
97
|
+
<dt className="text-af-text-secondary">Duration</dt>
|
|
98
|
+
<dd className="mt-0.5 tabular-nums text-af-text-primary">{formatDuration(session.duration)}</dd>
|
|
99
|
+
</div>
|
|
100
|
+
<div>
|
|
101
|
+
<dt className="text-af-text-secondary">Cost</dt>
|
|
102
|
+
<dd className="mt-0.5 tabular-nums font-mono text-af-text-primary">
|
|
103
|
+
{formatCost(session.costUsd)}
|
|
104
|
+
</dd>
|
|
105
|
+
</div>
|
|
106
|
+
<div>
|
|
107
|
+
<dt className="text-af-text-secondary">Started</dt>
|
|
108
|
+
<dd className="mt-0.5 text-af-text-primary">
|
|
109
|
+
{new Date(session.startedAt).toLocaleString()}
|
|
110
|
+
</dd>
|
|
111
|
+
</div>
|
|
112
|
+
</dl>
|
|
113
|
+
|
|
114
|
+
<div className="mt-6">
|
|
115
|
+
<TokenChart />
|
|
116
|
+
</div>
|
|
117
|
+
</CardContent>
|
|
118
|
+
</Card>
|
|
119
|
+
|
|
120
|
+
{/* Right: Timeline */}
|
|
121
|
+
<Card>
|
|
122
|
+
<CardHeader>
|
|
123
|
+
<CardTitle>Timeline</CardTitle>
|
|
124
|
+
</CardHeader>
|
|
125
|
+
<CardContent>
|
|
126
|
+
<SessionTimeline events={events} />
|
|
127
|
+
</CardContent>
|
|
128
|
+
</Card>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { useSessions } from '../../hooks/use-sessions'
|
|
5
|
+
import { StatusDot } from '../../components/fleet/status-dot'
|
|
6
|
+
import { Badge } from '../../components/ui/badge'
|
|
7
|
+
import { Skeleton } from '../../components/ui/skeleton'
|
|
8
|
+
import { EmptyState } from '../../components/shared/empty-state'
|
|
9
|
+
import { formatDuration, formatCost, formatRelativeTime } from '../../lib/format'
|
|
10
|
+
import { getWorkTypeConfig } from '../../lib/work-type-config'
|
|
11
|
+
import { getStatusConfig } from '../../lib/status-config'
|
|
12
|
+
|
|
13
|
+
interface SessionListProps {
|
|
14
|
+
onSelect?: (sessionId: string) => void
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SessionList({ onSelect, className }: SessionListProps) {
|
|
19
|
+
const { data, isLoading } = useSessions()
|
|
20
|
+
const sessions = data?.sessions ?? []
|
|
21
|
+
|
|
22
|
+
if (isLoading) {
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn('space-y-2 p-5', className)}>
|
|
25
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
26
|
+
<Skeleton key={i} className="h-14 rounded-lg" />
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (sessions.length === 0) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="p-5">
|
|
35
|
+
<EmptyState
|
|
36
|
+
title="No sessions"
|
|
37
|
+
description="Agent sessions will appear here once they start."
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn('p-5', className)}>
|
|
45
|
+
<div className="rounded-lg border border-af-surface-border overflow-hidden">
|
|
46
|
+
<table className="w-full text-sm">
|
|
47
|
+
<thead>
|
|
48
|
+
<tr className="border-b border-af-surface-border bg-af-bg-secondary">
|
|
49
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-af-text-secondary">Issue</th>
|
|
50
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-af-text-secondary">Status</th>
|
|
51
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-af-text-secondary">Type</th>
|
|
52
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-af-text-secondary">Duration</th>
|
|
53
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-af-text-secondary">Cost</th>
|
|
54
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-af-text-secondary">Started</th>
|
|
55
|
+
</tr>
|
|
56
|
+
</thead>
|
|
57
|
+
<tbody>
|
|
58
|
+
{sessions.map((session) => {
|
|
59
|
+
const statusConfig = getStatusConfig(session.status)
|
|
60
|
+
const workTypeConfig = getWorkTypeConfig(session.workType)
|
|
61
|
+
return (
|
|
62
|
+
<tr
|
|
63
|
+
key={session.id}
|
|
64
|
+
className="border-b border-af-surface-border last:border-0 hover:bg-af-surface/50 cursor-pointer transition-colors"
|
|
65
|
+
onClick={() => onSelect?.(session.id)}
|
|
66
|
+
>
|
|
67
|
+
<td className="px-4 py-2.5">
|
|
68
|
+
<span className="font-mono text-sm text-af-text-primary">{session.identifier}</span>
|
|
69
|
+
</td>
|
|
70
|
+
<td className="px-4 py-2.5">
|
|
71
|
+
<div className="flex items-center gap-1.5">
|
|
72
|
+
<StatusDot status={session.status} />
|
|
73
|
+
<span className={cn('text-xs', statusConfig.textColor)}>
|
|
74
|
+
{statusConfig.label}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
</td>
|
|
78
|
+
<td className="px-4 py-2.5">
|
|
79
|
+
<Badge
|
|
80
|
+
variant="outline"
|
|
81
|
+
className={cn('text-xs', workTypeConfig.bgColor, workTypeConfig.color, 'border-transparent')}
|
|
82
|
+
>
|
|
83
|
+
{workTypeConfig.label}
|
|
84
|
+
</Badge>
|
|
85
|
+
</td>
|
|
86
|
+
<td className="px-4 py-2.5">
|
|
87
|
+
<span className="text-xs text-af-text-secondary tabular-nums">
|
|
88
|
+
{formatDuration(session.duration)}
|
|
89
|
+
</span>
|
|
90
|
+
</td>
|
|
91
|
+
<td className="px-4 py-2.5">
|
|
92
|
+
<span className="text-xs text-af-text-secondary tabular-nums font-mono">
|
|
93
|
+
{formatCost(session.costUsd)}
|
|
94
|
+
</span>
|
|
95
|
+
</td>
|
|
96
|
+
<td className="px-4 py-2.5">
|
|
97
|
+
<span className="text-xs text-af-text-secondary">
|
|
98
|
+
{formatRelativeTime(session.startedAt)}
|
|
99
|
+
</span>
|
|
100
|
+
</td>
|
|
101
|
+
</tr>
|
|
102
|
+
)
|
|
103
|
+
})}
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
|
|
3
|
+
export interface TimelineEvent {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
timestamp: string
|
|
7
|
+
detail?: string
|
|
8
|
+
type?: 'info' | 'success' | 'warning' | 'error'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SessionTimelineProps {
|
|
12
|
+
events: TimelineEvent[]
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const typeColors = {
|
|
17
|
+
info: 'bg-blue-400',
|
|
18
|
+
success: 'bg-af-status-success',
|
|
19
|
+
warning: 'bg-af-status-warning',
|
|
20
|
+
error: 'bg-af-status-error',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SessionTimeline({ events, className }: SessionTimelineProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div className={cn('relative space-y-0', className)}>
|
|
26
|
+
{/* Vertical line */}
|
|
27
|
+
<div className="absolute left-[7px] top-2 bottom-2 w-px bg-af-surface-border" />
|
|
28
|
+
|
|
29
|
+
{events.map((event, i) => (
|
|
30
|
+
<div key={event.id} className="relative flex gap-3 py-2">
|
|
31
|
+
{/* Dot */}
|
|
32
|
+
<span
|
|
33
|
+
className={cn(
|
|
34
|
+
'relative z-10 mt-1 h-[14px] w-[14px] shrink-0 rounded-full border-2 border-af-bg-primary',
|
|
35
|
+
typeColors[event.type ?? 'info']
|
|
36
|
+
)}
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<div className="flex-1 min-w-0">
|
|
40
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
41
|
+
<span className="text-sm text-af-text-primary">{event.label}</span>
|
|
42
|
+
<span className="text-xs text-af-text-secondary whitespace-nowrap">
|
|
43
|
+
{new Date(event.timestamp).toLocaleTimeString()}
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
{event.detail && (
|
|
47
|
+
<p className="mt-0.5 text-xs text-af-text-secondary">{event.detail}</p>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
import { formatTokens } from '../../lib/format'
|
|
3
|
+
|
|
4
|
+
interface TokenChartProps {
|
|
5
|
+
inputTokens?: number
|
|
6
|
+
outputTokens?: number
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TokenChart({ inputTokens = 0, outputTokens = 0, className }: TokenChartProps) {
|
|
11
|
+
const total = inputTokens + outputTokens
|
|
12
|
+
if (total === 0) return null
|
|
13
|
+
|
|
14
|
+
const inputPct = (inputTokens / total) * 100
|
|
15
|
+
const outputPct = (outputTokens / total) * 100
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={cn('space-y-2', className)}>
|
|
19
|
+
<div className="flex items-center justify-between text-xs text-af-text-secondary">
|
|
20
|
+
<span>Token Usage</span>
|
|
21
|
+
<span className="tabular-nums font-mono">{formatTokens(total)} total</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{/* Bar */}
|
|
25
|
+
<div className="flex h-3 w-full overflow-hidden rounded-full bg-af-bg-primary">
|
|
26
|
+
<div
|
|
27
|
+
className="bg-blue-400 transition-all"
|
|
28
|
+
style={{ width: `${inputPct}%` }}
|
|
29
|
+
/>
|
|
30
|
+
<div
|
|
31
|
+
className="bg-af-accent transition-all"
|
|
32
|
+
style={{ width: `${outputPct}%` }}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
{/* Legend */}
|
|
37
|
+
<div className="flex items-center gap-4 text-xs">
|
|
38
|
+
<div className="flex items-center gap-1.5">
|
|
39
|
+
<span className="h-2 w-2 rounded-full bg-blue-400" />
|
|
40
|
+
<span className="text-af-text-secondary">Input</span>
|
|
41
|
+
<span className="tabular-nums font-mono text-af-text-primary">{formatTokens(inputTokens)}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div className="flex items-center gap-1.5">
|
|
44
|
+
<span className="h-2 w-2 rounded-full bg-af-accent" />
|
|
45
|
+
<span className="text-af-text-secondary">Output</span>
|
|
46
|
+
<span className="tabular-nums font-mono text-af-text-primary">{formatTokens(outputTokens)}</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../../components/ui/card'
|
|
5
|
+
import { Badge } from '../../components/ui/badge'
|
|
6
|
+
import { Separator } from '../../components/ui/separator'
|
|
7
|
+
import { useStats } from '../../hooks/use-stats'
|
|
8
|
+
import { useWorkers } from '../../hooks/use-workers'
|
|
9
|
+
import { ProviderIcon } from '../../components/fleet/provider-icon'
|
|
10
|
+
import { StatusDot } from '../../components/fleet/status-dot'
|
|
11
|
+
import { CheckCircle2, AlertCircle, Settings2 } from 'lucide-react'
|
|
12
|
+
|
|
13
|
+
interface SettingsViewProps {
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function SettingsView({ className }: SettingsViewProps) {
|
|
18
|
+
const { data: stats } = useStats()
|
|
19
|
+
const { data: workersData } = useWorkers()
|
|
20
|
+
|
|
21
|
+
const workers = workersData?.workers ?? []
|
|
22
|
+
const hasWorkerAuth = workersData !== null
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className={cn('space-y-6 p-5 max-w-3xl', className)}>
|
|
26
|
+
<div>
|
|
27
|
+
<h1 className="text-lg font-semibold text-af-text-primary">Settings</h1>
|
|
28
|
+
<p className="text-sm text-af-text-secondary">
|
|
29
|
+
Configuration and integration status for your AgentFactory instance.
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Integration Status */}
|
|
34
|
+
<Card>
|
|
35
|
+
<CardHeader>
|
|
36
|
+
<CardTitle>Integration Status</CardTitle>
|
|
37
|
+
<CardDescription>Connected services and API endpoints</CardDescription>
|
|
38
|
+
</CardHeader>
|
|
39
|
+
<CardContent className="space-y-4">
|
|
40
|
+
<div className="flex items-center justify-between">
|
|
41
|
+
<div className="flex items-center gap-3">
|
|
42
|
+
<CheckCircle2 className="h-4 w-4 text-af-status-success" />
|
|
43
|
+
<div>
|
|
44
|
+
<p className="text-sm text-af-text-primary">Linear Webhook</p>
|
|
45
|
+
<p className="text-xs text-af-text-secondary font-mono">/webhook</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<Badge variant="success">Connected</Badge>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<Separator />
|
|
52
|
+
|
|
53
|
+
<div className="flex items-center justify-between">
|
|
54
|
+
<div className="flex items-center gap-3">
|
|
55
|
+
<CheckCircle2 className="h-4 w-4 text-af-status-success" />
|
|
56
|
+
<div>
|
|
57
|
+
<p className="text-sm text-af-text-primary">Public API</p>
|
|
58
|
+
<p className="text-xs text-af-text-secondary font-mono">/api/public/stats</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<Badge variant="success">Active</Badge>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<Separator />
|
|
65
|
+
|
|
66
|
+
<div className="flex items-center justify-between">
|
|
67
|
+
<div className="flex items-center gap-3">
|
|
68
|
+
{hasWorkerAuth ? (
|
|
69
|
+
<CheckCircle2 className="h-4 w-4 text-af-status-success" />
|
|
70
|
+
) : (
|
|
71
|
+
<AlertCircle className="h-4 w-4 text-af-text-secondary" />
|
|
72
|
+
)}
|
|
73
|
+
<div>
|
|
74
|
+
<p className="text-sm text-af-text-primary">Worker API</p>
|
|
75
|
+
<p className="text-xs text-af-text-secondary font-mono">/api/workers</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<Badge variant={hasWorkerAuth ? 'success' : 'secondary'}>
|
|
79
|
+
{hasWorkerAuth ? 'Authenticated' : 'No Auth Key'}
|
|
80
|
+
</Badge>
|
|
81
|
+
</div>
|
|
82
|
+
</CardContent>
|
|
83
|
+
</Card>
|
|
84
|
+
|
|
85
|
+
{/* Workers */}
|
|
86
|
+
<Card>
|
|
87
|
+
<CardHeader>
|
|
88
|
+
<CardTitle>Workers</CardTitle>
|
|
89
|
+
<CardDescription>
|
|
90
|
+
{workers.length > 0
|
|
91
|
+
? `${workers.length} worker${workers.length !== 1 ? 's' : ''} registered`
|
|
92
|
+
: 'No workers connected'}
|
|
93
|
+
</CardDescription>
|
|
94
|
+
</CardHeader>
|
|
95
|
+
<CardContent>
|
|
96
|
+
{workers.length === 0 ? (
|
|
97
|
+
<p className="text-sm text-af-text-secondary">
|
|
98
|
+
Workers will appear here once they register with the server.
|
|
99
|
+
</p>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="space-y-3">
|
|
102
|
+
{workers.map((worker) => (
|
|
103
|
+
<div key={worker.id} className="flex items-center justify-between">
|
|
104
|
+
<div className="flex items-center gap-3">
|
|
105
|
+
<StatusDot status={worker.status === 'active' ? 'working' : 'stopped'} />
|
|
106
|
+
<div>
|
|
107
|
+
<p className="text-sm font-mono text-af-text-primary">
|
|
108
|
+
{worker.hostname ?? worker.id.slice(0, 8)}
|
|
109
|
+
</p>
|
|
110
|
+
<p className="text-xs text-af-text-secondary">
|
|
111
|
+
{worker.activeSessions}/{worker.capacity} slots
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<ProviderIcon provider={worker.provider} size={14} />
|
|
117
|
+
<Badge variant={worker.status === 'active' ? 'success' : 'secondary'}>
|
|
118
|
+
{worker.status}
|
|
119
|
+
</Badge>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</CardContent>
|
|
126
|
+
</Card>
|
|
127
|
+
|
|
128
|
+
{/* Fleet Stats */}
|
|
129
|
+
<Card>
|
|
130
|
+
<CardHeader>
|
|
131
|
+
<CardTitle className="flex items-center gap-2">
|
|
132
|
+
<Settings2 className="h-4 w-4" />
|
|
133
|
+
Fleet Configuration
|
|
134
|
+
</CardTitle>
|
|
135
|
+
</CardHeader>
|
|
136
|
+
<CardContent>
|
|
137
|
+
<dl className="grid grid-cols-2 gap-4 text-sm">
|
|
138
|
+
<div>
|
|
139
|
+
<dt className="text-af-text-secondary">Total Capacity</dt>
|
|
140
|
+
<dd className="mt-0.5 text-af-text-primary font-medium">
|
|
141
|
+
{stats?.availableCapacity ?? '—'}
|
|
142
|
+
</dd>
|
|
143
|
+
</div>
|
|
144
|
+
<div>
|
|
145
|
+
<dt className="text-af-text-secondary">Workers Online</dt>
|
|
146
|
+
<dd className="mt-0.5 text-af-text-primary font-medium">
|
|
147
|
+
{stats?.workersOnline ?? '—'}
|
|
148
|
+
</dd>
|
|
149
|
+
</div>
|
|
150
|
+
</dl>
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
import { Inbox } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface EmptyStateProps {
|
|
5
|
+
title?: string
|
|
6
|
+
description?: string
|
|
7
|
+
icon?: React.ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
children?: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function EmptyState({
|
|
13
|
+
title = 'No data',
|
|
14
|
+
description = 'Nothing to show yet.',
|
|
15
|
+
icon,
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
}: EmptyStateProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn('flex flex-col items-center justify-center py-12 text-center', className)}>
|
|
21
|
+
<div className="mb-4 text-af-text-secondary">
|
|
22
|
+
{icon ?? <Inbox className="h-10 w-10" />}
|
|
23
|
+
</div>
|
|
24
|
+
<h3 className="text-sm font-medium text-af-text-primary">{title}</h3>
|
|
25
|
+
<p className="mt-1 text-sm text-af-text-secondary">{description}</p>
|
|
26
|
+
{children && <div className="mt-4">{children}</div>}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|