fix: metadata viewer popup clipped by overflow-hidden parent

The MetadataViewer popup was rendered inside the message bubble which has
overflow-hidden, causing the popup to be invisible. Fix by using
createPortal to render the popup directly on document.body with fixed
positioning. Also adds click-outside-to-close behavior.

Closes feedback #46
This commit is contained in:
Nicolas Varrot
2026-02-12 23:22:05 +00:00
parent d03a02351f
commit 9f67c9e5dc
2 changed files with 37 additions and 5 deletions

View File

@@ -491,3 +491,12 @@
- Check the gateway WebSocket session/handshake data for avatar info
- Fallback to the current default icon if no avatar is configured
- Should also appear in the header next to the agent name
## Item #46
- **Date:** 2026-02-12
- **Priority:** high
- **Status:** in-progress
- **Description:** Bug: metadata viewer ( button) doesn't work
- Clicking the info button on messages does nothing — no panel appears
- Introduced in v1.15.0 (commit `b4813f0`)
- Fix the click handler / panel display logic

View File

@@ -1,5 +1,6 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
@@ -242,11 +243,32 @@ function CopyButton({ text }: { text: string }) {
function MetadataViewer({ metadata }: { metadata?: Record<string, unknown> }) {
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
useEffect(() => {
if (!open) return;
const btn = btnRef.current;
if (btn) {
const r = btn.getBoundingClientRect();
setPos({ top: r.top - 4, left: r.left });
}
const handleClick = (e: MouseEvent) => {
if (panelRef.current && !panelRef.current.contains(e.target as Node) && !btnRef.current?.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [open]);
if (!metadata || Object.keys(metadata).length === 0) return null;
return (
<div className="relative inline-block">
<button
ref={btnRef}
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')}
@@ -254,15 +276,16 @@ function MetadataViewer({ metadata }: { metadata?: Record<string, unknown> }) {
>
<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">
{open && pos && createPortal(
<div ref={panelRef} className="fixed z-[9999] 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" style={{ top: pos.top, left: pos.left, transform: 'translateY(-100%)' }}>
{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>,
document.body
)}
</div>
);
@@ -337,7 +360,7 @@ export function ChatMessageComponent({ message, onRetry }: { message: ChatMessag
{!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`}>
<div className={`absolute top-2 ${isUser ? 'left-2' : 'right-10'} flex gap-1 opacity-0 group-hover:opacity-100 transition-all z-10`}>
<MetadataViewer metadata={message.metadata} />
</div>
{/* Retry button (user messages only) */}