"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 */}
{/* 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 */}
{/* ===== 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 (
);
}
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);
}