IntegraChat / frontend /components /tenant-heatmap.tsx
nothingworry's picture
feat: Add LLM rule explanations, real-time visualizations, and fix analytics permissions
adf80ee
raw
history blame
10.1 kB
"use client";
import { useEffect, useState } from "react";
import { useTenant } from "@/contexts/TenantContext";
type HeatmapData = {
hour: number;
day: number; // 0-6 (Sunday-Saturday)
count: number;
};
type ToolUsageTrend = {
tool: string;
count: number;
trend: "up" | "down" | "stable";
};
type TenantHeatmapProps = {
days?: number;
};
const API_BASE =
process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const HOURS = Array.from({ length: 24 }, (_, i) => i);
export function TenantHeatmap({ days = 7 }: TenantHeatmapProps) {
const { tenantId, role } = useTenant();
const [loading, setLoading] = useState(false);
const [heatmapData, setHeatmapData] = useState<HeatmapData[]>([]);
const [toolTrends, setToolTrends] = useState<ToolUsageTrend[]>([]);
const [maxCount, setMaxCount] = useState(1);
useEffect(() => {
async function fetchHeatmapData() {
if (!tenantId) return;
setLoading(true);
try {
// Fetch activity data
const activityRes = await fetch(
`${API_BASE}/analytics/activity?days=${days}`,
{
headers: {
"x-tenant-id": tenantId,
"x-user-role": role,
},
},
);
if (!activityRes.ok) {
throw new Error(`Activity endpoint returned ${activityRes.status}`);
}
const activityData = await activityRes.json();
// Fetch tool usage stats
const toolRes = await fetch(
`${API_BASE}/analytics/tool-usage?days=${days}`,
{
headers: {
"x-tenant-id": tenantId,
"x-user-role": role,
},
},
);
if (!toolRes.ok) {
throw new Error(`Tool usage endpoint returned ${toolRes.status}`);
}
const toolData = await toolRes.json();
// Process activity data into heatmap format
const heatmap: HeatmapData[] = [];
const activityByHour: Record<string, number> = {};
// Group activities by hour and day
if (activityData.activities && Array.isArray(activityData.activities)) {
activityData.activities.forEach((activity: any) => {
try {
const timestamp = new Date(activity.timestamp || activity.created_at);
// Check if date is valid
if (!isNaN(timestamp.getTime())) {
const hour = timestamp.getHours();
const day = timestamp.getDay();
const key = `${day}-${hour}`;
activityByHour[key] = (activityByHour[key] || 0) + 1;
}
} catch (e) {
// Skip invalid timestamps
console.warn("Invalid timestamp in activity:", activity);
}
});
}
// Build heatmap data
for (let day = 0; day < 7; day++) {
for (let hour = 0; hour < 24; hour++) {
const key = `${day}-${hour}`;
const count = activityByHour[key] || 0;
heatmap.push({ hour, day, count });
}
}
setHeatmapData(heatmap);
setMaxCount(Math.max(...heatmap.map((h) => h.count), 1));
// Process tool trends
const trends: ToolUsageTrend[] = [];
if (toolData.tool_usage) {
Object.entries(toolData.tool_usage).forEach(([tool, stats]: [string, any]) => {
trends.push({
tool,
count: stats.count || 0,
trend: "stable", // Could be calculated from historical data
});
});
}
// Sort by count descending
trends.sort((a, b) => b.count - a.count);
setToolTrends(trends.slice(0, 10)); // Top 10 tools
} catch (err) {
console.error("Failed to fetch heatmap data:", err);
// Set empty data on error to show empty state
setHeatmapData([]);
setToolTrends([]);
setMaxCount(1);
} finally {
setLoading(false);
}
}
fetchHeatmapData();
}, [tenantId, role, days]);
const getIntensity = (count: number) => {
if (maxCount === 0) return 0;
const ratio = count / maxCount;
if (ratio === 0) return 0;
if (ratio < 0.2) return 1;
if (ratio < 0.4) return 2;
if (ratio < 0.6) return 3;
if (ratio < 0.8) return 4;
return 5;
};
const getColorClass = (intensity: number) => {
const colors = [
"bg-slate-800/30 border-slate-700/30", // 0
"bg-cyan-500/20 border-cyan-500/30", // 1
"bg-cyan-500/40 border-cyan-500/50", // 2
"bg-cyan-500/60 border-cyan-500/70", // 3
"bg-cyan-500/80 border-cyan-500/90", // 4
"bg-cyan-500 border-cyan-400", // 5
];
return colors[intensity] || colors[0];
};
return (
<div className="space-y-6">
{/* Query Heatmap */}
<div className="rounded-2xl border border-white/10 bg-slate-950/40 p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Query Activity Heatmap</h3>
<span className="text-xs text-slate-400">Last {days} days</span>
</div>
{loading ? (
<div className="flex h-64 items-center justify-center">
<span className="text-slate-400">Loading heatmap data...</span>
</div>
) : (
<div className="overflow-x-auto">
<div className="inline-block min-w-full">
{/* Hour labels */}
<div className="mb-2 flex">
<div className="w-12 shrink-0" />
{HOURS.map((hour) => (
<div
key={hour}
className="flex-1 text-center text-[10px] text-slate-500"
>
{hour % 6 === 0 ? hour : ""}
</div>
))}
</div>
{/* Heatmap grid */}
<div className="space-y-1">
{DAYS.map((dayName, dayIdx) => {
const dayData = heatmapData.filter((d) => d.day === dayIdx);
return (
<div key={dayIdx} className="flex items-center gap-2">
<div className="w-12 shrink-0 text-xs text-slate-400">
{dayName}
</div>
<div className="flex flex-1 gap-0.5">
{HOURS.map((hour) => {
const data = dayData.find((d) => d.hour === hour);
const count = data?.count || 0;
const intensity = getIntensity(count);
return (
<div
key={hour}
className={`h-4 flex-1 rounded border transition-all hover:scale-110 ${getColorClass(
intensity,
)}`}
title={`${dayName} ${hour}:00 - ${count} queries`}
/>
);
})}
</div>
</div>
);
})}
</div>
{/* Legend */}
<div className="mt-4 flex items-center justify-center gap-4 text-xs text-slate-400">
<span>Less</span>
<div className="flex gap-0.5">
{[0, 1, 2, 3, 4, 5].map((intensity) => (
<div
key={intensity}
className={`h-3 w-3 rounded border ${getColorClass(intensity)}`}
/>
))}
</div>
<span>More</span>
</div>
</div>
</div>
)}
</div>
{/* Tool Usage Trends */}
<div className="rounded-2xl border border-white/10 bg-slate-950/40 p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Per-Tool Usage Trends</h3>
<span className="text-xs text-slate-400">Top 10 tools</span>
</div>
{loading ? (
<div className="flex h-32 items-center justify-center">
<span className="text-slate-400">Loading tool trends...</span>
</div>
) : toolTrends.length === 0 ? (
<div className="flex h-32 items-center justify-center">
<span className="text-slate-400">No tool usage data available</span>
</div>
) : (
<div className="space-y-3">
{toolTrends.map((trend, idx) => {
const maxToolCount = Math.max(...toolTrends.map((t) => t.count), 1);
const widthPercent = (trend.count / maxToolCount) * 100;
return (
<div key={idx} className="space-y-1">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<span className="font-medium text-white">{trend.tool}</span>
{trend.trend === "up" && (
<span className="text-xs text-emerald-400"></span>
)}
{trend.trend === "down" && (
<span className="text-xs text-red-400"></span>
)}
</div>
<span className="text-slate-300">{trend.count}</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-slate-800">
<div
className="h-full bg-gradient-to-r from-cyan-500 to-cyan-400 transition-all"
style={{ width: `${widthPercent}%` }}
/>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
}