Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

98 lines
3.3 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { ChevronDown, Loader2, Terminal } from "lucide-react";
import { useTranslation } from "react-i18next";
interface ProcessLogsProps {
logs: string[];
executing: boolean;
title?: string;
emptyMessage?: string;
}
export default function ProcessLogs({
logs,
executing,
title,
emptyMessage = "Waiting for output...",
}: ProcessLogsProps) {
const { t } = useTranslation();
const resolvedTitle = title ?? t("Process Logs");
const [open, setOpen] = useState(true);
const logContainerRef = useRef<HTMLDivElement>(null);
const stickToBottomRef = useRef(true);
useEffect(() => {
if (executing && logs.length === 0) {
stickToBottomRef.current = true;
}
}, [executing, logs.length]);
useEffect(() => {
const container = logContainerRef.current;
if (!container || !open || !logs.length) return;
if (!stickToBottomRef.current) return;
container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
}, [logs.length, open]);
if (!logs.length && !executing) return null;
return (
<details
open={open}
onToggle={(e) => setOpen((e.target as HTMLDetailsElement).open)}
className="group rounded-lg border border-[var(--border)] bg-[var(--card)]"
>
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-3 py-2 text-[13px] font-medium text-[var(--foreground)] transition-colors hover:bg-[var(--muted)]/50">
<span className="inline-flex items-center gap-1.5">
<Terminal size={13} strokeWidth={1.7} />
{resolvedTitle}
{logs.length > 0 && (
<span className="rounded-full bg-[var(--muted)] px-1.5 py-0.5 text-[10px] font-normal text-[var(--muted-foreground)]">
{logs.length}
</span>
)}
</span>
<div className="flex items-center gap-2">
{executing && (
<Loader2 size={12} className="animate-spin text-[var(--primary)]" />
)}
<ChevronDown
size={13}
className="text-[var(--muted-foreground)] transition-transform group-open:rotate-180"
/>
</div>
</summary>
<div className="border-t border-[var(--border)] bg-[var(--background)]">
<div
ref={logContainerRef}
onScroll={(e) => {
const container = e.currentTarget;
const distanceFromBottom =
container.scrollHeight -
container.scrollTop -
container.clientHeight;
stickToBottomRef.current = distanceFromBottom <= 24;
}}
className="max-h-[220px] overflow-y-auto px-3 py-2.5 font-mono text-[11px] leading-[1.7] text-[var(--muted-foreground)]"
>
{logs.map((line, i) => (
<div key={`log-${i}`} className="whitespace-pre-wrap break-all">
<span className="mr-2 select-none text-[var(--border)]">
{String(i + 1).padStart(3)}
</span>
{line}
</div>
))}
{executing && logs.length === 0 && (
<div className="text-[var(--muted-foreground)]/60">
{emptyMessage}
</div>
)}
</div>
</div>
</details>
);
}