diff --git a/frontend/src/api/flowpilotAnalytics.ts b/frontend/src/api/flowpilotAnalytics.ts index 459e0b6c..0f4ccca4 100644 --- a/frontend/src/api/flowpilotAnalytics.ts +++ b/frontend/src/api/flowpilotAnalytics.ts @@ -1,5 +1,5 @@ import apiClient from './client' -import type { FlowPilotDashboard, KnowledgeGapReport } from '@/types/flowpilot-analytics' +import type { FlowPilotDashboard, KnowledgeGapReport, CoverageResponse, FlowQualityResponse, EnhancedPsaMetrics } from '@/types/flowpilot-analytics' export const flowpilotAnalyticsApi = { async getDashboard(period: string = '30d'): Promise { @@ -15,6 +15,27 @@ export const flowpilotAnalyticsApi = { }) return response.data }, + + async getCoverage(period: string = '30d'): Promise { + const response = await apiClient.get('/analytics/flowpilot/coverage', { + params: { period }, + }) + return response.data + }, + + async getFlowQuality(period: string = '30d', sort: string = 'quality'): Promise { + const response = await apiClient.get('/analytics/flowpilot/flow-quality', { + params: { period, sort }, + }) + return response.data + }, + + async getPsaMetrics(period: string = '30d'): Promise { + const response = await apiClient.get('/analytics/flowpilot/psa-metrics', { + params: { period }, + }) + return response.data + }, } export default flowpilotAnalyticsApi diff --git a/frontend/src/components/analytics/CoverageHeatmap.tsx b/frontend/src/components/analytics/CoverageHeatmap.tsx new file mode 100644 index 00000000..be3e7d8d --- /dev/null +++ b/frontend/src/components/analytics/CoverageHeatmap.tsx @@ -0,0 +1,133 @@ +import { Link } from 'react-router-dom' +import { Plus, MapPin } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { CoverageResponse } from '@/types/flowpilot-analytics' + +interface CoverageHeatmapProps { + data: CoverageResponse +} + +function getCellStyle(value: number, thresholds: { green: number; amber: number }, inverse?: boolean) { + if (inverse) { + if (value <= thresholds.green) return 'bg-emerald-400/10 text-emerald-400' + if (value <= thresholds.amber) return 'bg-amber-400/10 text-amber-400' + return 'bg-rose-500/10 text-rose-500' + } + if (value >= thresholds.green) return 'bg-emerald-400/10 text-emerald-400' + if (value >= thresholds.amber) return 'bg-amber-400/10 text-amber-400' + return 'bg-rose-500/10 text-rose-500' +} + +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' + return 'bg-rose-500/10 text-rose-500' +} + +export default function CoverageHeatmap({ data }: CoverageHeatmapProps) { + if (data.domains.length === 0) { + return ( +
+
+ +

Domain Coverage

+
+

+ No session data for this period. Start using FlowPilot to see coverage metrics. +

+
+ ) + } + + return ( +
+
+
+ +

Domain Coverage

+
+

+ Resolution coverage and knowledge gaps by problem domain +

+
+ +
+ + + + + + + + + + + + + {data.domains.map((row) => ( + + + + + + + + + ))} + + {data.unmapped_session_count > 0 && ( + + + + + + )} +
+ Domain + + Flows + + Sessions + + Resolution % + + Escalation % + + Guided % +
+ {row.domain} + + {row.flow_count === 0 ? ( + + + Create Flow + + ) : ( + + {row.flow_count} + + )} + + {row.session_count} + + + {(row.resolution_rate * 100).toFixed(1)}% + + + + {(row.escalation_rate * 100).toFixed(1)}% + + + + {(row.guided_rate * 100).toFixed(1)}% + +
+ {data.unmapped_session_count} sessions had no domain classification +
+
+
+ ) +} diff --git a/frontend/src/pages/FlowPilotAnalyticsPage.tsx b/frontend/src/pages/FlowPilotAnalyticsPage.tsx index 7c7ff7dc..ca1d0457 100644 --- a/frontend/src/pages/FlowPilotAnalyticsPage.tsx +++ b/frontend/src/pages/FlowPilotAnalyticsPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { Link } from 'react-router-dom' import { BarChart3, Clock, Star, CheckCircle2, ArrowUpRight, @@ -8,9 +8,14 @@ import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from 'recharts' +import { cn } from '@/lib/utils' import { flowpilotAnalyticsApi } from '@/api' import { toast } from '@/lib/toast' -import type { FlowPilotDashboard, KnowledgeGapReport, KnowledgeGap } from '@/types/flowpilot-analytics' +import CoverageHeatmap from '@/components/analytics/CoverageHeatmap' +import type { + FlowPilotDashboard, KnowledgeGapReport, KnowledgeGap, + CoverageResponse, FlowQualityResponse, EnhancedPsaMetrics, +} from '@/types/flowpilot-analytics' const PERIOD_OPTIONS = [ { value: '7d', label: 'Last 7 days' }, @@ -18,6 +23,15 @@ const PERIOD_OPTIONS = [ { value: '90d', label: 'Last 90 days' }, ] +const TABS = [ + { id: 'overview', label: 'Overview' }, + { id: 'coverage', label: 'Coverage' }, + { id: 'quality', label: 'Flow Quality' }, + { id: 'psa', label: 'PSA' }, +] as const + +type TabId = typeof TABS[number]['id'] + const SEVERITY_STYLES = { high: 'bg-rose-500/10 text-rose-500 border-rose-500/20', medium: 'bg-amber-400/10 text-amber-400 border-amber-400/20', @@ -26,10 +40,25 @@ const SEVERITY_STYLES = { export default function FlowPilotAnalyticsPage() { const [period, setPeriod] = useState('30d') + const [activeTab, setActiveTab] = useState('overview') const [dashboard, setDashboard] = useState(null) const [gaps, setGaps] = useState(null) const [loading, setLoading] = useState(true) + // Lazy-loaded tab data + const [coverageData, setCoverageData] = useState(null) + const [coverageLoading, setCoverageLoading] = useState(false) + const [qualityData, setQualityData] = useState(null) + const [qualityLoading, setQualityLoading] = useState(false) + const [psaData, setPsaData] = useState(null) + const [psaLoading, setPsaLoading] = useState(false) + + // Track which period each tab was loaded for + const coveragePeriodRef = useRef(null) + const qualityPeriodRef = useRef(null) + const psaPeriodRef = useRef(null) + + // Load overview data useEffect(() => { setLoading(true) Promise.all([ @@ -42,8 +71,50 @@ export default function FlowPilotAnalyticsPage() { }) .catch(() => toast.error('Failed to load analytics')) .finally(() => setLoading(false)) + + // Reset lazy-loaded data when period changes + coveragePeriodRef.current = null + qualityPeriodRef.current = null + psaPeriodRef.current = null + setCoverageData(null) + setQualityData(null) + setPsaData(null) }, [period]) + // Lazy-load tab data on tab switch or period change + useEffect(() => { + if (activeTab === 'coverage' && coveragePeriodRef.current !== period) { + setCoverageLoading(true) + flowpilotAnalyticsApi.getCoverage(period) + .then((data) => { + setCoverageData(data) + coveragePeriodRef.current = period + }) + .catch(() => toast.error('Failed to load coverage data')) + .finally(() => setCoverageLoading(false)) + } + if (activeTab === 'quality' && qualityPeriodRef.current !== period) { + setQualityLoading(true) + flowpilotAnalyticsApi.getFlowQuality(period) + .then((data) => { + setQualityData(data) + qualityPeriodRef.current = period + }) + .catch(() => toast.error('Failed to load flow quality data')) + .finally(() => setQualityLoading(false)) + } + if (activeTab === 'psa' && psaPeriodRef.current !== period) { + setPsaLoading(true) + flowpilotAnalyticsApi.getPsaMetrics(period) + .then((data) => { + setPsaData(data) + psaPeriodRef.current = period + }) + .catch(() => toast.error('Failed to load PSA metrics')) + .finally(() => setPsaLoading(false)) + } + }, [activeTab, period]) + if (loading) { return (
@@ -91,201 +162,284 @@ export default function FlowPilotAnalyticsPage() {
- {/* Top row — Key metrics */} -
- - - - - + {/* Tab bar */} +
+ {TABS.map((tab) => ( + + ))}
- {/* Second row — Charts */} -
- {/* MTTR Trend */} -
-

- MTTR Trend -

- {dashboard.mttr_trend.length > 0 ? ( - - - - new Date(d).toLocaleDateString([], { month: 'short', day: 'numeric' })} + {/* Tab content */} + {activeTab === 'overview' && ( +
+ {/* Top row — Key metrics */} +
+ + + + + +
+ + {/* Second row — Charts */} +
+ {/* MTTR Trend */} +
+

+ MTTR Trend +

+ {dashboard.mttr_trend.length > 0 ? ( + + + + new Date(d).toLocaleDateString([], { month: 'short', day: 'numeric' })} + /> + `${Math.round(v)}m`} + /> + [`${Math.round(v ?? 0)}m`, 'MTTR']} + /> + + + + + + + + + + ) : ( +
+ No data for this period +
+ )} +
+ + {/* Domain Breakdown */} +
+

+ Sessions by Domain +

+ {dashboard.sessions_by_domain.length > 0 ? ( + + + + + + + + + + + ) : ( +
+ No domain data +
+ )} +
+
+ + {/* Third row — Confidence + Knowledge Coverage */} +
+ {/* Confidence Breakdown */} +
+

+ Confidence Tiers +

+
+ - `${Math.round(v)}m`} + - [`${Math.round(v ?? 0)}m`, 'MTTR']} + - - - - - - - - - +
+
+ + {/* Knowledge Coverage */} +
+

+ Knowledge Coverage +

+
+
+

Total Flows

+

{dashboard.knowledge_coverage.total_flows}

+
+
+

AI-Generated

+

{dashboard.knowledge_coverage.ai_generated_flows}

+
+
+

Pending Review

+

{dashboard.knowledge_coverage.total_proposals_pending}

+
+
+

Approved This Period

+

{dashboard.knowledge_coverage.proposals_approved_this_period}

+
+
+ {dashboard.knowledge_coverage.total_proposals_pending > 0 && ( + + + Review {dashboard.knowledge_coverage.total_proposals_pending} pending proposals + + )} +
+
+ + {/* Fourth row — Knowledge Gaps */} + {gaps && gaps.gaps.length > 0 && ( +
+
+ +

+ Knowledge Gaps ({gaps.gaps.length}) +

+
+
+ {gaps.gaps.map((gap, i) => ( + + ))} +
+
+ )} +
+ )} + + {activeTab === 'coverage' && ( +
+ {coverageLoading ? ( +
+ +
+ ) : coverageData ? ( + ) : ( -
- No data for this period +
+

No coverage data available

)}
+ )} - {/* Domain Breakdown */} -
-

- Sessions by Domain -

- {dashboard.sessions_by_domain.length > 0 ? ( - - - - - - - - - - + {activeTab === 'quality' && ( +
+ {qualityLoading ? ( +
+ +
+ ) : qualityData ? ( +
+

Flow Quality

+

Quality scores and performance metrics for your flows

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

No flow quality data available

)}
-
+ )} - {/* Third row — Confidence + Knowledge Coverage */} -
- {/* Confidence Breakdown */} -
-

- Confidence Tiers -

-
- - - -
-
- - {/* Knowledge Coverage */} -
-

- Knowledge Coverage -

-
-
-

Total Flows

-

{dashboard.knowledge_coverage.total_flows}

+ {activeTab === 'psa' && ( +
+ {psaLoading ? ( +
+
-
-

AI-Generated

-

{dashboard.knowledge_coverage.ai_generated_flows}

+ ) : psaData ? ( +
+

PSA Metrics

+

Time entry and ticket integration metrics

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

Pending Review

-

{dashboard.knowledge_coverage.total_proposals_pending}

+ ) : ( +
+

No PSA data available

-
-

Approved This Period

-

{dashboard.knowledge_coverage.proposals_approved_this_period}

-
-
- {dashboard.knowledge_coverage.total_proposals_pending > 0 && ( - - - Review {dashboard.knowledge_coverage.total_proposals_pending} pending proposals - )}
-
- - {/* Fourth row — Knowledge Gaps */} - {gaps && gaps.gaps.length > 0 && ( -
-
- -

- Knowledge Gaps ({gaps.gaps.length}) -

-
-
- {gaps.gaps.map((gap, i) => ( - - ))} -
-
)}
) diff --git a/frontend/src/types/flowpilot-analytics.ts b/frontend/src/types/flowpilot-analytics.ts index cd843d94..f1446f43 100644 --- a/frontend/src/types/flowpilot-analytics.ts +++ b/frontend/src/types/flowpilot-analytics.ts @@ -77,3 +77,60 @@ export interface KnowledgeGapReport { generated_at: string gaps: KnowledgeGap[] } + +// Phase 5 — Coverage +export interface CoverageDomainRow { + domain: string + flow_count: number + session_count: number + resolution_rate: number + escalation_rate: number + guided_rate: number + avg_resolution_minutes: number | null +} + +export interface CoverageResponse { + domains: CoverageDomainRow[] + unmapped_session_count: number + total_domains: number +} + +// Phase 5 — Flow Quality +export interface FlowQualityRow { + flow_id: string + name: string + tree_type: string + usage_count: number + success_rate: number | null + last_matched_at: string | null + avg_confidence: number | null + quality_score: number +} + +export interface FlowQualityResponse { + flows: FlowQualityRow[] + top_performers: FlowQualityRow[] + needs_attention: FlowQualityRow[] +} + +// Phase 5 — Enhanced PSA +export interface PsaFunnel { + total_sessions: number + linked_to_ticket: number + doc_pushed: number + time_entry_logged: number +} + +export interface PsaDailyTrend { + date: string + entries: number + hours: number +} + +export interface EnhancedPsaMetrics { + total_time_entries: number + total_hours_logged: number + avg_hours_per_session: number + push_funnel: PsaFunnel + daily_trend: PsaDailyTrend[] +}