gitdataai/src/components/room/RoomPerformanceMonitor.tsx
2026-04-15 09:08:09 +08:00

154 lines
5.2 KiB
TypeScript

import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
interface PerformanceStats {
totalMessages: number;
renderedMessages: number;
virtualizationEnabled: boolean;
}
interface RoomPerformanceMonitorProps {
messageCount: number;
renderedCount?: number;
}
const AUTO_CLOSE_DELAY = 5000; // auto-close after 5 seconds when stats are shown
export function RoomPerformanceMonitor({ messageCount, renderedCount }: RoomPerformanceMonitorProps) {
const [showStats, setShowStats] = useState(false);
const autoCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Auto-close after AUTO_CLOSE_DELAY when stats are visible
useEffect(() => {
if (!showStats) return;
autoCloseTimerRef.current = setTimeout(() => {
setShowStats(false);
}, AUTO_CLOSE_DELAY);
return () => {
if (autoCloseTimerRef.current) clearTimeout(autoCloseTimerRef.current);
};
}, [showStats]);
const stats = useMemo<PerformanceStats>(() => ({
totalMessages: messageCount,
renderedMessages: renderedCount ?? messageCount,
virtualizationEnabled: renderedCount !== undefined && renderedCount < messageCount,
}), [messageCount, renderedCount]);
// --- Drag state ---
const panelRef = useRef<HTMLDivElement>(null);
const draggingRef = useRef(false);
const dragStartRef = useRef({ x: 0, y: 0 });
const handleDragStart = useCallback((e: React.MouseEvent) => {
// Don't start drag if clicking the close button
if ((e.target as HTMLElement).closest('button')) return;
draggingRef.current = true;
dragStartRef.current = { x: e.clientX, y: e.clientY };
e.preventDefault();
}, []);
useEffect(() => {
const onMouseMove = (e: MouseEvent) => {
if (!draggingRef.current) return;
const panel = panelRef.current;
if (!panel) return;
const dx = e.clientX - dragStartRef.current.x;
const dy = e.clientY - dragStartRef.current.y;
const rect = panel.getBoundingClientRect();
panel.style.left = `${rect.left + dx}px`;
panel.style.top = `${rect.top + dy}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
dragStartRef.current = { x: e.clientX, y: e.clientY };
};
const onMouseUp = () => {
draggingRef.current = false;
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
return () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
}, []);
if (!showStats && import.meta.env.PROD) {
return (
<Button
variant="ghost"
size="sm"
className="fixed bottom-4 left-4 z-50 h-7 w-7 rounded-full p-0 opacity-50 hover:opacity-100"
onClick={() => setShowStats(true)}
title="Show performance stats"
>
📊
</Button>
);
}
return (
<div
ref={panelRef}
className="fixed z-50 rounded-lg border bg-background/95 p-3 shadow-lg backdrop-blur-sm select-none"
style={{ bottom: '1rem', left: '1rem' }}
>
{/* Draggable header */}
<div
className="mb-2 flex items-center justify-between cursor-grab active:cursor-grabbing select-none"
onMouseDown={handleDragStart}
>
<h4 className="text-xs font-semibold text-foreground">Performance Stats</h4>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0"
onClick={() => setShowStats(false)}
>
</Button>
</div>
<div className="space-y-1.5 text-xs">
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">Total messages:</span>
<Badge variant="secondary" className="font-mono">{stats.totalMessages}</Badge>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">Rendered:</span>
<Badge variant="secondary" className="font-mono">{stats.renderedMessages}</Badge>
</div>
{stats.virtualizationEnabled && (
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">Skipped:</span>
<Badge variant="outline" className="font-mono text-green-600">
{stats.totalMessages - stats.renderedMessages}
</Badge>
</div>
)}
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">Virtualization:</span>
<Badge
variant={stats.virtualizationEnabled ? 'default' : 'destructive'}
className="text-[10px]"
>
{stats.virtualizationEnabled ? '✓ Enabled' : '✗ Disabled'}
</Badge>
</div>
{stats.virtualizationEnabled && (
<div className="pt-1 border-t border-border">
<span className="text-[10px] text-green-600">
Rendering {((stats.renderedMessages / stats.totalMessages) * 100).toFixed(0)}% of messages
</span>
</div>
)}
</div>
</div>
);
}