import { useVirtualizer } from '@tanstack/react-virtual'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import styles from './LogViewer.module.css'; export interface LogViewerProps { content: string; scrollRef: React.RefObject; autoScroll?: boolean; ref?: React.Ref; } export interface LogViewerRef { push: (line: string) => void; // search: (query: string) => void; } const LINE_HEIGHT = 16; const REGEX_PATTERN = /\[((?:\S+ )?\d+:\d+:\d+(?:\.\d+)?)\] \[(.[^(\n\r/\u2028\u2029]*)\/(\w+)\]:? (?:\[(CHAT)\])?/; export function LogViewer({ content, scrollRef, autoScroll = false, ref, }: LogViewerProps) { const lines = useMemo>(() => content.split('\n'), [content]); const containerRef = useRef(null); // used to force re-render (and re-init of virtualizer hook) const [_lastUpdated, setLastUpdated] = useState(Date.now()); const virtualizer = useVirtualizer({ count: lines.length, estimateSize: () => LINE_HEIGHT, getScrollElement: () => scrollRef.current ?? null, paddingStart: 4, paddingEnd: 4, gap: 0, overscan: 8, scrollMargin: containerRef.current ? containerRef.current.offsetTop + 75 : 0, }); const scrollToEnd = useCallback(() => { requestAnimationFrame(() => scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })); }, [scrollRef]); const appendLine = useCallback((line: string) => { setLastUpdated(Date.now()); lines.push(line); if (autoScroll) scrollToEnd(); }, [autoScroll, lines, scrollToEnd]); useImperativeHandle(ref, () => ({ push: appendLine, scrollToEnd, }), [appendLine, scrollToEnd]); return (
{virtualizer.getVirtualItems().map(virtualRow => ( ))}
); } function LogLine({ line, style, }: { line: string; style: React.CSSProperties; }) { line = line.replace(/ยง./g, ''); // remove color codes const groups = line.match(REGEX_PATTERN); if (groups === null) return ( {line}
); const [matched, time, threadName, level, isChat] = groups as [string, string, string, string, string | undefined]; return ( [{time}] [{threadName}/{level}] {isChat ? ' [CHAT]' : ' '}{line.slice(matched.length)}
); }