Files
resolutionflow/frontend/src/components/analytics/CoverageHeatmap.tsx
Michael Chihlas 303a558432 refactor: replace hardcoded hex values with Tailwind semantic tokens
3,200+ hardcoded color values replaced with CSS variable-backed
Tailwind classes (bg-card, text-foreground, border-border, etc.).
Enables light mode via CSS variable swap. Only syntax highlighting
colors and intentional one-offs remain hardcoded (~15 values).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 04:34:35 -04:00

187 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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'
return 'bg-rose-500/10 text-rose-500'
}
export default function CoverageHeatmap({ data }: CoverageHeatmapProps) {
if (data.domains.length === 0) {
return (
<div className="card-flat p-6">
<div className="flex items-center gap-2 mb-2">
<MapPin size={16} className="text-foreground" />
<h3 className="font-heading text-sm font-semibold text-foreground">Domain Coverage</h3>
</div>
<p className="text-sm text-muted-foreground">
No session data for this period. Start using FlowPilot to see coverage metrics.
</p>
</div>
)
}
return (
<div className="card-flat p-3 sm:p-5">
<div className="mb-4">
<div className="flex items-center gap-2 mb-1">
<MapPin size={16} className="text-foreground" />
<h3 className="font-heading text-sm font-semibold text-foreground">Domain Coverage</h3>
</div>
<p className="text-xs text-muted-foreground">
Resolution coverage and knowledge gaps by problem domain
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border">
<th className="px-3 py-2 text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
Domain
</th>
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
Flows
</th>
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
Sessions
</th>
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
Resolution %
</th>
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
Escalation %
</th>
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
Guided %
</th>
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground"
title="Average time to resolve sessions in this domain. Green: &lt;10 min, Amber: 1020 min, Red: &gt;20 min. Lower is better.">
Avg Resolution
</th>
</tr>
</thead>
<tbody>
{data.domains.map((row) => (
<tr key={row.domain} className="border-b border-border">
<td className="px-3 py-2 text-sm text-foreground font-medium">
{row.domain}
</td>
<td className="px-3 py-2 text-sm text-right">
{row.flow_count === 0 ? (
<Link
to="/trees/new"
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
>
<Plus size={10} />
Create Flow
</Link>
) : (
<span
className={cn('inline-block rounded px-1.5 py-0.5 text-xs font-medium', getFlowCountStyle(row.flow_count))}
title={`${row.flow_count} flow${row.flow_count !== 1 ? 's' : ''}${row.flow_count >= 5 ? 'Good' : row.flow_count >= 1 ? 'Needs Improvement' : 'Critical'}`}
>
{row.flow_count}
</span>
)}
</td>
<td className="px-3 py-2 text-sm text-right text-muted-foreground">
{row.session_count}
</td>
<td className="px-3 py-2 text-sm text-right">
<span
className={cn('inline-block rounded px-1.5 py-0.5 text-xs font-medium', getCellStyle(row.resolution_rate, { green: 0.75, amber: 0.5 }))}
title={getCellTitle(row.resolution_rate, 'Resolution', { green: 0.75, amber: 0.5 })}
>
{(row.resolution_rate * 100).toFixed(1)}%
</span>
</td>
<td className="px-3 py-2 text-sm text-right">
<span
className={cn('inline-block rounded px-1.5 py-0.5 text-xs font-medium', getCellStyle(row.escalation_rate, { green: 0.10, amber: 0.25 }, true))}
title={getCellTitle(row.escalation_rate, 'Escalation', { green: 0.10, amber: 0.25 }, true)}
>
{(row.escalation_rate * 100).toFixed(1)}%
</span>
</td>
<td className="px-3 py-2 text-sm text-right">
<span
className={cn('inline-block rounded px-1.5 py-0.5 text-xs font-medium', getCellStyle(row.guided_rate, { green: 0.60, amber: 0.30 }))}
title={getCellTitle(row.guided_rate, 'Guided', { green: 0.60, amber: 0.30 })}
>
{(row.guided_rate * 100).toFixed(1)}%
</span>
</td>
<td className="px-3 py-2 text-sm text-right">
{row.avg_resolution_minutes == null ? (
<span className="text-muted-foreground"></span>
) : (
<span
className={cn(
'inline-block rounded px-1.5 py-0.5 text-xs font-medium',
row.avg_resolution_minutes < 10
? 'bg-emerald-400/10 text-emerald-400'
: row.avg_resolution_minutes <= 20
? 'bg-amber-400/10 text-amber-400'
: 'bg-rose-500/10 text-rose-500',
)}
title={`Avg resolution: ${row.avg_resolution_minutes.toFixed(1)} min — ${row.avg_resolution_minutes < 10 ? 'Good (<10 min)' : row.avg_resolution_minutes <= 20 ? 'Needs Improvement (1020 min)' : 'Slow (>20 min)'}`}
>
{row.avg_resolution_minutes.toFixed(1)} min
</span>
)}
</td>
</tr>
))}
</tbody>
{data.unmapped_session_count > 0 && (
<tfoot>
<tr>
<td colSpan={7} className="px-3 py-2 text-xs text-muted-foreground">
{data.unmapped_session_count} sessions had no domain classification
</td>
</tr>
</tfoot>
)}
</table>
</div>
<div className="flex flex-wrap gap-4 mt-3 text-[0.625rem] font-sans text-xs text-muted-foreground">
<span><span className="inline-block w-2 h-2 rounded-full bg-emerald-400 mr-1" />Good</span>
<span><span className="inline-block w-2 h-2 rounded-full bg-amber-400 mr-1" />Needs Improvement</span>
<span><span className="inline-block w-2 h-2 rounded-full bg-rose-500 mr-1" />Critical</span>
</div>
</div>
)
}