diff --git a/frontend/src/api/flowpilotAnalytics.ts b/frontend/src/api/flowpilotAnalytics.ts index 0f4ccca4..27552bee 100644 --- a/frontend/src/api/flowpilotAnalytics.ts +++ b/frontend/src/api/flowpilotAnalytics.ts @@ -1,5 +1,12 @@ import apiClient from './client' -import type { FlowPilotDashboard, KnowledgeGapReport, CoverageResponse, FlowQualityResponse, EnhancedPsaMetrics } from '@/types/flowpilot-analytics' +import type { + FlowPilotDashboard, + KnowledgeGapReport, + CoverageResponse, + FlowQualityResponse, + EnhancedPsaMetrics, + EscalationMetrics, +} from '@/types/flowpilot-analytics' export const flowpilotAnalyticsApi = { async getDashboard(period: string = '30d'): Promise { @@ -36,6 +43,13 @@ export const flowpilotAnalyticsApi = { }) return response.data }, + + async getEscalationMetrics(period: string = '30d'): Promise { + const response = await apiClient.get('/analytics/flowpilot/escalations', { + params: { period }, + }) + return response.data + }, } export default flowpilotAnalyticsApi diff --git a/frontend/src/components/flowpilot/EscalationMetricCard.tsx b/frontend/src/components/flowpilot/EscalationMetricCard.tsx new file mode 100644 index 00000000..78b97d34 --- /dev/null +++ b/frontend/src/components/flowpilot/EscalationMetricCard.tsx @@ -0,0 +1,130 @@ +import { useEffect, useState } from 'react' +import { Clock, TrendingUp, AlertCircle } from 'lucide-react' +import { flowpilotAnalyticsApi } from '@/api' +import type { EscalationMetrics } from '@/types/flowpilot-analytics' + +interface EscalationMetricCardProps { + period?: string +} + +function formatSeconds(s: number | null): string { + if (s === null) return '—' + if (s < 60) return `${Math.round(s)}s` + const mins = s / 60 + if (mins < 10) return `${mins.toFixed(1)} min` + return `${Math.round(mins)} min` +} + +/** + * Shows the in-product time-to-first-action metric above the EscalationQueue. + * + * NOTE: this is the in-product metric only. The "minutes recovered" sales + * claim requires a manual baseline measurement (see The Assignment in + * docs/plans/2026-04-27-escalation-mode-wedge-design.md). Frame the number + * as "time-to-first-action with structured handoff," not "minutes saved." + */ +export function EscalationMetricCard({ period = '30d' }: EscalationMetricCardProps) { + const [metrics, setMetrics] = useState(null) + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + let cancelled = false + + const load = async () => { + setIsLoading(true) + setError(null) + try { + const data = await flowpilotAnalyticsApi.getEscalationMetrics(period) + if (!cancelled) setMetrics(data) + } catch { + if (!cancelled) setError('Failed to load metric') + } finally { + if (!cancelled) setIsLoading(false) + } + } + load() + return () => { + cancelled = true + } + }, [period]) + + if (isLoading) { + return ( +
+
+
+ ) + } + + if (error) { + return ( +
+ + {error} +
+ ) + } + + if (!metrics || metrics.n_handoffs_claimed === 0) { + return ( +
+

+ Time to first action ({period}) +

+

+ No claimed escalations yet. Once your team starts using Pick Up, + we'll measure how fast they get into resolution. +

+
+ ) + } + + const avgLabel = formatSeconds(metrics.avg_seconds_to_first_action) + const medianLabel = formatSeconds(metrics.median_seconds_to_first_action) + const conversionRate = + metrics.n_handoffs_claimed > 0 + ? Math.round( + (metrics.n_handoffs_with_action / metrics.n_handoffs_claimed) * 100, + ) + : 0 + + return ( +
+
+ + Time to first action — last {period} +
+ +
+
+ + {avgLabel} + + avg +
+
+ {medianLabel} median +
+
+ + {metrics.n_handoffs_with_action} + + /{metrics.n_handoffs_claimed} claimed escalations + + ({conversionRate}% reached first action) + +
+
+ +

+ + + In-product measurement only. The savings claim requires a manual + baseline of pre-Escalation-Mode handoff time. See your team's + Assignment for the baseline number. + +

+
+ ) +} diff --git a/frontend/src/components/flowpilot/index.ts b/frontend/src/components/flowpilot/index.ts index 3fe5cc4e..0cdb9db0 100644 --- a/frontend/src/components/flowpilot/index.ts +++ b/frontend/src/components/flowpilot/index.ts @@ -9,6 +9,7 @@ export { AISessionListItem } from './AISessionListItem' export { SessionTicketCard } from './SessionTicketCard' export { EscalateModal } from './EscalateModal' export { EscalationQueue } from './EscalationQueue' +export { EscalationMetricCard } from './EscalationMetricCard' export { SessionBriefing } from './SessionBriefing' export { ProposalCard } from './ProposalCard' export { ProposalDetail } from './ProposalDetail' diff --git a/frontend/src/pages/EscalationQueuePage.tsx b/frontend/src/pages/EscalationQueuePage.tsx index cddbff18..5ae5a20e 100644 --- a/frontend/src/pages/EscalationQueuePage.tsx +++ b/frontend/src/pages/EscalationQueuePage.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { AlertTriangle } from 'lucide-react' -import { EscalationQueue } from '@/components/flowpilot' +import { EscalationQueue, EscalationMetricCard } from '@/components/flowpilot' export default function EscalationQueuePage() { const [count, setCount] = useState(null) @@ -21,6 +21,8 @@ export default function EscalationQueuePage() {
+ + ) diff --git a/frontend/src/types/flowpilot-analytics.ts b/frontend/src/types/flowpilot-analytics.ts index f1446f43..b5767060 100644 --- a/frontend/src/types/flowpilot-analytics.ts +++ b/frontend/src/types/flowpilot-analytics.ts @@ -134,3 +134,16 @@ export interface EnhancedPsaMetrics { push_funnel: PsaFunnel daily_trend: PsaDailyTrend[] } + +// Escalation Mode wedge metric — in-product time-to-first-action. +// Pair with a manual baseline measurement for the savings claim. +// See docs/plans/2026-04-27-escalation-mode-wedge-design.md. +export interface EscalationMetrics { + period: string + n_handoffs_claimed: number + n_handoffs_with_action: number + avg_seconds_to_first_action: number | null + median_seconds_to_first_action: number | null + p95_seconds_to_first_action: number | null + metric_definition: string +}