import { LOG_LEVELS, MARKER_EVENTS, type LogEntry, type LogLevel, type LogMarker, } from "@web-video-editor/observability"; import { useMemo, useState, useSyncExternalStore } from "react"; import "./LogPanel.css"; import { DEFAULT_LOG_FILTERS, copyLogEntries, entriesToJsonl, exportLogEntries, filterLogEntries, type LogPanelFilters, } from "./logPanelModel"; export type LogPanelSource = { clear(): void; getEntries(): readonly LogEntry[]; subscribe(listener: (entry?: LogEntry) => void): () => void; }; export type LogPanelProps = { className?: string; source: LogPanelSource; title?: string; }; function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } export function LogPanel({ className = "", source, title = "结构化日志", }: LogPanelProps) { const [filters, setFilters] = useState(DEFAULT_LOG_FILTERS); const [status, setStatus] = useState(""); const entries = useSyncExternalStore( (onStoreChange) => source.subscribe(onStoreChange), () => source.getEntries(), () => source.getEntries(), ); const filteredEntries = useMemo( () => filterLogEntries(entries, filters), [entries, filters], ); const updateFilter = ( key: K, value: LogPanelFilters[K], ) => { setFilters((current) => ({ ...current, [key]: value })); }; const copy = async () => { try { const count = await copyLogEntries(filteredEntries); setStatus(`已复制 ${count} 条 JSONL 日志`); } catch (error) { setStatus(`复制失败:${errorMessage(error)}`); } }; const clear = () => { source.clear(); setStatus("日志已清空"); }; const exportJsonl = () => { try { const fileName = exportLogEntries(filteredEntries); setStatus(`已导出 ${fileName}`); } catch (error) { setStatus(`导出失败:${errorMessage(error)}`); } }; const markerOptions = Object.keys(MARKER_EVENTS) as LogMarker[]; return (

{title}

显示 {filteredEntries.length} / {entries.length} 条

{status}

    {filteredEntries.map((entry, index) => (
  1. {entry.marker} {entry.scope} {entry.event} {entry.requestId && {entry.requestId}}
    JSON
    {JSON.stringify(entry, null, 2)}
  2. ))}