Spaces:
Sleeping
Sleeping
| "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, userRole } = 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": userRole, | |
| }, | |
| }, | |
| ); | |
| 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": userRole, | |
| }, | |
| }, | |
| ); | |
| 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) { | |
| activityData.activities.forEach((activity: any) => { | |
| const timestamp = new Date(activity.timestamp || activity.created_at); | |
| const hour = timestamp.getHours(); | |
| const day = timestamp.getDay(); | |
| const key = `${day}-${hour}`; | |
| activityByHour[key] = (activityByHour[key] || 0) + 1; | |
| }); | |
| } | |
| // 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); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| fetchHeatmapData(); | |
| }, [tenantId, userRole, 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> | |
| ); | |
| } | |