JSX element rendering the log viewer panel
export function LogViewer(): React.ReactElement {
const { logEntries, maxLogEntries } = useDebugState();
const { clearLogs, exportLogs } = useDebugActions();
const [searchTerm, setSearchTerm] = useState("");
const [levelFilters, setLevelFilters] = useState(DEFAULT_LEVEL_FILTER);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
const scrollRootRef = useRef<HTMLDivElement | null>(null);
const filteredEntries = useMemo(() => {
return logEntries.filter(
(entry) =>
matchesEntry(entry, levelFilters) && includesQuery(entry, searchTerm),
);
}, [logEntries, levelFilters, searchTerm]);
useEffect(() => {
if (!shouldAutoScroll) return;
// Scroll to the bottom of the viewport when new entries are added
const viewport = scrollRootRef.current?.querySelector<HTMLDivElement>(
'[data-slot="scroll-area-viewport"]',
);
if (viewport) {
viewport.scrollTop = viewport.scrollHeight;
}
}, [filteredEntries, shouldAutoScroll]);
const toggleLevel = (level: VisibleLogLevel) => {
setLevelFilters((prev) => ({
...prev,
[level]: !prev[level],
}));
};
const totalEntries = logEntries.length;
const handleClear = () => {
if (!totalEntries) return;
clearLogs();
toast.success("Captured logs cleared");
};
const handleExport = () => {
if (!totalEntries) return;
exportLogs();
toast.success("Debug logs exported");
};
const handleCopyEntry = async (entry: LogEntry) => {
try {
const [serialized] = serializeLogEntries([entry]);
await navigator.clipboard.writeText(JSON.stringify(serialized, null, 2));
toast.success("Log entry copied to clipboard");
} catch (error) {
toast.error("Unable to copy log entry");
console.error("Failed to copy log entry", error);
}
};
return (
<div className="flex h-full flex-col gap-4">
<div className="flex flex-col gap-4 lg:items-center lg:justify-between">
<div className="space-y-2 lg:max-w-3xl">
<h2 className="text-lg font-semibold">Console log viewer</h2>
<p className="text-muted-foreground text-sm">
Inspect captured logs when developer tools are unavailable. The most
recent {maxLogEntries} entries are retained.
</p>
<div className="flex flex-wrap items-center gap-3 text-xs">
<Badge variant="outline" className="border-border/60 bg-muted/40">
{filteredEntries.length} showing
</Badge>
<Badge variant="outline" className="border-border/60 bg-muted/40">
{totalEntries} captured
</Badge>
</div>
</div>
<div className="flex w-full flex-col gap-3">
<div className="relative w-full">
<Search className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
<Input
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
placeholder="Search message, details, or source"
className="pl-9"
/>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<div className="text-muted-foreground flex items-center gap-2">
<Switch
id="debug-log-autoscroll"
checked={shouldAutoScroll}
onCheckedChange={(checked) =>
setShouldAutoScroll(Boolean(checked))
}
/>
<label htmlFor="debug-log-autoscroll" className="cursor-pointer">
Auto-scroll to newest
</label>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={handleClear}
disabled={!totalEntries}
>
<Trash2 className="mr-2 h-4 w-4" />
Clear
</Button>
<Button
variant="outline"
onClick={handleExport}
disabled={!totalEntries}
>
<Download className="mr-2 h-4 w-4" />
Export JSON
</Button>
</div>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
{FILTERABLE_LEVELS.map((level) => {
const active = levelFilters[level];
const meta = LEVEL_META[level];
return (
<Button
key={level}
type="button"
variant={active ? "default" : "outline"}
className={cn(
"flex items-center gap-2 rounded-full px-3 py-1 text-xs",
!active && "bg-transparent",
)}
onClick={() => toggleLevel(level)}
>
<span className={cn(meta.tone, "flex items-center gap-1")}>
{meta.icon}
</span>
{meta.label}
</Button>
);
})}
</div>
<Separator />
<LogEntriesContainer
scrollRootRef={scrollRootRef}
filteredEntries={filteredEntries}
searchTerm={searchTerm}
onCopyEntry={handleCopyEntry}
/>
</div>
);
}
Console log viewer with filtering, search, and export functionality. Displays captured logs with auto-scrolling and level-based filtering.