@renseiai/agentfactory-dashboard 0.8.7 → 0.8.9
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/package.json +1 -1
- package/src/components/fleet/routing-metrics.tsx +233 -0
- package/src/hooks/use-routing-metrics.ts +13 -0
- package/src/index.ts +6 -0
- package/src/types/api.ts +35 -0
package/package.json
CHANGED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { useRoutingMetrics } from '../../hooks/use-routing-metrics'
|
|
5
|
+
import { EmptyState } from '../../components/shared/empty-state'
|
|
6
|
+
import { Skeleton } from '../../components/ui/skeleton'
|
|
7
|
+
import { formatCost } from '../../lib/format'
|
|
8
|
+
import { Router, Activity, TrendingUp, CheckCircle2, XCircle, HelpCircle } from 'lucide-react'
|
|
9
|
+
|
|
10
|
+
interface RoutingMetricsProps {
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function confidenceColor(confidence: number): string {
|
|
15
|
+
if (confidence > 0.7) return 'text-emerald-400'
|
|
16
|
+
if (confidence >= 0.3) return 'text-amber-400'
|
|
17
|
+
return 'text-red-400'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function confidenceBg(confidence: number): string {
|
|
21
|
+
if (confidence > 0.7) return 'bg-emerald-500/10 border-emerald-500/20'
|
|
22
|
+
if (confidence >= 0.3) return 'bg-amber-500/10 border-amber-500/20'
|
|
23
|
+
return 'bg-red-500/10 border-red-500/20'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function RoutingMetrics({ className }: RoutingMetricsProps) {
|
|
27
|
+
const { data, isLoading } = useRoutingMetrics()
|
|
28
|
+
|
|
29
|
+
const routingDisabled = !isLoading && (!data || !data.summary.routingEnabled)
|
|
30
|
+
|
|
31
|
+
if (routingDisabled) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={cn('p-6', className)}>
|
|
34
|
+
<EmptyState
|
|
35
|
+
title="Routing not enabled"
|
|
36
|
+
description="Multi-armed bandit routing will appear here once providers begin receiving observations."
|
|
37
|
+
icon={<Router className="h-8 w-8" />}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn('space-y-8 p-6', className)}>
|
|
45
|
+
{/* Section: Summary */}
|
|
46
|
+
<div>
|
|
47
|
+
<div className="mb-4 flex items-center gap-3">
|
|
48
|
+
<h2 className="font-display text-lg font-bold text-af-text-primary tracking-tight">
|
|
49
|
+
Routing Analytics
|
|
50
|
+
</h2>
|
|
51
|
+
{!isLoading && data && (
|
|
52
|
+
<span className="text-2xs font-body text-af-text-tertiary tabular-nums">
|
|
53
|
+
{data.summary.totalObservations} observation{data.summary.totalObservations !== 1 ? 's' : ''} total
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{isLoading ? (
|
|
59
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
60
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
61
|
+
<Skeleton key={i} className="h-[72px] rounded-xl" />
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
) : data ? (
|
|
65
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
66
|
+
<div className="rounded-xl border border-af-surface-border/50 bg-af-surface/40 p-4">
|
|
67
|
+
<p className="text-2xs font-body text-af-text-tertiary">Total Observations</p>
|
|
68
|
+
<p className="mt-1 font-display text-xl font-bold text-af-text-primary tabular-nums">
|
|
69
|
+
{data.summary.totalObservations}
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="rounded-xl border border-af-surface-border/50 bg-af-surface/40 p-4">
|
|
73
|
+
<p className="text-2xs font-body text-af-text-tertiary">Avg Confidence</p>
|
|
74
|
+
<p className={cn('mt-1 font-display text-xl font-bold tabular-nums', confidenceColor(data.summary.avgConfidence))}>
|
|
75
|
+
{(data.summary.avgConfidence * 100).toFixed(1)}%
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="rounded-xl border border-af-surface-border/50 bg-af-surface/40 p-4">
|
|
79
|
+
<p className="text-2xs font-body text-af-text-tertiary">Exploration Rate</p>
|
|
80
|
+
<p className="mt-1 font-display text-xl font-bold text-af-text-primary tabular-nums">
|
|
81
|
+
{(data.summary.explorationRate * 100).toFixed(0)}%
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="rounded-xl border border-af-surface-border/50 bg-af-surface/40 p-4">
|
|
85
|
+
<p className="text-2xs font-body text-af-text-tertiary">Providers Tracked</p>
|
|
86
|
+
<p className="mt-1 font-display text-xl font-bold text-af-text-primary tabular-nums">
|
|
87
|
+
{data.posteriors.length}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
) : null}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Section: Provider Performance Matrix */}
|
|
95
|
+
<div>
|
|
96
|
+
<div className="mb-4 flex items-center gap-3">
|
|
97
|
+
<TrendingUp className="h-4 w-4 text-af-text-tertiary" />
|
|
98
|
+
<h2 className="font-display text-lg font-bold text-af-text-primary tracking-tight">
|
|
99
|
+
Provider Performance
|
|
100
|
+
</h2>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{isLoading ? (
|
|
104
|
+
<Skeleton className="h-[200px] rounded-xl" />
|
|
105
|
+
) : data && data.posteriors.length > 0 ? (
|
|
106
|
+
<div className="overflow-x-auto rounded-xl border border-af-surface-border/50 bg-af-surface/40">
|
|
107
|
+
<table className="w-full text-left">
|
|
108
|
+
<thead>
|
|
109
|
+
<tr className="border-b border-af-surface-border/50">
|
|
110
|
+
<th className="px-4 py-3 text-2xs font-body font-medium text-af-text-tertiary">Provider</th>
|
|
111
|
+
<th className="px-4 py-3 text-2xs font-body font-medium text-af-text-tertiary">Work Type</th>
|
|
112
|
+
<th className="px-4 py-3 text-2xs font-body font-medium text-af-text-tertiary text-right">Expected Reward</th>
|
|
113
|
+
<th className="px-4 py-3 text-2xs font-body font-medium text-af-text-tertiary text-right">Confidence</th>
|
|
114
|
+
<th className="px-4 py-3 text-2xs font-body font-medium text-af-text-tertiary text-right">Observations</th>
|
|
115
|
+
<th className="px-4 py-3 text-2xs font-body font-medium text-af-text-tertiary text-right">Avg Cost</th>
|
|
116
|
+
<th className="px-4 py-3 text-2xs font-body font-medium text-af-text-tertiary text-right">Alpha / Beta</th>
|
|
117
|
+
</tr>
|
|
118
|
+
</thead>
|
|
119
|
+
<tbody>
|
|
120
|
+
{data.posteriors.map((p) => (
|
|
121
|
+
<tr
|
|
122
|
+
key={`${p.provider}-${p.workType}`}
|
|
123
|
+
className="border-b border-af-surface-border/30 last:border-b-0 hover:bg-af-surface/60 transition-colors"
|
|
124
|
+
>
|
|
125
|
+
<td className="px-4 py-3 text-xs font-body font-medium text-af-text-primary">{p.provider}</td>
|
|
126
|
+
<td className="px-4 py-3 text-xs font-body text-af-text-secondary">{p.workType}</td>
|
|
127
|
+
<td className="px-4 py-3 text-xs font-body text-af-text-primary text-right tabular-nums">
|
|
128
|
+
{(p.expectedReward * 100).toFixed(1)}%
|
|
129
|
+
</td>
|
|
130
|
+
<td className="px-4 py-3 text-right">
|
|
131
|
+
<span
|
|
132
|
+
className={cn(
|
|
133
|
+
'inline-block rounded-md border px-2 py-0.5 text-xs font-body tabular-nums',
|
|
134
|
+
confidenceBg(p.confidence),
|
|
135
|
+
confidenceColor(p.confidence),
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{(p.confidence * 100).toFixed(1)}%
|
|
139
|
+
</span>
|
|
140
|
+
</td>
|
|
141
|
+
<td className="px-4 py-3 text-xs font-body text-af-text-secondary text-right tabular-nums">
|
|
142
|
+
{p.totalObservations}
|
|
143
|
+
</td>
|
|
144
|
+
<td className="px-4 py-3 text-xs font-body text-af-text-secondary text-right tabular-nums">
|
|
145
|
+
{formatCost(p.avgCostUsd)}
|
|
146
|
+
</td>
|
|
147
|
+
<td className="px-4 py-3 text-xs font-body text-af-text-tertiary text-right tabular-nums">
|
|
148
|
+
{p.alpha.toFixed(1)} / {p.beta.toFixed(1)}
|
|
149
|
+
</td>
|
|
150
|
+
</tr>
|
|
151
|
+
))}
|
|
152
|
+
</tbody>
|
|
153
|
+
</table>
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
<EmptyState
|
|
157
|
+
title="No provider data"
|
|
158
|
+
description="Posterior distributions will appear once routing observations are recorded."
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Section: Recent Routing Decisions */}
|
|
164
|
+
<div>
|
|
165
|
+
<div className="mb-4 flex items-center gap-3">
|
|
166
|
+
<Activity className="h-4 w-4 text-af-text-tertiary" />
|
|
167
|
+
<h2 className="font-display text-lg font-bold text-af-text-primary tracking-tight">
|
|
168
|
+
Recent Decisions
|
|
169
|
+
</h2>
|
|
170
|
+
{!isLoading && data && (
|
|
171
|
+
<span className="text-2xs font-body text-af-text-tertiary tabular-nums">
|
|
172
|
+
last {data.recentDecisions.length}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{isLoading ? (
|
|
178
|
+
<div className="space-y-2">
|
|
179
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
180
|
+
<Skeleton key={i} className="h-[52px] rounded-xl" />
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
) : data && data.recentDecisions.length > 0 ? (
|
|
184
|
+
<div className="space-y-2">
|
|
185
|
+
{data.recentDecisions.map((d, i) => (
|
|
186
|
+
<div
|
|
187
|
+
key={`${d.timestamp}-${i}`}
|
|
188
|
+
className="flex items-center gap-4 rounded-xl border border-af-surface-border/50 bg-af-surface/40 px-4 py-3"
|
|
189
|
+
>
|
|
190
|
+
<div className="flex-shrink-0">
|
|
191
|
+
{d.taskCompleted ? (
|
|
192
|
+
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
|
193
|
+
) : d.reward > 0 ? (
|
|
194
|
+
<HelpCircle className="h-4 w-4 text-amber-400" />
|
|
195
|
+
) : (
|
|
196
|
+
<XCircle className="h-4 w-4 text-red-400" />
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
<div className="min-w-0 flex-1">
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<span className="text-xs font-body font-medium text-af-text-primary">{d.provider}</span>
|
|
202
|
+
<span className="text-2xs font-body text-af-text-tertiary">{d.workType}</span>
|
|
203
|
+
{d.explorationReason && (
|
|
204
|
+
<span className="rounded-full bg-af-surface/60 px-1.5 py-0.5 text-2xs font-body text-af-text-tertiary">
|
|
205
|
+
{d.explorationReason}
|
|
206
|
+
</span>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
<div className="flex items-center gap-4 flex-shrink-0">
|
|
211
|
+
<span className={cn('text-xs font-body tabular-nums', confidenceColor(d.confidence))}>
|
|
212
|
+
{(d.confidence * 100).toFixed(0)}%
|
|
213
|
+
</span>
|
|
214
|
+
<span className="text-xs font-body text-af-text-secondary tabular-nums">
|
|
215
|
+
reward {d.reward.toFixed(2)}
|
|
216
|
+
</span>
|
|
217
|
+
<span className="text-2xs font-body text-af-text-tertiary tabular-nums">
|
|
218
|
+
{new Date(d.timestamp).toLocaleTimeString()}
|
|
219
|
+
</span>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
))}
|
|
223
|
+
</div>
|
|
224
|
+
) : (
|
|
225
|
+
<EmptyState
|
|
226
|
+
title="No recent decisions"
|
|
227
|
+
description="Routing decisions will appear here as agents complete tasks."
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import useSWR from 'swr'
|
|
4
|
+
import type { PublicRoutingMetricsResponse } from '../types/api'
|
|
5
|
+
|
|
6
|
+
const fetcher = (url: string) => fetch(url).then((r) => r.json())
|
|
7
|
+
|
|
8
|
+
export function useRoutingMetrics(refreshInterval = 10000) {
|
|
9
|
+
return useSWR<PublicRoutingMetricsResponse>('/api/public/routing-metrics', fetcher, {
|
|
10
|
+
refreshInterval,
|
|
11
|
+
dedupingInterval: 5000,
|
|
12
|
+
})
|
|
13
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { SettingsPage } from './pages/settings-page'
|
|
|
12
12
|
|
|
13
13
|
// Fleet components
|
|
14
14
|
export { FleetOverview } from './components/fleet/fleet-overview'
|
|
15
|
+
export { RoutingMetrics } from './components/fleet/routing-metrics'
|
|
15
16
|
export { AgentCard } from './components/fleet/agent-card'
|
|
16
17
|
export { StatCard } from './components/fleet/stat-card'
|
|
17
18
|
export { StatusDot } from './components/fleet/status-dot'
|
|
@@ -59,6 +60,7 @@ export {
|
|
|
59
60
|
export { useStats } from './hooks/use-stats'
|
|
60
61
|
export { useSessions } from './hooks/use-sessions'
|
|
61
62
|
export { useWorkers } from './hooks/use-workers'
|
|
63
|
+
export { useRoutingMetrics } from './hooks/use-routing-metrics'
|
|
62
64
|
|
|
63
65
|
// Utilities
|
|
64
66
|
export { cn } from './lib/utils'
|
|
@@ -75,6 +77,10 @@ export type {
|
|
|
75
77
|
WorkerResponse,
|
|
76
78
|
WorkersListResponse,
|
|
77
79
|
PipelineStatus,
|
|
80
|
+
PublicRoutingMetricsResponse,
|
|
81
|
+
RoutingPosteriorResponse,
|
|
82
|
+
RoutingDecisionResponse,
|
|
83
|
+
RoutingSummaryResponse,
|
|
78
84
|
} from './types/api'
|
|
79
85
|
export type { WorkTypeConfig } from './lib/work-type-config'
|
|
80
86
|
export type { StatusConfig } from './lib/status-config'
|
package/src/types/api.ts
CHANGED
|
@@ -46,3 +46,38 @@ export interface WorkersListResponse {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export type PipelineStatus = 'backlog' | 'started' | 'finished' | 'delivered' | 'accepted'
|
|
49
|
+
|
|
50
|
+
export interface RoutingPosteriorResponse {
|
|
51
|
+
provider: string
|
|
52
|
+
workType: string
|
|
53
|
+
alpha: number
|
|
54
|
+
beta: number
|
|
55
|
+
expectedReward: number
|
|
56
|
+
confidence: number
|
|
57
|
+
totalObservations: number
|
|
58
|
+
avgCostUsd: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RoutingDecisionResponse {
|
|
62
|
+
timestamp: number
|
|
63
|
+
provider: string
|
|
64
|
+
workType: string
|
|
65
|
+
reward: number
|
|
66
|
+
taskCompleted: boolean
|
|
67
|
+
confidence: number
|
|
68
|
+
explorationReason?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface RoutingSummaryResponse {
|
|
72
|
+
totalObservations: number
|
|
73
|
+
routingEnabled: boolean
|
|
74
|
+
explorationRate: number
|
|
75
|
+
avgConfidence: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PublicRoutingMetricsResponse {
|
|
79
|
+
posteriors: RoutingPosteriorResponse[]
|
|
80
|
+
recentDecisions: RoutingDecisionResponse[]
|
|
81
|
+
summary: RoutingSummaryResponse
|
|
82
|
+
timestamp: string
|
|
83
|
+
}
|