File size: 4,268 Bytes
3f76ff4
 
ffac1ce
3f76ff4
 
 
ffac1ce
3f76ff4
 
 
 
 
 
 
 
 
 
 
 
837e3ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f76ff4
 
 
 
 
ffac1ce
 
 
 
 
 
 
837e3ac
ffac1ce
 
 
 
 
 
 
 
 
3f76ff4
 
 
 
 
 
 
 
 
 
 
ffac1ce
 
 
 
 
 
 
 
 
 
 
 
 
 
3f76ff4
 
 
ffac1ce
3f76ff4
 
ffac1ce
 
3f76ff4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffac1ce
 
 
3f76ff4
 
 
 
 
 
 
 
 
837e3ac
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"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>
  );
}