• Console log viewer with filtering, search, and export functionality. Displays captured logs with auto-scrolling and level-based filtering.

    Returns ReactElement

    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>
    );
    }