File size: 1,914 Bytes
db764ae e29b232 db764ae e29b232 db764ae e29b232 db764ae e29b232 db764ae e29b232 db764ae e29b232 db764ae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 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>
);
}
|