@renseiai/agentfactory-dashboard 0.8.17 → 0.8.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renseiai/agentfactory-dashboard",
3
- "version": "0.8.17",
3
+ "version": "0.8.19",
4
4
  "description": "Premium dashboard UI components for AgentFactory",
5
5
  "author": "Rensei AI (https://rensei.ai)",
6
6
  "license": "MIT",
@@ -1,16 +1,40 @@
1
1
  'use client'
2
2
 
3
+ import { useState, useCallback } from 'react'
3
4
  import { cn } from '../../lib/utils'
4
5
  import { useRoutingMetrics } from '../../hooks/use-routing-metrics'
5
6
  import { EmptyState } from '../../components/shared/empty-state'
6
7
  import { Skeleton } from '../../components/ui/skeleton'
8
+ import { Button } from '../../components/ui/button'
7
9
  import { formatCost } from '../../lib/format'
8
- import { Router, Activity, TrendingUp, CheckCircle2, XCircle, HelpCircle } from 'lucide-react'
10
+ import { Router, Activity, TrendingUp, CheckCircle2, XCircle, HelpCircle, ChevronLeft, ChevronRight } from 'lucide-react'
9
11
 
10
12
  interface RoutingMetricsProps {
11
13
  className?: string
12
14
  }
13
15
 
16
+ const TIME_RANGES = [
17
+ { label: 'All', value: '' },
18
+ { label: '1h', value: '1h' },
19
+ { label: '6h', value: '6h' },
20
+ { label: '24h', value: '24h' },
21
+ { label: '7d', value: '7d' },
22
+ ] as const
23
+
24
+ type TimeRangeValue = (typeof TIME_RANGES)[number]['value']
25
+
26
+ function getTimeRangeFrom(value: TimeRangeValue): string | undefined {
27
+ if (!value) return undefined
28
+ const now = Date.now()
29
+ const ms: Record<string, number> = {
30
+ '1h': 60 * 60 * 1000,
31
+ '6h': 6 * 60 * 60 * 1000,
32
+ '24h': 24 * 60 * 60 * 1000,
33
+ '7d': 7 * 24 * 60 * 60 * 1000,
34
+ }
35
+ return new Date(now - ms[value]!).toISOString()
36
+ }
37
+
14
38
  function confidenceColor(confidence: number): string {
15
39
  if (confidence > 0.7) return 'text-emerald-400'
16
40
  if (confidence >= 0.3) return 'text-amber-400'
@@ -24,7 +48,34 @@ function confidenceBg(confidence: number): string {
24
48
  }
25
49
 
26
50
  export function RoutingMetrics({ className }: RoutingMetricsProps) {
27
- const { data, isLoading } = useRoutingMetrics()
51
+ const [timeRange, setTimeRange] = useState<TimeRangeValue>('')
52
+ const [cursor, setCursor] = useState<string | undefined>(undefined)
53
+ const [cursorStack, setCursorStack] = useState<string[]>([])
54
+
55
+ const from = getTimeRangeFrom(timeRange)
56
+ const { data, isLoading } = useRoutingMetrics({ from, cursor })
57
+
58
+ const handleTimeRangeChange = useCallback((value: TimeRangeValue) => {
59
+ setTimeRange(value)
60
+ setCursor(undefined)
61
+ setCursorStack([])
62
+ }, [])
63
+
64
+ const handleNextPage = useCallback(() => {
65
+ if (data?.nextCursor) {
66
+ setCursorStack((prev) => [...prev, cursor ?? ''])
67
+ setCursor(data.nextCursor)
68
+ }
69
+ }, [data?.nextCursor, cursor])
70
+
71
+ const handlePrevPage = useCallback(() => {
72
+ setCursorStack((prev) => {
73
+ const next = [...prev]
74
+ const prevCursor = next.pop()
75
+ setCursor(prevCursor || undefined)
76
+ return next
77
+ })
78
+ }, [])
28
79
 
29
80
  const routingDisabled = !isLoading && (!data || !data.summary.routingEnabled)
30
81
 
@@ -162,16 +213,58 @@ export function RoutingMetrics({ className }: RoutingMetricsProps) {
162
213
 
163
214
  {/* Section: Recent Routing Decisions */}
164
215
  <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
- )}
216
+ <div className="mb-4 flex items-center justify-between gap-3">
217
+ <div className="flex items-center gap-3">
218
+ <Activity className="h-4 w-4 text-af-text-tertiary" />
219
+ <h2 className="font-display text-lg font-bold text-af-text-primary tracking-tight">
220
+ Recent Decisions
221
+ </h2>
222
+ {!isLoading && data && (
223
+ <span className="text-2xs font-body text-af-text-tertiary tabular-nums">
224
+ showing {data.recentDecisions.length}{timeRange ? ` from last ${timeRange}` : ''}
225
+ </span>
226
+ )}
227
+ </div>
228
+ <div className="flex items-center gap-2">
229
+ {/* Time-range selector */}
230
+ <div className="flex items-center rounded-lg border border-af-surface-border/50 bg-af-surface/20 p-0.5">
231
+ {TIME_RANGES.map((range) => (
232
+ <button
233
+ key={range.value}
234
+ onClick={() => handleTimeRangeChange(range.value)}
235
+ className={cn(
236
+ 'rounded-md px-2.5 py-1 text-2xs font-body transition-colors',
237
+ timeRange === range.value
238
+ ? 'bg-af-surface/60 text-af-text-primary font-medium'
239
+ : 'text-af-text-tertiary hover:text-af-text-secondary',
240
+ )}
241
+ >
242
+ {range.label}
243
+ </button>
244
+ ))}
245
+ </div>
246
+ {/* Pagination controls */}
247
+ <div className="flex items-center gap-1">
248
+ <Button
249
+ variant="ghost"
250
+ size="icon"
251
+ className="h-7 w-7"
252
+ onClick={handlePrevPage}
253
+ disabled={cursorStack.length === 0}
254
+ >
255
+ <ChevronLeft className="h-4 w-4" />
256
+ </Button>
257
+ <Button
258
+ variant="ghost"
259
+ size="icon"
260
+ className="h-7 w-7"
261
+ onClick={handleNextPage}
262
+ disabled={!data?.nextCursor}
263
+ >
264
+ <ChevronRight className="h-4 w-4" />
265
+ </Button>
266
+ </div>
267
+ </div>
175
268
  </div>
176
269
 
177
270
  {isLoading ? (
@@ -0,0 +1,57 @@
1
+ import { cn } from '../../lib/utils'
2
+
3
+ interface DeprecationBannerProps {
4
+ message?: string
5
+ ctaHref?: string
6
+ onDismiss?: () => void
7
+ className?: string
8
+ }
9
+
10
+ export function DeprecationBanner({
11
+ message = 'This pipeline view is deprecated and will be removed in a future version.',
12
+ ctaHref,
13
+ onDismiss,
14
+ className,
15
+ }: DeprecationBannerProps) {
16
+ return (
17
+ <div
18
+ role="alert"
19
+ className={cn(
20
+ 'flex items-center justify-between gap-3 rounded-lg border border-yellow-500/30 bg-yellow-500/10 px-4 py-3 text-sm text-yellow-200',
21
+ className,
22
+ )}
23
+ >
24
+ <div className="flex items-center gap-2">
25
+ <svg
26
+ className="h-4 w-4 shrink-0 text-yellow-400"
27
+ viewBox="0 0 16 16"
28
+ fill="currentColor"
29
+ aria-hidden="true"
30
+ >
31
+ <path d="M8 1a7 7 0 100 14A7 7 0 008 1zm-.75 3.75a.75.75 0 011.5 0v3.5a.75.75 0 01-1.5 0v-3.5zM8 11a1 1 0 100 2 1 1 0 000-2z" />
32
+ </svg>
33
+ <span>{message}</span>
34
+ {ctaHref && (
35
+ <a
36
+ href={ctaHref}
37
+ className="font-medium text-yellow-300 underline underline-offset-2 hover:text-yellow-100"
38
+ >
39
+ Learn more
40
+ </a>
41
+ )}
42
+ </div>
43
+ {onDismiss && (
44
+ <button
45
+ type="button"
46
+ onClick={onDismiss}
47
+ className="shrink-0 rounded p-1 text-yellow-400 hover:bg-yellow-500/20 hover:text-yellow-200"
48
+ aria-label="Dismiss deprecation notice"
49
+ >
50
+ <svg className="h-4 w-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
51
+ <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z" />
52
+ </svg>
53
+ </button>
54
+ )}
55
+ </div>
56
+ )
57
+ }
@@ -10,19 +10,23 @@ interface PipelineCardProps {
10
10
  session: PublicSessionResponse
11
11
  className?: string
12
12
  onSelect?: (sessionId: string) => void
13
+ readOnly?: boolean
13
14
  }
14
15
 
15
- export function PipelineCard({ session, className, onSelect }: PipelineCardProps) {
16
+ export function PipelineCard({ session, className, onSelect, readOnly }: PipelineCardProps) {
16
17
  const workTypeConfig = getWorkTypeConfig(session.workType)
18
+ const isInteractive = onSelect && !readOnly
17
19
 
18
20
  return (
19
21
  <div
20
22
  className={cn(
21
- 'rounded-lg border border-af-surface-border/40 bg-af-surface/50 p-3 transition-all duration-200 hover-glow',
22
- onSelect && 'cursor-pointer',
23
+ 'rounded-lg border border-af-surface-border/40 bg-af-surface/50 p-3 transition-all duration-200',
24
+ !readOnly && 'hover-glow',
25
+ isInteractive && 'cursor-pointer',
26
+ readOnly && 'pointer-events-none',
23
27
  className
24
28
  )}
25
- {...(onSelect && {
29
+ {...(isInteractive && {
26
30
  role: 'button',
27
31
  tabIndex: 0,
28
32
  onClick: () => onSelect(session.id),
@@ -10,9 +10,10 @@ interface PipelineColumnProps {
10
10
  accentClass?: string
11
11
  className?: string
12
12
  onSessionSelect?: (sessionId: string) => void
13
+ readOnly?: boolean
13
14
  }
14
15
 
15
- export function PipelineColumn({ title, sessions, count, accentClass, className, onSessionSelect }: PipelineColumnProps) {
16
+ export function PipelineColumn({ title, sessions, count, accentClass, className, onSessionSelect, readOnly }: PipelineColumnProps) {
16
17
  return (
17
18
  <div
18
19
  className={cn(
@@ -35,7 +36,7 @@ export function PipelineColumn({ title, sessions, count, accentClass, className,
35
36
  <ScrollArea className="flex-1 px-2 pb-2">
36
37
  <div className="space-y-2">
37
38
  {sessions.map((session) => (
38
- <PipelineCard key={session.id} session={session} onSelect={onSessionSelect} />
39
+ <PipelineCard key={session.id} session={session} onSelect={onSessionSelect} readOnly={readOnly} />
39
40
  ))}
40
41
  </div>
41
42
  </ScrollArea>
@@ -31,9 +31,11 @@ function groupByColumn(sessions: PublicSessionResponse[]) {
31
31
  interface PipelineViewProps {
32
32
  className?: string
33
33
  onSessionSelect?: (sessionId: string) => void
34
+ /** When true, renders pipeline in a muted, non-interactive state */
35
+ readOnly?: boolean
34
36
  }
35
37
 
36
- export function PipelineView({ className, onSessionSelect }: PipelineViewProps) {
38
+ export function PipelineView({ className, onSessionSelect, readOnly }: PipelineViewProps) {
37
39
  const { data, isLoading } = useSessions()
38
40
  const sessions = data?.sessions ?? []
39
41
 
@@ -62,7 +64,7 @@ export function PipelineView({ className, onSessionSelect }: PipelineViewProps)
62
64
  description="Sessions will populate the pipeline as agents work on issues."
63
65
  />
64
66
  ) : (
65
- <div className="flex gap-3 overflow-x-auto pb-2">
67
+ <div className={cn('flex gap-3 overflow-x-auto pb-2', readOnly && 'opacity-60')}>
66
68
  {groupByColumn(sessions).map((col, i) => (
67
69
  <div
68
70
  key={col.title}
@@ -74,7 +76,8 @@ export function PipelineView({ className, onSessionSelect }: PipelineViewProps)
74
76
  sessions={col.sessions}
75
77
  count={col.sessions.length}
76
78
  accentClass={col.accentClass}
77
- onSessionSelect={onSessionSelect}
79
+ onSessionSelect={readOnly ? undefined : onSessionSelect}
80
+ readOnly={readOnly}
78
81
  />
79
82
  </div>
80
83
  ))}
@@ -5,8 +5,29 @@ import type { PublicRoutingMetricsResponse } from '../types/api'
5
5
 
6
6
  const fetcher = (url: string) => fetch(url).then((r) => r.json())
7
7
 
8
- export function useRoutingMetrics(refreshInterval = 10000) {
9
- return useSWR<PublicRoutingMetricsResponse>('/api/public/routing-metrics', fetcher, {
8
+ export interface UseRoutingMetricsOptions {
9
+ from?: string
10
+ to?: string
11
+ limit?: number
12
+ cursor?: string
13
+ refreshInterval?: number
14
+ }
15
+
16
+ function buildUrl(opts: UseRoutingMetricsOptions): string {
17
+ const params = new URLSearchParams()
18
+ if (opts.from) params.set('from', opts.from)
19
+ if (opts.to) params.set('to', opts.to)
20
+ if (opts.limit) params.set('limit', String(opts.limit))
21
+ if (opts.cursor) params.set('cursor', opts.cursor)
22
+ const qs = params.toString()
23
+ return qs ? `/api/public/routing-metrics?${qs}` : '/api/public/routing-metrics'
24
+ }
25
+
26
+ export function useRoutingMetrics(opts: UseRoutingMetricsOptions = {}) {
27
+ const { refreshInterval = 10000, ...queryOpts } = opts
28
+ const url = buildUrl(queryOpts)
29
+
30
+ return useSWR<PublicRoutingMetricsResponse>(url, fetcher, {
10
31
  refreshInterval,
11
32
  dedupingInterval: 5000,
12
33
  })
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export { BottomBar } from './components/layout/bottom-bar'
7
7
  // Pages
8
8
  export { DashboardPage } from './pages/dashboard-page'
9
9
  export { PipelinePage } from './pages/pipeline-page'
10
+ export type { PipelinePageProps } from './pages/pipeline-page'
10
11
  export { SessionPage } from './pages/session-page'
11
12
  export { SettingsPage } from './pages/settings-page'
12
13
 
@@ -22,6 +23,7 @@ export { ProviderIcon } from './components/fleet/provider-icon'
22
23
  export { PipelineView } from './components/pipeline/pipeline-view'
23
24
  export { PipelineColumn } from './components/pipeline/pipeline-column'
24
25
  export { PipelineCard } from './components/pipeline/pipeline-card'
26
+ export { DeprecationBanner } from './components/pipeline/deprecation-banner'
25
27
 
26
28
  // Session components
27
29
  export { SessionList } from './components/sessions/session-list'
@@ -1,11 +1,43 @@
1
1
  'use client'
2
2
 
3
+ import { useState } from 'react'
3
4
  import { PipelineView } from '../components/pipeline/pipeline-view'
5
+ import { DeprecationBanner } from '../components/pipeline/deprecation-banner'
4
6
 
5
- interface PipelinePageProps {
7
+ export interface PipelinePageProps {
6
8
  onSessionSelect?: (sessionId: string) => void
9
+ /** When true, disables all interactive elements (drag, edit, status changes) */
10
+ readOnly?: boolean
11
+ /** When true, shows a deprecation banner */
12
+ deprecated?: boolean
13
+ /** Custom deprecation message */
14
+ deprecationMessage?: string
15
+ /** CTA link in the deprecation banner */
16
+ deprecationCtaHref?: string
7
17
  }
8
18
 
9
- export function PipelinePage({ onSessionSelect }: PipelinePageProps) {
10
- return <PipelineView onSessionSelect={onSessionSelect} />
19
+ export function PipelinePage({
20
+ onSessionSelect,
21
+ readOnly,
22
+ deprecated,
23
+ deprecationMessage,
24
+ deprecationCtaHref,
25
+ }: PipelinePageProps) {
26
+ const [bannerDismissed, setBannerDismissed] = useState(false)
27
+
28
+ return (
29
+ <div>
30
+ {deprecated && !bannerDismissed && (
31
+ <DeprecationBanner
32
+ message={deprecationMessage}
33
+ ctaHref={deprecationCtaHref}
34
+ onDismiss={() => setBannerDismissed(true)}
35
+ />
36
+ )}
37
+ <PipelineView
38
+ onSessionSelect={readOnly ? undefined : onSessionSelect}
39
+ readOnly={readOnly}
40
+ />
41
+ </div>
42
+ )
11
43
  }
package/src/types/api.ts CHANGED
@@ -78,6 +78,26 @@ export interface RoutingSummaryResponse {
78
78
  export interface PublicRoutingMetricsResponse {
79
79
  posteriors: RoutingPosteriorResponse[]
80
80
  recentDecisions: RoutingDecisionResponse[]
81
+ nextCursor?: string
81
82
  summary: RoutingSummaryResponse
82
83
  timestamp: string
83
84
  }
85
+
86
+ export type WorkflowPhase = 'development' | 'qa' | 'refinement' | 'acceptance'
87
+ export type EscalationStrategy = 'normal' | 'context-enriched' | 'decompose' | 'escalate-human'
88
+
89
+ export interface PhaseMetricsDetail {
90
+ avgCycleTimeMs: number
91
+ avgCostUsd: number
92
+ avgAttempts: number
93
+ totalRecords: number
94
+ }
95
+
96
+ export interface PhaseMetricsResponse {
97
+ timeRange: '7d' | '30d' | '90d'
98
+ phases: Record<WorkflowPhase, PhaseMetricsDetail>
99
+ reworkRate: number
100
+ escalationDistribution: Record<EscalationStrategy, number>
101
+ issueCount: number
102
+ timestamp: string
103
+ }