"use client"; import React, { useState, useRef, useEffect } from "react"; import { createPortal } from "react-dom"; import { Activity, X, Server, MessageSquare, Wrench, Layers, Clock, Zap, CheckCircle2, XCircle, Loader2, DollarSign, ChevronDown, ChevronRight, } from "lucide-react"; import { useObservability } from "@/hooks/useObservability"; import { Message } from "@/hooks/useConversation"; interface ObservabilityPanelProps { isOpen?: boolean; onToggle?: () => void; messages: Message[]; isSessionActive: boolean; isConnected: boolean; } export function ObservabilityPanel({ isOpen, onToggle, messages, isSessionActive, isConnected, }: ObservabilityPanelProps) { const data = useObservability({ messages, isSessionActive }); if (!isOpen) { return ( ); } return ( <> {createPortal(
{ if (e.target === e.currentTarget) onToggle?.(); }} >
{/* Header — merged with status strip */}

System Monitor

Real-Time Observability

{/* Inline status indicators */}
{/* Connection */}
{isConnected ? "Connected" : "Disconnected"}
{/* Session uptime */}
{isSessionActive && data.sessionUptimeSeconds != null ? formatDuration(data.sessionUptimeSeconds) : "No session"}
{/* Events/sec */}
{data.eventsPerSecond.toFixed(1)} evt/s
{/* Content — 3-column bento grid */}
{/* ===== LEFT COLUMN: Messages + GenUI ===== */}
{/* Messages */}
} label="Messages" />
{/* GenUI Components */} {data.genuiComponentCount > 0 && (
} label="GenUI Components" />
{data.genuiComponentCount} rendered {data.genuiComponentNames.map((name) => ( {name} ))}
)}
{/* ===== MIDDLE COLUMN: Token Usage + Server ===== */}
{/* Token Usage */}
} label="Token Usage" />
{data.costMetrics && data.costMetrics.response_count > 0 && (
{data.costMetrics.response_count} responses {data.costMetrics.input_audio > 0 && ( 🎙 {formatTokenCount(data.costMetrics.input_audio)} audio in )} {data.costMetrics.output_audio > 0 && ( 🔊 {formatTokenCount(data.costMetrics.output_audio)} audio out )} {data.costMetrics.cache_hit_rate > 0 && ( 💾 {Math.round(data.costMetrics.cache_hit_rate * 100)}% cached )}
)}
{/* Server Stats */}
} label="Server" />
{/* ===== RIGHT COLUMN: Tool Calls ===== */}
} label="Tool Calls" />
{data.toolCalls.length === 0 ? (

No tool calls yet this session

) : ( data.toolCalls.map((tool) => ( )) )}
{/* ===== BOTTOM ROW: Conversation Log (full-width) ===== */}
{/* Footer */}
Dev

WS events: {data.totalWsEvents} this session

, document.body )} ); } // ---- Sub-components ---- function SectionLabel({ icon, label }: { icon: React.ReactNode; label: string }) { return (
{icon} {label}
); } function StatCard({ label, value, color, }: { label: string; value: string | number; color: string; }) { return (
{value}
{label}
); } function ToolCallRow({ tool }: { tool: { id: string; name: string; status: string; duration: number | null } }) { const statusIcon = tool.status === "completed" ? ( ) : tool.status === "error" ? ( ) : ( ); return (
{statusIcon} {tool.name}
{tool.duration != null && ( {tool.duration}ms )} {tool.status}
); } // ---- Conversation Log (Chat-style) ---- function relativeTime(timestamp: string): string { try { const now = Date.now(); const then = new Date(timestamp).getTime(); const diff = Math.max(0, Math.floor((now - then) / 1000)); if (diff < 5) return "just now"; if (diff < 60) return `${diff}s ago`; const mins = Math.floor(diff / 60); if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); return `${hrs}h ${mins % 60}m ago`; } catch { return ""; } } function fullTime(timestamp: string): string { try { return new Date(timestamp).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit", }); } catch { return ""; } } /** Friendly tool name: "log_entry" → "Log Entry" */ function friendlyToolName(name: string): string { return name .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()); } function ToolDetailBlock({ data, label }: { data: Record; label: string }) { const [expanded, setExpanded] = useState(false); const keys = Object.keys(data); const summary = keys.length <= 3 ? keys .map((k) => { const v = data[k]; const display = typeof v === "string" ? v.length > 40 ? v.slice(0, 40) + "…" : v : JSON.stringify(v); return `${k}: ${display}`; }) .join(", ") : `${keys.length} fields`; return (
{expanded && (
          {JSON.stringify(data, null, 2)}
        
)}
); } function ConversationLog({ messages }: { messages: Message[] }) { const [isOpen, setIsOpen] = useState(true); const scrollRef = useRef(null); // Re-render every 30s so relative timestamps update const [, setTick] = useState(0); useEffect(() => { const id = setInterval(() => setTick((t) => t + 1), 30_000); return () => clearInterval(id); }, []); // No auto-scroll needed — newest messages are at the top // Newest first const logMessages = messages.filter((m) => !m.isPartial).slice().reverse(); return (
{/* Header toggle */} {isOpen && (
{logMessages.length === 0 ? (

Start talking to see the conversation here

) : ( logMessages.map((msg) => { const isUser = msg.role === "user"; const isTool = msg.role === "tool" && msg.tool; const isSystem = msg.role === "system"; const isGenUI = msg.role === "assistant" && !!msg.component; // ── System event chip (centered) ── if (isSystem) { return (
{msg.content}
); } // ── Tool call badge (compact inline) ── if (isTool) { const toolStatus = msg.tool!.status; const statusEmoji = toolStatus === "completed" ? "✓" : toolStatus === "error" ? "✗" : "⋯"; const statusColor = toolStatus === "completed" ? "var(--color-success)" : toolStatus === "error" ? "var(--color-error)" : "var(--color-accent-cyan)"; return (
🛠 {friendlyToolName(msg.tool!.name)} {statusEmoji} {relativeTime(msg.timestamp)}
{/* Expandable args/result (hidden by default) */} {msg.tool!.args && Object.keys(msg.tool!.args).length > 0 && (
)} {msg.tool!.result && Object.keys(msg.tool!.result).length > 0 && (
)}
); } // ── GenUI badge (compact) ── if (isGenUI) { return (
🖥 {msg.component!.name} shown {relativeTime(msg.timestamp)}
{Object.keys(msg.component!.props).length > 0 && (
)}
); } // ── Chat bubble (user = left, assistant = right) ── return (
{/* Sender label */} {isUser ? "You" : "Reachy"} {/* Bubble */}
{msg.content || "(no text)"}
{/* Timestamp */} {relativeTime(msg.timestamp)}
); }) )}
)}
); } // ---- Helpers ---- function formatDuration(seconds: number): string { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); if (mins > 0) { return `${mins}m ${secs.toString().padStart(2, "0")}s`; } return `${secs}s`; } function formatTokenCount(count: number): string { if (count >= 1_000_000) { return `${(count / 1_000_000).toFixed(1)}M`; } if (count >= 10_000) { return `${(count / 1_000).toFixed(1)}k`; } return new Intl.NumberFormat().format(count); }