@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,153 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { Badge } from '../../components/ui/badge'
|
|
5
|
+
import { StatusDot } from '../../components/fleet/status-dot'
|
|
6
|
+
import { SessionTimeline, type TimelineEvent } from './session-timeline'
|
|
7
|
+
import { TokenChart } from './token-chart'
|
|
8
|
+
import { formatDuration, formatCost } from '../../lib/format'
|
|
9
|
+
import { getStatusConfig } from '../../lib/status-config'
|
|
10
|
+
import { getWorkTypeConfig } from '../../lib/work-type-config'
|
|
11
|
+
import type { PublicSessionResponse } from '../../types/api'
|
|
12
|
+
import { ArrowLeft, Clock, Coins, Calendar, Tag } from 'lucide-react'
|
|
13
|
+
import { Button } from '../../components/ui/button'
|
|
14
|
+
|
|
15
|
+
interface SessionDetailProps {
|
|
16
|
+
session: PublicSessionResponse
|
|
17
|
+
onBack?: () => void
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function SessionDetail({ session, onBack, className }: SessionDetailProps) {
|
|
22
|
+
const statusConfig = getStatusConfig(session.status)
|
|
23
|
+
const workTypeConfig = getWorkTypeConfig(session.workType)
|
|
24
|
+
|
|
25
|
+
const events: TimelineEvent[] = [
|
|
26
|
+
{
|
|
27
|
+
id: 'started',
|
|
28
|
+
label: 'Session started',
|
|
29
|
+
timestamp: session.startedAt,
|
|
30
|
+
type: 'info',
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
if (session.status === 'completed') {
|
|
35
|
+
const endTime = new Date(new Date(session.startedAt).getTime() + session.duration * 1000)
|
|
36
|
+
events.push({
|
|
37
|
+
id: 'completed',
|
|
38
|
+
label: 'Session completed',
|
|
39
|
+
timestamp: endTime.toISOString(),
|
|
40
|
+
type: 'success',
|
|
41
|
+
})
|
|
42
|
+
} else if (session.status === 'failed') {
|
|
43
|
+
const endTime = new Date(new Date(session.startedAt).getTime() + session.duration * 1000)
|
|
44
|
+
events.push({
|
|
45
|
+
id: 'failed',
|
|
46
|
+
label: 'Session failed',
|
|
47
|
+
timestamp: endTime.toISOString(),
|
|
48
|
+
type: 'error',
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={cn('space-y-6 p-6 animate-fade-in', className)}>
|
|
54
|
+
{/* Back button */}
|
|
55
|
+
{onBack && (
|
|
56
|
+
<Button
|
|
57
|
+
variant="ghost"
|
|
58
|
+
size="sm"
|
|
59
|
+
onClick={onBack}
|
|
60
|
+
className="gap-1.5 text-af-text-secondary hover:text-af-text-primary font-body -ml-2"
|
|
61
|
+
>
|
|
62
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
63
|
+
Back
|
|
64
|
+
</Button>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{/* Header */}
|
|
68
|
+
<div className="flex items-center gap-3">
|
|
69
|
+
<StatusDot status={session.status} showHeartbeat={session.status === 'working'} className="h-3 w-3" />
|
|
70
|
+
<h1 className="font-display text-xl font-bold font-mono text-af-text-primary tracking-tight">
|
|
71
|
+
{session.identifier}
|
|
72
|
+
</h1>
|
|
73
|
+
<Badge
|
|
74
|
+
variant="outline"
|
|
75
|
+
className={cn(
|
|
76
|
+
'text-xs border',
|
|
77
|
+
statusConfig.bgColor, statusConfig.textColor, statusConfig.borderColor
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{statusConfig.label}
|
|
81
|
+
</Badge>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
|
85
|
+
{/* Left: Details */}
|
|
86
|
+
<div className="lg:col-span-2 rounded-xl border border-af-surface-border/40 bg-af-surface/30 p-6">
|
|
87
|
+
<h3 className="font-display text-sm font-semibold text-af-text-primary tracking-tight mb-5">
|
|
88
|
+
Session Details
|
|
89
|
+
</h3>
|
|
90
|
+
|
|
91
|
+
<div className="grid grid-cols-2 gap-6">
|
|
92
|
+
<div className="space-y-1">
|
|
93
|
+
<dt className="flex items-center gap-1.5 text-2xs font-body uppercase tracking-wider text-af-text-tertiary">
|
|
94
|
+
<Tag className="h-3 w-3" />
|
|
95
|
+
Work Type
|
|
96
|
+
</dt>
|
|
97
|
+
<dd>
|
|
98
|
+
<Badge
|
|
99
|
+
variant="outline"
|
|
100
|
+
className={cn('text-xs border', workTypeConfig.bgColor, workTypeConfig.color, workTypeConfig.borderColor)}
|
|
101
|
+
>
|
|
102
|
+
{workTypeConfig.label}
|
|
103
|
+
</Badge>
|
|
104
|
+
</dd>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="space-y-1">
|
|
108
|
+
<dt className="flex items-center gap-1.5 text-2xs font-body uppercase tracking-wider text-af-text-tertiary">
|
|
109
|
+
<Clock className="h-3 w-3" />
|
|
110
|
+
Duration
|
|
111
|
+
</dt>
|
|
112
|
+
<dd className="font-display text-lg font-bold tabular-nums text-af-text-primary">
|
|
113
|
+
{formatDuration(session.duration)}
|
|
114
|
+
</dd>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="space-y-1">
|
|
118
|
+
<dt className="flex items-center gap-1.5 text-2xs font-body uppercase tracking-wider text-af-text-tertiary">
|
|
119
|
+
<Coins className="h-3 w-3" />
|
|
120
|
+
Cost
|
|
121
|
+
</dt>
|
|
122
|
+
<dd className="font-display text-lg font-bold tabular-nums font-mono text-af-accent">
|
|
123
|
+
{formatCost(session.costUsd)}
|
|
124
|
+
</dd>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="space-y-1">
|
|
128
|
+
<dt className="flex items-center gap-1.5 text-2xs font-body uppercase tracking-wider text-af-text-tertiary">
|
|
129
|
+
<Calendar className="h-3 w-3" />
|
|
130
|
+
Started
|
|
131
|
+
</dt>
|
|
132
|
+
<dd className="text-sm font-body text-af-text-primary">
|
|
133
|
+
{new Date(session.startedAt).toLocaleString()}
|
|
134
|
+
</dd>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div className="mt-8">
|
|
139
|
+
<TokenChart />
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Right: Timeline */}
|
|
144
|
+
<div className="rounded-xl border border-af-surface-border/40 bg-af-surface/30 p-6">
|
|
145
|
+
<h3 className="font-display text-sm font-semibold text-af-text-primary tracking-tight mb-5">
|
|
146
|
+
Timeline
|
|
147
|
+
</h3>
|
|
148
|
+
<SessionTimeline events={events} />
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
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-6', 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-6">
|
|
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-6', className)}>
|
|
45
|
+
<div className="mb-5 flex items-center gap-3">
|
|
46
|
+
<h2 className="font-display text-lg font-bold text-af-text-primary tracking-tight">
|
|
47
|
+
Sessions
|
|
48
|
+
</h2>
|
|
49
|
+
<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">
|
|
50
|
+
{sessions.length}
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="rounded-xl border border-af-surface-border/40 overflow-hidden">
|
|
55
|
+
<table className="w-full text-sm font-body">
|
|
56
|
+
<thead>
|
|
57
|
+
<tr className="border-b border-af-surface-border/40 bg-af-bg-secondary/50">
|
|
58
|
+
<th className="px-4 py-3 text-left text-2xs font-semibold uppercase tracking-wider text-af-text-tertiary">Issue</th>
|
|
59
|
+
<th className="px-4 py-3 text-left text-2xs font-semibold uppercase tracking-wider text-af-text-tertiary">Status</th>
|
|
60
|
+
<th className="px-4 py-3 text-left text-2xs font-semibold uppercase tracking-wider text-af-text-tertiary">Type</th>
|
|
61
|
+
<th className="px-4 py-3 text-left text-2xs font-semibold uppercase tracking-wider text-af-text-tertiary">Duration</th>
|
|
62
|
+
<th className="px-4 py-3 text-left text-2xs font-semibold uppercase tracking-wider text-af-text-tertiary">Cost</th>
|
|
63
|
+
<th className="px-4 py-3 text-left text-2xs font-semibold uppercase tracking-wider text-af-text-tertiary">Started</th>
|
|
64
|
+
</tr>
|
|
65
|
+
</thead>
|
|
66
|
+
<tbody>
|
|
67
|
+
{sessions.map((session, i) => {
|
|
68
|
+
const statusConfig = getStatusConfig(session.status)
|
|
69
|
+
const workTypeConfig = getWorkTypeConfig(session.workType)
|
|
70
|
+
return (
|
|
71
|
+
<tr
|
|
72
|
+
key={session.id}
|
|
73
|
+
className={cn(
|
|
74
|
+
'border-b border-af-surface-border/20 last:border-0 cursor-pointer transition-all duration-200',
|
|
75
|
+
'hover:bg-af-surface/30'
|
|
76
|
+
)}
|
|
77
|
+
onClick={() => onSelect?.(session.id)}
|
|
78
|
+
style={{ animationDelay: `${i * 30}ms` }}
|
|
79
|
+
>
|
|
80
|
+
<td className="px-4 py-3">
|
|
81
|
+
<span className="font-mono text-sm text-af-text-primary">{session.identifier}</span>
|
|
82
|
+
</td>
|
|
83
|
+
<td className="px-4 py-3">
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<StatusDot status={session.status} />
|
|
86
|
+
<span className={cn('text-xs', statusConfig.textColor)}>
|
|
87
|
+
{statusConfig.label}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
</td>
|
|
91
|
+
<td className="px-4 py-3">
|
|
92
|
+
<Badge
|
|
93
|
+
variant="outline"
|
|
94
|
+
className={cn(
|
|
95
|
+
'text-2xs border',
|
|
96
|
+
workTypeConfig.bgColor, workTypeConfig.color, workTypeConfig.borderColor
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
{workTypeConfig.label}
|
|
100
|
+
</Badge>
|
|
101
|
+
</td>
|
|
102
|
+
<td className="px-4 py-3">
|
|
103
|
+
<span className="text-xs text-af-text-secondary tabular-nums">
|
|
104
|
+
{formatDuration(session.duration)}
|
|
105
|
+
</span>
|
|
106
|
+
</td>
|
|
107
|
+
<td className="px-4 py-3">
|
|
108
|
+
<span className="text-xs text-af-text-secondary tabular-nums font-mono">
|
|
109
|
+
{formatCost(session.costUsd)}
|
|
110
|
+
</span>
|
|
111
|
+
</td>
|
|
112
|
+
<td className="px-4 py-3">
|
|
113
|
+
<span className="text-xs text-af-text-tertiary">
|
|
114
|
+
{formatRelativeTime(session.startedAt)}
|
|
115
|
+
</span>
|
|
116
|
+
</td>
|
|
117
|
+
</tr>
|
|
118
|
+
)
|
|
119
|
+
})}
|
|
120
|
+
</tbody>
|
|
121
|
+
</table>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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 typeStyles = {
|
|
17
|
+
info: {
|
|
18
|
+
dot: 'bg-af-blue',
|
|
19
|
+
glow: 'shadow-[0_0_8px_2px_rgba(75,139,245,0.3)]',
|
|
20
|
+
line: 'bg-af-blue/20',
|
|
21
|
+
},
|
|
22
|
+
success: {
|
|
23
|
+
dot: 'bg-af-status-success',
|
|
24
|
+
glow: 'shadow-[0_0_8px_2px_rgba(34,197,94,0.3)]',
|
|
25
|
+
line: 'bg-af-status-success/20',
|
|
26
|
+
},
|
|
27
|
+
warning: {
|
|
28
|
+
dot: 'bg-af-status-warning',
|
|
29
|
+
glow: 'shadow-[0_0_8px_2px_rgba(245,158,11,0.3)]',
|
|
30
|
+
line: 'bg-af-status-warning/20',
|
|
31
|
+
},
|
|
32
|
+
error: {
|
|
33
|
+
dot: 'bg-af-status-error',
|
|
34
|
+
glow: 'shadow-[0_0_8px_2px_rgba(239,68,68,0.3)]',
|
|
35
|
+
line: 'bg-af-status-error/20',
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function SessionTimeline({ events, className }: SessionTimelineProps) {
|
|
40
|
+
return (
|
|
41
|
+
<div className={cn('relative space-y-0', className)}>
|
|
42
|
+
{/* Vertical connector line */}
|
|
43
|
+
{events.length > 1 && (
|
|
44
|
+
<div className="absolute left-[5px] top-3 bottom-3 w-px bg-af-surface-border/40" />
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{events.map((event) => {
|
|
48
|
+
const styles = typeStyles[event.type ?? 'info']
|
|
49
|
+
return (
|
|
50
|
+
<div key={event.id} className="relative flex gap-3.5 py-2.5">
|
|
51
|
+
{/* Glowing dot */}
|
|
52
|
+
<span
|
|
53
|
+
className={cn(
|
|
54
|
+
'relative z-10 mt-1.5 h-[10px] w-[10px] shrink-0 rounded-full',
|
|
55
|
+
styles.dot,
|
|
56
|
+
styles.glow
|
|
57
|
+
)}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<div className="flex-1 min-w-0">
|
|
61
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
62
|
+
<span className="text-sm font-body text-af-text-primary">{event.label}</span>
|
|
63
|
+
<span className="text-2xs font-body text-af-text-tertiary whitespace-nowrap tabular-nums">
|
|
64
|
+
{new Date(event.timestamp).toLocaleTimeString()}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
{event.detail && (
|
|
68
|
+
<p className="mt-0.5 text-xs font-body text-af-text-tertiary">{event.detail}</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -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-3', className)}>
|
|
19
|
+
<div className="flex items-center justify-between text-2xs font-body">
|
|
20
|
+
<span className="uppercase tracking-wider text-af-text-tertiary">Token Usage</span>
|
|
21
|
+
<span className="tabular-nums font-mono text-af-text-secondary">{formatTokens(total)} total</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{/* Bar */}
|
|
25
|
+
<div className="flex h-2.5 w-full overflow-hidden rounded-full bg-af-bg-primary/60">
|
|
26
|
+
<div
|
|
27
|
+
className="bg-af-blue rounded-l-full transition-all duration-500"
|
|
28
|
+
style={{ width: `${inputPct}%` }}
|
|
29
|
+
/>
|
|
30
|
+
<div
|
|
31
|
+
className="bg-af-accent transition-all duration-500"
|
|
32
|
+
style={{ width: `${outputPct}%` }}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
{/* Legend */}
|
|
37
|
+
<div className="flex items-center gap-5 text-2xs font-body">
|
|
38
|
+
<div className="flex items-center gap-1.5">
|
|
39
|
+
<span className="h-2 w-2 rounded-full bg-af-blue shadow-[0_0_6px_1px_rgba(75,139,245,0.3)]" />
|
|
40
|
+
<span className="text-af-text-tertiary">Input</span>
|
|
41
|
+
<span className="tabular-nums font-mono text-af-text-secondary">{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 shadow-[0_0_6px_1px_rgba(255,107,53,0.3)]" />
|
|
45
|
+
<span className="text-af-text-tertiary">Output</span>
|
|
46
|
+
<span className="tabular-nums font-mono text-af-text-secondary">{formatTokens(outputTokens)}</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { Badge } from '../../components/ui/badge'
|
|
5
|
+
import { Separator } from '../../components/ui/separator'
|
|
6
|
+
import { useStats } from '../../hooks/use-stats'
|
|
7
|
+
import { useWorkers } from '../../hooks/use-workers'
|
|
8
|
+
import { ProviderIcon } from '../../components/fleet/provider-icon'
|
|
9
|
+
import { StatusDot } from '../../components/fleet/status-dot'
|
|
10
|
+
import { CheckCircle2, AlertCircle, Settings2, Webhook, Server, Shield } from 'lucide-react'
|
|
11
|
+
|
|
12
|
+
interface SettingsViewProps {
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SettingsView({ className }: SettingsViewProps) {
|
|
17
|
+
const { data: stats } = useStats()
|
|
18
|
+
const { data: workersData } = useWorkers()
|
|
19
|
+
|
|
20
|
+
const workers = workersData?.workers ?? []
|
|
21
|
+
const hasWorkerAuth = workersData !== null
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn('space-y-6 p-6 max-w-3xl', className)}>
|
|
25
|
+
{/* Page header */}
|
|
26
|
+
<div>
|
|
27
|
+
<h1 className="font-display text-xl font-bold text-af-text-primary tracking-tight">Settings</h1>
|
|
28
|
+
<p className="mt-1 text-sm font-body text-af-text-secondary">
|
|
29
|
+
Configuration and integration status for your AgentFactory instance.
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Integration Status */}
|
|
34
|
+
<div className="rounded-xl border border-af-surface-border/40 bg-af-surface/30 overflow-hidden">
|
|
35
|
+
<div className="px-6 py-4 border-b border-af-surface-border/30">
|
|
36
|
+
<h3 className="flex items-center gap-2 font-display text-sm font-semibold text-af-text-primary tracking-tight">
|
|
37
|
+
<Webhook className="h-4 w-4 text-af-text-tertiary" />
|
|
38
|
+
Integration Status
|
|
39
|
+
</h3>
|
|
40
|
+
<p className="mt-0.5 text-xs font-body text-af-text-tertiary">Connected services and API endpoints</p>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="px-6 py-4 space-y-4">
|
|
44
|
+
<div className="flex items-center justify-between">
|
|
45
|
+
<div className="flex items-center gap-3">
|
|
46
|
+
<CheckCircle2 className="h-4 w-4 text-af-status-success" />
|
|
47
|
+
<div>
|
|
48
|
+
<p className="text-sm font-body text-af-text-primary">Linear Webhook</p>
|
|
49
|
+
<p className="text-2xs font-mono text-af-text-tertiary">/webhook</p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<Badge variant="success">Connected</Badge>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<Separator className="bg-af-surface-border/30" />
|
|
56
|
+
|
|
57
|
+
<div className="flex items-center justify-between">
|
|
58
|
+
<div className="flex items-center gap-3">
|
|
59
|
+
<CheckCircle2 className="h-4 w-4 text-af-status-success" />
|
|
60
|
+
<div>
|
|
61
|
+
<p className="text-sm font-body text-af-text-primary">Public API</p>
|
|
62
|
+
<p className="text-2xs font-mono text-af-text-tertiary">/api/public/stats</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<Badge variant="success">Active</Badge>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<Separator className="bg-af-surface-border/30" />
|
|
69
|
+
|
|
70
|
+
<div className="flex items-center justify-between">
|
|
71
|
+
<div className="flex items-center gap-3">
|
|
72
|
+
{hasWorkerAuth ? (
|
|
73
|
+
<CheckCircle2 className="h-4 w-4 text-af-status-success" />
|
|
74
|
+
) : (
|
|
75
|
+
<AlertCircle className="h-4 w-4 text-af-text-tertiary" />
|
|
76
|
+
)}
|
|
77
|
+
<div>
|
|
78
|
+
<p className="text-sm font-body text-af-text-primary">Worker API</p>
|
|
79
|
+
<p className="text-2xs font-mono text-af-text-tertiary">/api/workers</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<Badge variant={hasWorkerAuth ? 'success' : 'secondary'}>
|
|
83
|
+
{hasWorkerAuth ? 'Authenticated' : 'No Auth Key'}
|
|
84
|
+
</Badge>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Workers */}
|
|
90
|
+
<div className="rounded-xl border border-af-surface-border/40 bg-af-surface/30 overflow-hidden">
|
|
91
|
+
<div className="px-6 py-4 border-b border-af-surface-border/30">
|
|
92
|
+
<h3 className="flex items-center gap-2 font-display text-sm font-semibold text-af-text-primary tracking-tight">
|
|
93
|
+
<Server className="h-4 w-4 text-af-text-tertiary" />
|
|
94
|
+
Workers
|
|
95
|
+
</h3>
|
|
96
|
+
<p className="mt-0.5 text-xs font-body text-af-text-tertiary">
|
|
97
|
+
{workers.length > 0
|
|
98
|
+
? `${workers.length} worker${workers.length !== 1 ? 's' : ''} registered`
|
|
99
|
+
: !hasWorkerAuth && (stats?.workersOnline ?? 0) > 0
|
|
100
|
+
? `${stats!.workersOnline} worker${stats!.workersOnline !== 1 ? 's' : ''} online`
|
|
101
|
+
: 'No workers connected'}
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className="px-6 py-4">
|
|
106
|
+
{!hasWorkerAuth && (stats?.workersOnline ?? 0) > 0 ? (
|
|
107
|
+
<div className="space-y-2">
|
|
108
|
+
<p className="text-sm font-body text-af-text-secondary">
|
|
109
|
+
{stats!.workersOnline} worker{stats!.workersOnline !== 1 ? 's' : ''} connected to the fleet.
|
|
110
|
+
</p>
|
|
111
|
+
<p className="text-xs font-body text-af-text-tertiary">
|
|
112
|
+
Set <code className="font-mono text-2xs px-1 py-0.5 rounded bg-af-surface-border/30">WORKER_API_KEY</code> to view detailed worker information.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
) : workers.length === 0 ? (
|
|
116
|
+
<p className="text-sm font-body text-af-text-tertiary">
|
|
117
|
+
Workers will appear here once they register with the server.
|
|
118
|
+
</p>
|
|
119
|
+
) : (
|
|
120
|
+
<div className="space-y-3">
|
|
121
|
+
{workers.map((worker) => (
|
|
122
|
+
<div key={worker.id} className="flex items-center justify-between py-1">
|
|
123
|
+
<div className="flex items-center gap-3">
|
|
124
|
+
<StatusDot status={worker.status === 'active' ? 'working' : 'stopped'} />
|
|
125
|
+
<div>
|
|
126
|
+
<p className="text-sm font-mono text-af-text-primary">
|
|
127
|
+
{worker.hostname ?? worker.id.slice(0, 8)}
|
|
128
|
+
</p>
|
|
129
|
+
<p className="text-2xs font-body text-af-text-tertiary">
|
|
130
|
+
{worker.activeSessions}/{worker.capacity} slots
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="flex items-center gap-2.5">
|
|
135
|
+
<ProviderIcon provider={worker.provider} size={14} />
|
|
136
|
+
<Badge variant={worker.status === 'active' ? 'success' : 'secondary'}>
|
|
137
|
+
{worker.status}
|
|
138
|
+
</Badge>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Fleet Stats */}
|
|
148
|
+
<div className="rounded-xl border border-af-surface-border/40 bg-af-surface/30 overflow-hidden">
|
|
149
|
+
<div className="px-6 py-4 border-b border-af-surface-border/30">
|
|
150
|
+
<h3 className="flex items-center gap-2 font-display text-sm font-semibold text-af-text-primary tracking-tight">
|
|
151
|
+
<Shield className="h-4 w-4 text-af-text-tertiary" />
|
|
152
|
+
Fleet Configuration
|
|
153
|
+
</h3>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="px-6 py-4">
|
|
157
|
+
<div className="grid grid-cols-2 gap-6">
|
|
158
|
+
<div className="space-y-1">
|
|
159
|
+
<dt className="text-2xs font-body uppercase tracking-wider text-af-text-tertiary">Total Capacity</dt>
|
|
160
|
+
<dd className="font-display text-lg font-bold tabular-nums text-af-text-primary">
|
|
161
|
+
{stats?.availableCapacity ?? '—'}
|
|
162
|
+
</dd>
|
|
163
|
+
</div>
|
|
164
|
+
<div className="space-y-1">
|
|
165
|
+
<dt className="text-2xs font-body uppercase tracking-wider text-af-text-tertiary">Workers Online</dt>
|
|
166
|
+
<dd className="font-display text-lg font-bold tabular-nums text-af-text-primary">
|
|
167
|
+
{stats?.workersOnline ?? '—'}
|
|
168
|
+
</dd>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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(
|
|
21
|
+
'flex flex-col items-center justify-center py-16 text-center',
|
|
22
|
+
className
|
|
23
|
+
)}>
|
|
24
|
+
<div className="mb-4 rounded-xl border border-af-surface-border/30 bg-af-surface/20 p-4">
|
|
25
|
+
<div className="text-af-text-tertiary">
|
|
26
|
+
{icon ?? <Inbox className="h-8 w-8" />}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<h3 className="font-display text-sm font-semibold text-af-text-primary tracking-tight">{title}</h3>
|
|
30
|
+
<p className="mt-1.5 max-w-xs text-xs font-body text-af-text-tertiary">{description}</p>
|
|
31
|
+
{children && <div className="mt-5">{children}</div>}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
|
|
3
|
+
interface LogoProps {
|
|
4
|
+
className?: string
|
|
5
|
+
size?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Logo({ className, size = 32 }: LogoProps) {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
width={size}
|
|
12
|
+
height={size}
|
|
13
|
+
viewBox="0 0 512 512"
|
|
14
|
+
fill="none"
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
className={cn('text-af-accent', className)}
|
|
17
|
+
>
|
|
18
|
+
<g transform="matrix(1.28321,0,0,1.28321,-56.4612,-56.0276)">
|
|
19
|
+
<g transform="matrix(1,0,0,1,-278,-269)">
|
|
20
|
+
<path
|
|
21
|
+
d="M458,365L448,370L440,378L436,388L436,401L441,412L448,419L459,425L459,569L505,616L504.5,650.092L494.5,655.092L486.5,664.092L482.5,674.092L482.5,686.092L488.5,699.092L496.5,706.092L502.5,709.092L516.5,710.092L530.5,704.092L539.5,693.092L541.5,687.092L541.5,673.092L537.5,664.092L529.5,655.092L519.5,650.092L519,614L564,569L564,507L573,499L577,489L576,481L572,474L562,468L549,469L539,479L539,495L542,500L550,506L550,563L524,589L520,589L519,476L555,447L560,447L648,518L655,521L668,330L693.779,330L695,335L707,467L707,696L704,698L566,698L564,696L565,632L610,587L610,546L620,537L623,524L620,516L613,509L599,507L592,510L584,521L585,535L596,546L596,581L550,627L550,711.779L721,712L721,467L709,334L706,312.662L655,312.662L644,491L637,491L564,431L564,429L575,423L585,411L587,405L587,392L584,384L574,373L565,369L547,370L533,381L528,392L528,405L530,411L541,424L550,428L550,433L524,454L519,453L519,374L528,370L537,361L541,351L540,335L535,326L529,320L517,315L502,316L494,320L488,326L482,339L482,351L487,362L494,369L503,373L505,373.559L504,588L499,589L473,563L473,424L482,420L492,409L495,401L495,388L490,377L482,369L473,365L458,365ZM528.287,679.998C528.287,671.063 520.935,663.711 512,663.711C503.065,663.711 495.713,671.063 495.713,679.998C495.713,688.933 503.065,696.285 512,696.285C520.935,696.285 528.287,688.933 528.287,679.998ZM527.607,344.629C527.607,344.587 527.608,344.545 527.608,344.502C527.608,335.705 520.368,328.465 511.57,328.465C502.773,328.465 495.534,335.705 495.534,344.502C495.534,344.545 495.534,344.587 495.534,344.629C495.534,344.671 495.534,344.713 495.534,344.756C495.534,353.553 502.773,360.793 511.57,360.793C520.368,360.793 527.608,353.553 527.608,344.756C527.608,344.713 527.607,344.671 527.607,344.629ZM573.561,398.285C573.561,389.447 566.288,382.174 557.45,382.174C548.612,382.174 541.339,389.447 541.339,398.285L541.339,398.285C541.339,407.123 548.612,414.396 557.45,414.396C566.288,414.396 573.561,407.123 573.561,398.285L573.561,398.285ZM481.681,394.375C481.681,385.567 474.433,378.319 465.625,378.319C456.817,378.319 449.569,385.567 449.569,394.375L449.569,394.375C449.569,403.183 456.817,410.431 465.625,410.431C474.433,410.431 481.681,403.183 481.681,394.375L481.681,394.375Z"
|
|
22
|
+
fill="currentColor"
|
|
23
|
+
fillRule="evenodd"
|
|
24
|
+
clipRule="evenodd"
|
|
25
|
+
/>
|
|
26
|
+
</g>
|
|
27
|
+
<g transform="matrix(1,0,0,1,-278,-269)">
|
|
28
|
+
<path
|
|
29
|
+
d="M427,365L424,365L322,469L322,712L473,711.779L473,628L427,581L427,520L438,509L439,495L431,484L424,481L416,481L409,484L403,490L400,499L403,511L413,520L413,587L459,634L459,696L457,698L339,698L337,696L337,476L408,401.455L413,397L413,460L426,460L427,365Z"
|
|
30
|
+
fill="currentColor"
|
|
31
|
+
fillRule="nonzero"
|
|
32
|
+
/>
|
|
33
|
+
</g>
|
|
34
|
+
</g>
|
|
35
|
+
</svg>
|
|
36
|
+
)
|
|
37
|
+
}
|