diff --git a/frontend/src/components/analytics/CoverageHeatmap.tsx b/frontend/src/components/analytics/CoverageHeatmap.tsx index be3e7d8d..a66560d1 100644 --- a/frontend/src/components/analytics/CoverageHeatmap.tsx +++ b/frontend/src/components/analytics/CoverageHeatmap.tsx @@ -18,6 +18,18 @@ function getCellStyle(value: number, thresholds: { green: number; amber: number return 'bg-rose-500/10 text-rose-500' } +function getCellTitle(value: number, label: string, thresholds: { green: number; amber: number }, inverse?: boolean): string { + const pct = (value * 100).toFixed(1) + if (inverse) { + if (value <= thresholds.green) return `${label}: ${pct}% (Good — below ${(thresholds.green * 100).toFixed(0)}%)` + if (value <= thresholds.amber) return `${label}: ${pct}% (Needs Improvement — below ${(thresholds.amber * 100).toFixed(0)}%)` + return `${label}: ${pct}% (Critical — above ${(thresholds.amber * 100).toFixed(0)}%)` + } + if (value >= thresholds.green) return `${label}: ${pct}% (Good — above ${(thresholds.green * 100).toFixed(0)}%)` + if (value >= thresholds.amber) return `${label}: ${pct}% (Needs Improvement — above ${(thresholds.amber * 100).toFixed(0)}%)` + return `${label}: ${pct}% (Critical — below ${(thresholds.amber * 100).toFixed(0)}%)` +} + function getFlowCountStyle(count: number) { if (count >= 5) return 'bg-emerald-400/10 text-emerald-400' if (count >= 1) return 'bg-amber-400/10 text-amber-400' @@ -91,7 +103,10 @@ export default function CoverageHeatmap({ data }: CoverageHeatmapProps) { Create Flow ) : ( - + = 5 ? 'Good' : row.flow_count >= 1 ? 'Needs Improvement' : 'Critical'}`} + > {row.flow_count} )} @@ -100,17 +115,26 @@ export default function CoverageHeatmap({ data }: CoverageHeatmapProps) { {row.session_count} - + {(row.resolution_rate * 100).toFixed(1)}% - + {(row.escalation_rate * 100).toFixed(1)}% - + {(row.guided_rate * 100).toFixed(1)}% @@ -128,6 +152,12 @@ export default function CoverageHeatmap({ data }: CoverageHeatmapProps) { )} + +
+ Good + Needs Improvement + Critical +
) } diff --git a/frontend/src/components/analytics/FlowQualityTable.tsx b/frontend/src/components/analytics/FlowQualityTable.tsx index 428fa711..9ea24c32 100644 --- a/frontend/src/components/analytics/FlowQualityTable.tsx +++ b/frontend/src/components/analytics/FlowQualityTable.tsx @@ -1,7 +1,8 @@ import { useState, useMemo } from 'react' import { Link } from 'react-router-dom' -import { ArrowUpDown } from 'lucide-react' +import { ArrowUpDown, ArrowUp, ArrowDown, Info } from 'lucide-react' import { cn } from '@/lib/utils' +import { getTreeEditorPath } from '@/lib/routing' import type { FlowQualityResponse, FlowQualityRow } from '@/types/flowpilot-analytics' interface FlowQualityTableProps { @@ -113,21 +114,29 @@ export default function FlowQualityTable({ data }: FlowQualityTableProps) { ) } - const columns: { key: SortColumn; label: string }[] = [ + const columns: { key: SortColumn; label: string; title?: 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' }, + { key: 'quality_score', label: 'Quality Score', title: 'Combines success rate (50%), AI confidence (30%), and recent usage (20%)' }, ] return (
-

- Flow Quality Scores -

+
+

+ Flow Quality Scores +

+ + +
+ Quality score combines success rate (50%), AI confidence level (30%), and recent usage (20%). Higher is better. +
+
+

Performance and usage metrics for your troubleshooting flows

@@ -137,24 +146,31 @@ export default function FlowQualityTable({ data }: FlowQualityTableProps) { - {columns.map((col) => ( - - ))} + {columns.map((col) => { + const isActive = sortCol === col.key + const SortIcon = isActive + ? sortDir === 'asc' ? ArrowUp : ArrowDown + : ArrowUpDown + return ( + + ) + })} @@ -194,7 +210,7 @@ function FlowRow({
handleSort(col.key)} - > - - {col.label} - - - handleSort(col.key)} + title={col.title} + > + + {col.label} + + +
{flow.name} diff --git a/frontend/src/components/analytics/PsaMetricsPanel.tsx b/frontend/src/components/analytics/PsaMetricsPanel.tsx index 28dd88d3..7cf098bb 100644 --- a/frontend/src/components/analytics/PsaMetricsPanel.tsx +++ b/frontend/src/components/analytics/PsaMetricsPanel.tsx @@ -34,19 +34,19 @@ export default function PsaMetricsPanel({ data }: PsaMetricsPanelProps) { {/* Row 1 — Metric cards */}
-

{data.total_time_entries}

+

Time Entries

+

{data.total_time_entries}

logged to PSA

-

Time Entries

-

{data.total_hours_logged.toFixed(1)}

+

Hours Logged

+

{data.total_hours_logged.toFixed(1)}

total hours tracked

-

Hours Logged

-

{data.avg_hours_per_session.toFixed(2)}

+

Avg Hours/Session

+

{data.avg_hours_per_session.toFixed(2)}

per resolved session

-

Avg Hours/Session

diff --git a/frontend/src/pages/FlowPilotAnalyticsPage.tsx b/frontend/src/pages/FlowPilotAnalyticsPage.tsx index 2076c616..3ac65901 100644 --- a/frontend/src/pages/FlowPilotAnalyticsPage.tsx +++ b/frontend/src/pages/FlowPilotAnalyticsPage.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react' import { Link } from 'react-router-dom' import { BarChart3, Clock, Star, CheckCircle2, ArrowUpRight, - AlertTriangle, Lightbulb, Loader2, Ticket, + AlertTriangle, Lightbulb, Loader2, Ticket, RotateCcw, } from 'lucide-react' import { AreaChart, Area, BarChart, Bar, @@ -55,6 +55,12 @@ export default function FlowPilotAnalyticsPage() { const [psaData, setPsaData] = useState(null) const [psaLoading, setPsaLoading] = useState(false) + // Error states per tab + const [coverageError, setCoverageError] = useState(false) + const [qualityError, setQualityError] = useState(false) + const [psaError, setPsaError] = useState(false) + const [retryKey, setRetryKey] = useState(0) + // Track which period each tab was loaded for const coveragePeriodRef = useRef(null) const qualityPeriodRef = useRef(null) @@ -81,41 +87,59 @@ export default function FlowPilotAnalyticsPage() { setCoverageData(null) setQualityData(null) setPsaData(null) + setCoverageLoading(true) + setQualityLoading(true) + setPsaLoading(true) + setCoverageError(false) + setQualityError(false) + setPsaError(false) }, [period]) // Lazy-load tab data on tab switch or period change useEffect(() => { if (activeTab === 'coverage' && coveragePeriodRef.current !== period) { setCoverageLoading(true) + setCoverageError(false) flowpilotAnalyticsApi.getCoverage(period) .then((data) => { setCoverageData(data) coveragePeriodRef.current = period }) - .catch(() => toast.error('Failed to load coverage data')) + .catch(() => { + toast.error('Failed to load coverage data') + setCoverageError(true) + }) .finally(() => setCoverageLoading(false)) } if (activeTab === 'quality' && qualityPeriodRef.current !== period) { setQualityLoading(true) + setQualityError(false) flowpilotAnalyticsApi.getFlowQuality(period) .then((data) => { setQualityData(data) qualityPeriodRef.current = period }) - .catch(() => toast.error('Failed to load flow quality data')) + .catch(() => { + toast.error('Failed to load flow quality data') + setQualityError(true) + }) .finally(() => setQualityLoading(false)) } if (activeTab === 'psa' && psaPeriodRef.current !== period) { setPsaLoading(true) + setPsaError(false) flowpilotAnalyticsApi.getPsaMetrics(period) .then((data) => { setPsaData(data) psaPeriodRef.current = period }) - .catch(() => toast.error('Failed to load PSA metrics')) + .catch(() => { + toast.error('Failed to load PSA metrics') + setPsaError(true) + }) .finally(() => setPsaLoading(false)) } - }, [activeTab, period]) + }, [activeTab, period, retryKey]) if (loading) { return ( @@ -165,7 +189,7 @@ export default function FlowPilotAnalyticsPage() {
{/* Tab bar */} -
+
{TABS.map((tab) => (
)} {activeTab === 'quality' && (
- {qualityLoading ? ( + {qualityError ? ( + { setQualityError(false); qualityPeriodRef.current = null; setRetryKey((k) => k + 1) }} /> + ) : qualityLoading ? (
) : qualityData ? ( - ) : ( -
-

No flow quality data available

-
- )} + ) : null}
)} {activeTab === 'psa' && (
- {psaLoading ? ( + {psaError ? ( + { setPsaError(false); psaPeriodRef.current = null; setRetryKey((k) => k + 1) }} /> + ) : psaLoading ? (
) : psaData ? ( - ) : ( -
-

No PSA data available

-
- )} + ) : null}
)}
@@ -498,6 +516,22 @@ function ConfidenceTierRow({ ) } +function ErrorRetry({ label, onRetry }: { label: string; onRetry: () => void }) { + return ( +
+ +

Failed to load {label}

+ +
+ ) +} + function GapCard({ gap }: { gap: KnowledgeGap }) { const severityStyle = SEVERITY_STYLES[gap.severity as keyof typeof SEVERITY_STYLES] ?? SEVERITY_STYLES.low return (