diff --git a/frontend/src/components/analytics/FlowAnalyticsPanel.tsx b/frontend/src/components/analytics/FlowAnalyticsPanel.tsx new file mode 100644 index 00000000..7c5823ea --- /dev/null +++ b/frontend/src/components/analytics/FlowAnalyticsPanel.tsx @@ -0,0 +1,308 @@ +import { useState, useEffect } from 'react' +import { Loader2, Star } from 'lucide-react' +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts' +import { analyticsApi } from '@/api' +import { cn } from '@/lib/utils' +import type { FlowAnalyticsResponse, AnalyticsPeriod } from '@/types' + +const CHART_COLORS = { + resolved: '#34d399', + escalated: '#f87171', + workaround: '#fbbf24', + unresolved: '#94a3b8', +} + +const PERIOD_OPTIONS: { value: AnalyticsPeriod; label: string }[] = [ + { value: '7d', label: 'Last 7 days' }, + { value: '30d', label: 'Last 30 days' }, + { value: '90d', label: 'Last 90 days' }, +] + +interface FlowAnalyticsPanelProps { + treeId: string +} + +export function FlowAnalyticsPanel({ treeId }: FlowAnalyticsPanelProps) { + const [period, setPeriod] = useState('30d') + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + + useEffect(() => { + setLoading(true) + setError(false) + analyticsApi + .getFlowAnalytics(treeId, period) + .then(setData) + .catch(() => { + setError(true) + setData(null) + }) + .finally(() => setLoading(false)) + }, [treeId, period]) + + if (loading) { + return ( +
+ +
+ ) + } + + if (error || !data) { + return ( +
+ No analytics data available for this flow. +
+ ) + } + + const { summary, avg_csat, total_ratings, time_series, step_feedback, recent_comments } = data + + return ( +
+ {/* Period selector */} +
+

Flow Analytics

+ +
+ + {/* Summary stat cards */} +
+ + + + 0 ? `${total_ratings} rating${total_ratings !== 1 ? 's' : ''}` : undefined} + /> +
+ + {/* Area chart - Sessions over time */} + {time_series.length > 0 && ( +
+

Sessions Over Time

+ + + + { + const d = new Date(String(value)) + return `${d.getMonth() + 1}/${d.getDate()}` + }} + /> + + { + const d = new Date(String(value)) + return d.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }) + }} + /> + + + + + + + + {/* Chart legend */} +
+ {Object.entries(CHART_COLORS).map(([key, color]) => ( +
+
+ + {key} + +
+ ))} +
+
+ )} + + {/* Step Feedback Table with Dropoff Metrics */} + {step_feedback.length > 0 && ( +
+

Step Performance

+
+ + + + + + + + + + + {step_feedback.map((step) => ( + 0.2 && 'bg-red-400/5' + )} + > + + + + + + ))} + +
StepVisitsDropoffsDropoff Rate
+ {step.node_title} + + {step.visit_count} + + {step.dropoff_count} + 0.2 ? 'text-red-400' : 'text-muted-foreground' + )} + > + {(step.dropoff_rate * 100).toFixed(1)}% +
+
+
+ )} + + {/* Recent Comments (Anonymous) */} + {recent_comments.length > 0 && ( +
+

Recent Feedback

+
+ {recent_comments.map((item, i) => ( +
+
+ {[1, 2, 3, 4, 5].map((v) => ( + + ))} +
+
+ {item.comment && ( +

{item.comment}

+ )} +

+ {new Date(item.created_at).toLocaleDateString()} +

+
+
+ ))} +
+
+ )} +
+ ) +} + +function StatCard({ + label, + value, + subtitle, +}: { + label: string + value: string | number + subtitle?: string +}) { + return ( +
+

{label}

+

{value}

+ {subtitle && ( +

{subtitle}

+ )} +
+ ) +} diff --git a/frontend/src/pages/TreeEditorPage.tsx b/frontend/src/pages/TreeEditorPage.tsx index 7afd3b62..5a7f582b 100644 --- a/frontend/src/pages/TreeEditorPage.tsx +++ b/frontend/src/pages/TreeEditorPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useCallback } from 'react' import { useParams, useNavigate, useBlocker } from 'react-router-dom' import { useStore } from 'zustand' -import { Undo2, Redo2, Save, CheckCircle2, Monitor, FileText, Code2, LayoutList } from 'lucide-react' +import { Undo2, Redo2, Save, CheckCircle2, Monitor, FileText, Code2, LayoutList, BarChart3 } from 'lucide-react' import { getMonacoEditor } from '@/components/tree-editor/code-mode' import { treesApi } from '@/api/trees' import { treeMarkdownApi } from '@/api/treeMarkdown' @@ -13,6 +13,7 @@ import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts' import { usePermissions } from '@/hooks/usePermissions' import { cn, safeGetItem } from '@/lib/utils' import { toast } from '@/lib/toast' +import { FlowAnalyticsPanel } from '@/components/analytics/FlowAnalyticsPanel' export function TreeEditorPage() { const { id } = useParams<{ id: string }>() @@ -46,6 +47,7 @@ export function TreeEditorPage() { const [showDraftPrompt, setShowDraftPrompt] = useState(false) const [treeStatus, setTreeStatus] = useState('draft') + const [showAnalytics, setShowAnalytics] = useState(false) // Mobile detection const [isMobile, setIsMobile] = useState(false) @@ -538,6 +540,23 @@ export function TreeEditorPage() {
+ {/* Analytics toggle (only for existing trees) */} + {isEditMode && ( + + )} + {/* Validate */}
) }