Spaces:
Running
Running
| "use client"; | |
| import { useCallback, useEffect, useRef } from "react"; | |
| import type { AgentLogEntry } from "@/lib/work-package-types"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Download } from "lucide-react"; | |
| function levelTone(level: AgentLogEntry["level"]) { | |
| if (level === "success") return "text-emerald-700"; | |
| if (level === "warn") return "text-amber-700"; | |
| if (level === "error") return "text-rose-700"; | |
| return "text-foreground"; | |
| } | |
| function formatLogTime(value: string) { | |
| return value.slice(11, 19); | |
| } | |
| export function buildAgentLogExportText(args: { | |
| logs: AgentLogEntry[]; | |
| busy: boolean; | |
| }) { | |
| const { logs, busy } = args; | |
| const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); | |
| const header = [ | |
| `Agent Log Export — ${timestamp}`, | |
| `Total entries: ${logs.length}`, | |
| `Status: ${busy ? "Running" : "Idle"}`, | |
| "", | |
| ]; | |
| const body = logs.map( | |
| (log) => | |
| `[${formatLogTime(log.createdAt)}] ${log.level.toUpperCase()} ${log.title}\n${log.detail}\n`, | |
| ); | |
| return [...header, ...body].join("\n"); | |
| } | |
| export function AgentLogPanel(props: { | |
| logs: AgentLogEntry[]; | |
| busy: boolean; | |
| }) { | |
| const { logs, busy } = props; | |
| const bottomRef = useRef<HTMLDivElement | null>(null); | |
| useEffect(() => { | |
| bottomRef.current?.scrollIntoView({ block: "end" }); | |
| }, [logs.length]); | |
| const exportLogs = useCallback(() => { | |
| const content = buildAgentLogExportText({ logs, busy }); | |
| const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); | |
| const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = `agent-log-${timestamp}.txt`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }, [logs, busy]); | |
| return ( | |
| <div className="flex h-full min-h-0 flex-col rounded-2xl bg-muted/24 px-1"> | |
| <div className="px-3 py-2 md:px-4"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <div> | |
| <div className="text-sm font-semibold">Agent Log</div> | |
| <div className="text-xs text-muted-foreground"> | |
| Background activity, command routing, and board updates. | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <Badge variant={busy ? "secondary" : "outline"} className="text-[11px]"> | |
| {busy ? "Running" : "Idle"} | |
| </Badge> | |
| <button | |
| type="button" | |
| onClick={exportLogs} | |
| className="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:border-primary/30 hover:text-foreground" | |
| title="Export full log as .txt" | |
| > | |
| <Download className="h-3 w-3" /> | |
| Export | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <ScrollArea className="flex-1 min-h-0"> | |
| <div className="px-3 py-1 font-mono text-[11px] leading-5 md:px-4"> | |
| {logs.length ? ( | |
| <> | |
| {logs.map((log) => ( | |
| <div key={log.id} className="py-1 last:border-b-0"> | |
| <div className="flex items-start justify-between gap-2"> | |
| <div className={["min-w-0", levelTone(log.level)].join(" ")}> | |
| <div className="font-semibold">{log.title}</div> | |
| <div className="mt-0.5 whitespace-pre-wrap break-words text-muted-foreground/90"> | |
| {log.detail} | |
| </div> | |
| </div> | |
| <div | |
| className="shrink-0 text-[10px] text-muted-foreground" | |
| suppressHydrationWarning | |
| > | |
| {formatLogTime(log.createdAt)} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| <div ref={bottomRef} /> | |
| </> | |
| ) : ( | |
| <div className="text-muted-foreground"> | |
| No agent activity yet. | |
| </div> | |
| )} | |
| </div> | |
| </ScrollArea> | |
| </div> | |
| ); | |
| } | |