Files
resolutionflow/frontend/src/components/analytics/FlowAnalyticsPanel.tsx
chihlasm ff985fb755 refactor: replace cyan accent with ember orange across entire frontend
Swap accent color from cyan (#22d3ee) to ember orange (#f97316) site-wide.
Cyan caused contrast issues and felt generic — orange brings warmth and
urgency fitting for a troubleshooting tool.

Changes:
- CSS variables: accent, accent-hover, accent-dim, accent-text, primary, ring
- Warning color shifted from amber (#fbbf24) to yellow (#eab308) for
  semantic separation from orange accent
- Brand SVGs: logo gradient updated to orange
- 50+ component files: all hardcoded cyan hex values, Tailwind cyan-*
  classes, and rgba(34,211,238,...) glow values replaced
- landing.css: all 45+ cyan references + 5 old border color fixes
- DESIGN-SYSTEM.md bumped to v5 with decisions log
- CLAUDE.md: all color references synced to charcoal palette + orange accent
- PWA theme-color meta tag updated to match sidebar (#10121a)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 07:37:44 +00:00

311 lines
10 KiB
TypeScript

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: '#eab308',
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<AnalyticsPeriod>('30d')
const [data, setData] = useState<FlowAnalyticsResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setLoading(true)
// eslint-disable-next-line react-hooks/set-state-in-effect
setError(false)
analyticsApi
.getFlowAnalytics(treeId, period)
.then(setData)
.catch(() => {
setError(true)
setData(null)
})
.finally(() => setLoading(false))
}, [treeId, period])
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)
}
if (error || !data) {
return (
<div className="text-center py-12 text-muted-foreground">
No analytics data available for this flow.
</div>
)
}
const { summary, avg_csat, total_ratings, time_series, step_feedback, recent_comments } = data
return (
<div className="space-y-6">
{/* Period selector */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-foreground">Flow Analytics</h3>
<select
value={period}
onChange={(e) => setPeriod(e.target.value as AnalyticsPeriod)}
className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:outline-hidden focus:ring-1 focus:ring-ring"
>
{PERIOD_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{/* Summary stat cards */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard label="Sessions" value={summary.total_sessions.toLocaleString()} />
<StatCard
label="Completion"
value={`${(summary.completion_rate * 100).toFixed(0)}%`}
/>
<StatCard
label="Median Time"
value={`${summary.median_duration_minutes} min`}
/>
<StatCard
label="CSAT"
value={avg_csat != null ? `${avg_csat.toFixed(1)}/5` : 'N/A'}
subtitle={total_ratings > 0 ? `${total_ratings} rating${total_ratings !== 1 ? 's' : ''}` : undefined}
/>
</div>
{/* Area chart - Sessions over time */}
{time_series.length > 0 && (
<div className="bg-card border border-border rounded-xl p-4">
<p className="text-sm font-semibold text-foreground mb-3">Sessions Over Time</p>
<ResponsiveContainer width="100%" height={180}>
<AreaChart data={time_series}>
<CartesianGrid
strokeDasharray="3 3"
stroke="var(--color-border)"
vertical={false}
/>
<XAxis
dataKey="date"
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 11 }}
tickLine={false}
axisLine={{ stroke: 'var(--color-border)' }}
tickFormatter={(value) => {
const d = new Date(String(value))
return `${d.getMonth() + 1}/${d.getDate()}`
}}
/>
<YAxis
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 11 }}
tickLine={false}
axisLine={false}
allowDecimals={false}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--color-card)',
border: '1px solid var(--color-border)',
borderRadius: '8px',
color: 'var(--color-foreground)',
fontSize: '13px',
}}
labelFormatter={(value) => {
const d = new Date(String(value))
return d.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
})
}}
/>
<Area
type="monotone"
dataKey="resolved"
stackId="1"
stroke={CHART_COLORS.resolved}
fill={CHART_COLORS.resolved}
fillOpacity={0.3}
/>
<Area
type="monotone"
dataKey="escalated"
stackId="1"
stroke={CHART_COLORS.escalated}
fill={CHART_COLORS.escalated}
fillOpacity={0.3}
/>
<Area
type="monotone"
dataKey="workaround"
stackId="1"
stroke={CHART_COLORS.workaround}
fill={CHART_COLORS.workaround}
fillOpacity={0.3}
/>
<Area
type="monotone"
dataKey="unresolved"
stackId="1"
stroke={CHART_COLORS.unresolved}
fill={CHART_COLORS.unresolved}
fillOpacity={0.3}
/>
</AreaChart>
</ResponsiveContainer>
{/* Chart legend */}
<div className="flex items-center gap-4 mt-2 justify-center">
{Object.entries(CHART_COLORS).map(([key, color]) => (
<div key={key} className="flex items-center gap-1.5">
<div
className="h-2 w-2 rounded-full"
style={{ backgroundColor: color }}
/>
<span className="text-xs text-muted-foreground capitalize">
{key}
</span>
</div>
))}
</div>
</div>
)}
{/* Step Feedback Table with Dropoff Metrics */}
{step_feedback.length > 0 && (
<div className="bg-card border border-border rounded-xl p-4">
<p className="text-sm font-semibold text-foreground mb-3">Step Performance</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 pr-4 text-foreground font-medium">Step</th>
<th className="text-right py-2 pr-4 text-foreground font-medium">Visits</th>
<th className="text-right py-2 pr-4 text-foreground font-medium">Dropoffs</th>
<th className="text-right py-2 text-foreground font-medium">Dropoff Rate</th>
</tr>
</thead>
<tbody>
{step_feedback.map((step) => (
<tr
key={step.node_id}
className={cn(
'border-b border-border last:border-0',
step.dropoff_rate > 0.2 && 'bg-red-400/5'
)}
>
<td className="py-2 pr-4 text-muted-foreground truncate max-w-[200px]">
{step.node_title}
</td>
<td className="py-2 pr-4 text-right text-muted-foreground">
{step.visit_count}
</td>
<td className="py-2 pr-4 text-right text-muted-foreground">
{step.dropoff_count}
</td>
<td
className={cn(
'py-2 text-right font-medium',
step.dropoff_rate > 0.2 ? 'text-red-400' : 'text-muted-foreground'
)}
>
{(step.dropoff_rate * 100).toFixed(1)}%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Recent Comments (Anonymous) */}
{recent_comments.length > 0 && (
<div className="bg-card border border-border rounded-xl p-4">
<p className="text-sm font-semibold text-foreground mb-3">Recent Feedback</p>
<div className="space-y-3">
{recent_comments.map((item, i) => (
<div
key={i}
className="flex items-start gap-3 border-b border-border/50 pb-3 last:border-0 last:pb-0"
>
<div className="flex items-center gap-0.5 shrink-0 pt-0.5">
{[1, 2, 3, 4, 5].map((v) => (
<Star
key={v}
size={12}
className={cn(
v <= item.rating
? 'fill-yellow-400 text-yellow-400'
: 'fill-none text-muted-foreground'
)}
/>
))}
</div>
<div className="min-w-0">
{item.comment && (
<p className="text-sm text-foreground">{item.comment}</p>
)}
<p className="text-xs text-muted-foreground mt-0.5">
{new Date(item.created_at).toLocaleDateString()}
</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
function StatCard({
label,
value,
subtitle,
}: {
label: string
value: string | number
subtitle?: string
}) {
return (
<div className="bg-card border border-border rounded-xl p-4">
<p className="text-xs text-muted-foreground">{label}</p>
<p className="text-xl font-bold text-foreground mt-1">{value}</p>
{subtitle && (
<p className="text-xs text-muted-foreground mt-0.5">{subtitle}</p>
)}
</div>
)
}