| import { useState, useEffect, useRef } from "react"; |
| import { api } from "../api"; |
|
|
| interface Props { |
| |
| 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 { |
| |
| } |
| }, 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> |
| ); |
| } |
|
|