feat: add message metadata viewer on hover

Small info button appears on hover of each message bubble.
Click to expand a panel showing raw message metadata (id, role,
timestamp, channel, sender info, etc.) from the gateway.
Useful for debugging and understanding message routing.
Collapsed by default, doesn't clutter the UI.

Closes feedback item #39
This commit is contained in:
Nicolas Varrot
2026-02-12 23:17:27 +00:00
parent 53a8655bb1
commit b4813f091a
5 changed files with 54 additions and 2 deletions

View File

@@ -10,7 +10,7 @@ import { CodeBlock } from './CodeBlock';
import { ToolCall } from './ToolCall';
import { ImageBlock } from './ImageBlock';
import { buildImageSrc } from '../lib/image';
import { Bot, User, Wrench, Copy, Check, RefreshCw, Zap } from 'lucide-react';
import { Bot, User, Wrench, Copy, Check, RefreshCw, Zap, Info } from 'lucide-react';
import { t, getLocale } from '../lib/i18n';
import { useLocale } from '../hooks/useLocale';
// ChevronDown, ChevronRight, Wrench still used by InternalOnlyMessage
@@ -240,6 +240,34 @@ function CopyButton({ text }: { text: string }) {
);
}
function MetadataViewer({ metadata }: { metadata?: Record<string, unknown> }) {
const [open, setOpen] = useState(false);
if (!metadata || Object.keys(metadata).length === 0) return null;
return (
<div className="relative inline-block">
<button
onClick={() => setOpen(o => !o)}
className="h-7 w-7 rounded-lg border border-white/8 bg-zinc-800/80 backdrop-blur-sm flex items-center justify-center text-zinc-400 hover:text-cyan-300 hover:border-cyan-400/30 transition-all opacity-0 group-hover:opacity-100"
title={t('message.metadata')}
aria-label={t('message.metadata')}
>
<Info size={13} />
</button>
{open && (
<div className="absolute bottom-9 left-0 z-50 w-72 max-h-64 overflow-auto rounded-xl border border-white/10 bg-zinc-900/95 backdrop-blur-md shadow-xl p-3 text-[11px] text-zinc-400 font-mono leading-relaxed custom-scrollbar">
{Object.entries(metadata).map(([k, v]) => (
<div key={k} className="flex gap-2 py-0.5">
<span className="text-cyan-400/70 shrink-0">{k}:</span>
<span className="text-zinc-300 break-all">{typeof v === 'object' ? JSON.stringify(v) : String(v)}</span>
</div>
))}
</div>
)}
</div>
);
}
/** Extract plain text from message blocks for clipboard copy */
function getPlainText(message: ChatMessageType): string {
if (message.blocks.length > 0) {
@@ -305,10 +333,13 @@ export function ChatMessageComponent({ message, onRetry }: { message: ChatMessag
? 'bg-gradient-to-b from-cyan-800/40 to-cyan-900/25 text-zinc-100 border border-cyan-400/30'
: 'bg-zinc-800/40 text-zinc-300 border border-white/8 shadow-[0_0_0_1px_rgba(255,255,255,0.03)]'
}`}>
{/* Copy button (assistant messages only) */}
{/* Action buttons */}
{!isUser && !message.isStreaming && getPlainText(message).trim() && (
<CopyButton text={getPlainText(message)} />
)}
<div className={`absolute top-2 ${isUser ? 'left-2' : 'right-10'} flex gap-1 opacity-0 group-hover:opacity-100 transition-all`}>
<MetadataViewer metadata={message.metadata} />
</div>
{/* Retry button (user messages only) */}
{isUser && onRetry && (
<button