"use client"; import { Pause, Play, RefreshCw } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { extractDouyinRoomHints } from "@/lib/youtubeConfigUtils"; type TFn = (zh: string, en: string) => string; type StatusPayload = { process_status?: string; recent_log?: string; recent_error?: string; }; function fmtDuration(sec: number): string { if (sec < 60) return `${sec}s`; const m = Math.floor(sec / 60); const s = sec % 60; if (m < 60) return `${m}m ${s}s`; const h = Math.floor(m / 60); const mm = m % 60; return `${h}h ${mm}m`; } function statusLine(t: TFn, raw: string | undefined): string { const s = (raw || "unknown").toLowerCase(); if (s === "online" || s === "running") return t("运行中(PM2/进程在线)", "Running (process online)"); if (s === "stopped" || s === "stopping") return t("已停止", "Stopped"); if (s === "not_found" || s === "errored") return t("未运行或未找到", "Not running / not found"); if (s === "invalid") return t("无效进程名", "Invalid process"); return raw || "—"; } type Props = { t: TFn; currentProc: string; fetchJson: (path: string, init?: RequestInit) => Promise; /** 用于源站房间线索(与 Electron 一致) */ urlListText: string; /** YouTube 密钥尾码;非 YouTube 进程可传空并关 showKeyRow */ keySuffixUi: string; showKeyRow?: boolean; pollMs?: number; }; export function LiveProcessLiveLogCard({ t, currentProc, fetchJson, urlListText, keySuffixUi, showKeyRow = true, pollMs = 1000, }: Props) { const [processStatus, setProcessStatus] = useState(""); const [recentLog, setRecentLog] = useState(""); const [recentError, setRecentError] = useState(""); const [paused, setPaused] = useState(false); const [liveSince, setLiveSince] = useState(null); const [nowTick, setNowTick] = useState(0); const outRef = useRef(null); const errRef = useRef(null); const prevOnlineRef = useRef(false); const douyinHints = extractDouyinRoomHints(urlListText); const pull = useCallback(async () => { if (!currentProc) return; try { const data = await fetchJson( `/status?process=${encodeURIComponent(currentProc)}`, ); const ps = data.process_status || "unknown"; setProcessStatus(ps); setRecentLog(data.recent_log || ""); setRecentError(data.recent_error || ""); const online = /^(online|running)$/i.test(ps); if (online && !prevOnlineRef.current) { setLiveSince(Date.now()); } if (!online) { setLiveSince(null); } prevOnlineRef.current = online; } catch { setProcessStatus("error"); } }, [currentProc, fetchJson]); useEffect(() => { void pull(); }, [pull]); useEffect(() => { if (paused || !currentProc) return; const id = window.setInterval(() => void pull(), pollMs); return () => clearInterval(id); }, [paused, currentProc, pull, pollMs]); useEffect(() => { const onVis = () => { if (document.visibilityState === "visible") void pull(); }; document.addEventListener("visibilitychange", onVis); return () => document.removeEventListener("visibilitychange", onVis); }, [pull]); useEffect(() => { if (!liveSince) return; const id = window.setInterval(() => setNowTick((n) => n + 1), 1000); return () => clearInterval(id); }, [liveSince]); useEffect(() => { if (paused) return; const el = outRef.current; if (el) el.scrollTop = el.scrollHeight; }, [recentLog, paused]); useEffect(() => { if (paused) return; const el = errRef.current; if (el) el.scrollTop = el.scrollHeight; }, [recentError, paused]); const durationLabel = liveSince == null ? t("—(未检测到在线)", "— (not online)") : fmtDuration(Math.max(0, Math.floor((Date.now() - liveSince) / 1000))); return (
{nowTick}

{t("当前直播日志", "Live process log")}

{paused ? t("已暂停刷新", "Paused") : t("每秒刷新", "~1s refresh")}
{t("进程状态", "Status")} {statusLine(t, processStatus)}
{t("本次在线时长(估算)", "Online duration (est.)")} {durationLabel}
{showKeyRow ? (
{t("YouTube key 尾码(参考)", "YT key suffix")} {keySuffixUi || "—"}
) : null} {douyinHints.length > 0 ? (
{t("源站房间线索", "Douyin room hints")} {douyinHints.map((id, i) => ( {id} ))}
) : (

{t("(地址列表中未解析到抖音房间线索)", "(No Douyin room id in URL list)")}

)}
{processStatus !== "not_found" && processStatus !== "invalid" ? (
{t("进程输出(stdout / 合并日志)", "Process output")}
              {recentLog.trim() || t("(暂无)", "(empty)")}
            
{t("异常信息(stderr)", "Stderr")}
              {recentError.trim() || t("(暂无)", "(empty)")}
            
) : (

{t("进程未在 PM2 / 本地注册表中运行,日志区域已隐藏。", "Process not running — log panes hidden.")}

)}
); }