diff --git a/frontend/src/components/analytics/FlowQualityTable.tsx b/frontend/src/components/analytics/FlowQualityTable.tsx new file mode 100644 index 00000000..428fa711 --- /dev/null +++ b/frontend/src/components/analytics/FlowQualityTable.tsx @@ -0,0 +1,244 @@ +import { useState, useMemo } from 'react' +import { Link } from 'react-router-dom' +import { ArrowUpDown } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { FlowQualityResponse, FlowQualityRow } from '@/types/flowpilot-analytics' + +interface FlowQualityTableProps { + data: FlowQualityResponse +} + +type SortColumn = 'name' | 'usage_count' | 'success_rate' | 'last_matched_at' | 'avg_confidence' | 'quality_score' +type SortDir = 'asc' | 'desc' + +function formatRelativeTime(dateStr: string | null): string { + if (!dateStr) return 'Never' + const diff = Date.now() - new Date(dateStr).getTime() + const minutes = Math.floor(diff / 60000) + if (minutes < 1) return 'Just now' + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + const days = Math.floor(hours / 24) + if (days < 30) return `${days}d ago` + const months = Math.floor(days / 30) + return `${months}mo ago` +} + +function rateColor(value: number | null): string { + if (value === null) return 'text-muted-foreground' + if (value > 75) return 'text-emerald-400' + if (value >= 50) return 'text-amber-400' + return 'text-rose-500' +} + +function barColor(score: number): string { + if (score > 0.7) return 'bg-emerald-400' + if (score >= 0.4) return 'bg-amber-400' + return 'bg-rose-500' +} + +export default function FlowQualityTable({ data }: FlowQualityTableProps) { + const [sortCol, setSortCol] = useState('quality_score') + const [sortDir, setSortDir] = useState('desc') + + const topPerformerIds = useMemo( + () => new Set(data.top_performers.map((f) => f.flow_id)), + [data.top_performers], + ) + const needsAttentionIds = useMemo( + () => new Set(data.needs_attention.map((f) => f.flow_id)), + [data.needs_attention], + ) + + const sorted = useMemo(() => { + const rows = [...data.flows] + rows.sort((a, b) => { + let aVal: number | string + let bVal: number | string + + switch (sortCol) { + case 'name': + aVal = a.name.toLowerCase() + bVal = b.name.toLowerCase() + break + case 'usage_count': + aVal = a.usage_count + bVal = b.usage_count + break + case 'success_rate': + aVal = a.success_rate ?? -1 + bVal = b.success_rate ?? -1 + break + case 'last_matched_at': + aVal = a.last_matched_at ? new Date(a.last_matched_at).getTime() : 0 + bVal = b.last_matched_at ? new Date(b.last_matched_at).getTime() : 0 + break + case 'avg_confidence': + aVal = a.avg_confidence ?? -1 + bVal = b.avg_confidence ?? -1 + break + case 'quality_score': + default: + aVal = a.quality_score + bVal = b.quality_score + break + } + + if (aVal < bVal) return sortDir === 'asc' ? -1 : 1 + if (aVal > bVal) return sortDir === 'asc' ? 1 : -1 + return 0 + }) + return rows + }, [data.flows, sortCol, sortDir]) + + function handleSort(col: SortColumn) { + if (sortCol === col) { + setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')) + } else { + setSortCol(col) + setSortDir('desc') + } + } + + if (data.flows.length === 0) { + return ( +
+
+

+ No flows found for this account. Create your first flow to start tracking quality. +

+
+
+ ) + } + + const columns: { key: SortColumn; label: string }[] = [ + { key: 'name', label: 'Flow Name' }, + { key: 'usage_count', label: 'Usage' }, + { key: 'success_rate', label: 'Success Rate' }, + { key: 'last_matched_at', label: 'Last Used' }, + { key: 'avg_confidence', label: 'Avg Confidence' }, + { key: 'quality_score', label: 'Quality Score' }, + ] + + return ( +
+
+

+ Flow Quality Scores +

+

+ Performance and usage metrics for your troubleshooting flows +

+
+ +
+ + + + {columns.map((col) => ( + + ))} + + + + {sorted.map((flow) => ( + + ))} + +
handleSort(col.key)} + > + + {col.label} + + +
+
+
+ ) +} + +function FlowRow({ + flow, + isTopPerformer, + needsAttention, +}: { + flow: FlowQualityRow + isTopPerformer: boolean + needsAttention: boolean +}) { + return ( + + {/* Flow Name */} + +
+ + {flow.name} + + {needsAttention && ( + + Needs attention + + )} +
+ + + {/* Usage */} + {flow.usage_count} + + {/* Success Rate */} + + {flow.success_rate !== null ? `${flow.success_rate.toFixed(1)}%` : '--'} + + + {/* Last Used */} + + {formatRelativeTime(flow.last_matched_at)} + + + {/* Avg Confidence */} + + {flow.avg_confidence !== null ? `${flow.avg_confidence.toFixed(1)}%` : '--'} + + + {/* Quality Score */} + +
+
+
+
+ + {(flow.quality_score * 100).toFixed(0)} + +
+ + + ) +} diff --git a/frontend/src/components/analytics/PsaMetricsPanel.tsx b/frontend/src/components/analytics/PsaMetricsPanel.tsx new file mode 100644 index 00000000..28dd88d3 --- /dev/null +++ b/frontend/src/components/analytics/PsaMetricsPanel.tsx @@ -0,0 +1,182 @@ +import { ArrowRight, ArrowDown } from 'lucide-react' +import { + ComposedChart, Area, Bar, + XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, +} from 'recharts' +import type { EnhancedPsaMetrics, PsaFunnel } from '@/types/flowpilot-analytics' + +interface PsaMetricsPanelProps { + data: EnhancedPsaMetrics +} + +function funnelPct(from: number, to: number): string { + if (from === 0) return '0%' + return `${Math.round((to / from) * 100)}%` +} + +export default function PsaMetricsPanel({ data }: PsaMetricsPanelProps) { + const hasActivity = data.total_time_entries > 0 || data.push_funnel.total_sessions > 0 || data.daily_trend.length > 0 + + if (!hasActivity) { + return ( +
+
+

+ No PSA activity data for this period. Link sessions to PSA tickets to see metrics. +

+
+
+ ) + } + + return ( +
+ {/* Row 1 — Metric cards */} +
+
+

{data.total_time_entries}

+

logged to PSA

+

Time Entries

+
+
+

{data.total_hours_logged.toFixed(1)}

+

total hours tracked

+

Hours Logged

+
+
+

{data.avg_hours_per_session.toFixed(2)}

+

per resolved session

+

Avg Hours/Session

+
+
+ + {/* Row 2 — Push Success Funnel */} + + + {/* Row 3 — Daily Trend Chart */} + {data.daily_trend.length > 0 && ( +
+

+ PSA Activity Trend +

+ + + + new Date(d).toLocaleDateString([], { month: 'short', day: 'numeric' })} + /> + + + [ + name === 'hours' ? `${(value ?? 0).toFixed(1)}h` : (value ?? 0), + name === 'hours' ? 'Hours' : 'Entries', + ]) as never} + /> + + + + + + + + + + +
+ )} +
+ ) +} + +function FunnelCard({ funnel }: { funnel: PsaFunnel }) { + const steps = [ + { label: 'Sessions', count: funnel.total_sessions }, + { label: 'Linked', count: funnel.linked_to_ticket }, + { label: 'Doc Pushed', count: funnel.doc_pushed }, + { label: 'Time Entry', count: funnel.time_entry_logged }, + ] + + return ( +
+

+ Documentation Push Funnel +

+ + {/* Desktop: horizontal */} +
+ {steps.map((step, i) => ( +
+
+

+ {step.label} +

+

{step.count}

+
+ {i < steps.length - 1 && ( +
+ + + {funnelPct(steps[i].count, steps[i + 1].count)} + +
+ )} +
+ ))} +
+ + {/* Mobile: vertical */} +
+ {steps.map((step, i) => ( +
+
+

+ {step.label} +

+

{step.count}

+
+ {i < steps.length - 1 && ( +
+ + + {funnelPct(steps[i].count, steps[i + 1].count)} + +
+ )} +
+ ))} +
+
+ ) +} diff --git a/frontend/src/pages/FlowPilotAnalyticsPage.tsx b/frontend/src/pages/FlowPilotAnalyticsPage.tsx index ca1d0457..2076c616 100644 --- a/frontend/src/pages/FlowPilotAnalyticsPage.tsx +++ b/frontend/src/pages/FlowPilotAnalyticsPage.tsx @@ -12,6 +12,8 @@ import { cn } from '@/lib/utils' import { flowpilotAnalyticsApi } from '@/api' import { toast } from '@/lib/toast' import CoverageHeatmap from '@/components/analytics/CoverageHeatmap' +import FlowQualityTable from '@/components/analytics/FlowQualityTable' +import PsaMetricsPanel from '@/components/analytics/PsaMetricsPanel' import type { FlowPilotDashboard, KnowledgeGapReport, KnowledgeGap, CoverageResponse, FlowQualityResponse, EnhancedPsaMetrics, @@ -405,13 +407,7 @@ export default function FlowPilotAnalyticsPage() {
) : qualityData ? ( -
-

Flow Quality

-

Quality scores and performance metrics for your flows

-
- {qualityData.flows.length} flows analyzed -
-
+ ) : (

No flow quality data available

@@ -427,13 +423,7 @@ export default function FlowPilotAnalyticsPage() {
) : psaData ? ( -
-

PSA Metrics

-

Time entry and ticket integration metrics

-
- {psaData.total_time_entries} time entries logged ({psaData.total_hours_logged.toFixed(1)} hours) -
-
+ ) : (

No PSA data available