esfiles / frontend /src /components /LogViewer.tsx
Besjon Cifliku
feat: upadte logging in hf spaces
e29b232
import { useState, useEffect, useRef } from "react";
import { api } from "../api";
interface Props {
/** Whether to actively poll for logs */
active: boolean;
}
export default function LogViewer({ active }: Props) {
const [lines, setLines] = useState<string[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const cursorRef = useRef(0);
useEffect(() => {
if (!active) return;
setLines([]);
cursorRef.current = 0;
const interval = setInterval(async () => {
try {
const res = await api.pollLogs(cursorRef.current);
if (res.lines.length > 0) {
setLines((prev) => {
const next = [...prev, ...res.lines];
return next.length > 200 ? next.slice(-200) : next;
});
}
cursorRef.current = res.cursor;
} catch {
// ignore polling errors
}
}, 800);
return () => clearInterval(interval);
}, [active]);
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
}, [lines]);
if (!active && lines.length === 0) return null;
return (
<div
ref={containerRef}
style={{
background: "#0a0c10",
border: "1px solid var(--border)",
borderRadius: "var(--radius)",
padding: "10px 14px",
marginTop: 12,
maxHeight: 220,
overflowY: "auto",
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
fontSize: "0.75rem",
lineHeight: 1.7,
color: "var(--text-dim)",
}}
>
{lines.length === 0 && active && (
<span style={{ color: "var(--text-dim)", opacity: 0.5 }}>Waiting for logs...</span>
)}
{lines.map((line, i) => (
<div key={i} style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
{line}
</div>
))}
</div>
);
}