| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- "use client";
- import Link from "next/link";
- import { startTransition, useEffect, useState } from "react";
- import { AgentAvatar } from "@/components/agents/agent-avatar";
- import { agentStatusFilters } from "@/data/demo-agents";
- import { AgentFeed, AgentStatus } from "@/types/agent";
- const statusClassMap = {
- working: "status-pill status-pill--working",
- idle: "status-pill status-pill--idle",
- warning: "status-pill status-pill--warning",
- offline: "status-pill status-pill--offline"
- } as const;
- type AgentsDashboardProps = {
- initialFeed: AgentFeed;
- initialStatus: "all" | AgentStatus;
- };
- function isAgentStatus(value: string): value is AgentStatus {
- return value === "working" || value === "idle" || value === "warning" || value === "offline";
- }
- function formatTimeLabel(value: string) {
- return new Date(value).toLocaleTimeString("zh-CN", {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
- }
- export function AgentsDashboard({ initialFeed, initialStatus }: AgentsDashboardProps) {
- const [feed, setFeed] = useState(initialFeed);
- const [activeStatus, setActiveStatus] = useState<"all" | AgentStatus>(initialStatus);
- useEffect(() => {
- let mounted = true;
- async function loadAgents() {
- const query = activeStatus === "all" ? "" : `?status=${activeStatus}`;
- const response = await fetch(`/api/agents${query}`, { cache: "no-store" });
- if (!response.ok) {
- throw new Error("Failed to load agents");
- }
- const data = (await response.json()) as AgentFeed;
- if (!mounted) {
- return;
- }
- startTransition(() => {
- setFeed(data);
- });
- }
- loadAgents().catch(() => undefined);
- const interval = window.setInterval(() => {
- loadAgents().catch(() => undefined);
- }, 10000);
- return () => {
- mounted = false;
- window.clearInterval(interval);
- };
- }, [activeStatus]);
- const agents = feed.agents;
- const counts = {
- working: agents.filter((agent) => agent.status === "working").length,
- idle: agents.filter((agent) => agent.status === "idle").length,
- warning: agents.filter((agent) => agent.status === "warning").length,
- offline: agents.filter((agent) => agent.status === "offline").length
- };
- return (
- <section className="agents-shell agents-shell--compact">
- <div className="agents-monitor-top">
- <div className="agents-monitor-source">
- <strong>{feed.source === "file" ? "已接入真实数据源" : "当前使用演示数据"}</strong>
- <span>
- 来源 {feed.sourceLabel} · 最近刷新 {formatTimeLabel(feed.fetchedAt)}
- </span>
- </div>
- <div className="chip-row">
- {agentStatusFilters.map((filter) => (
- <button
- key={filter.key}
- type="button"
- className={`filter-chip${activeStatus === filter.key ? " filter-chip--active" : ""}`}
- onClick={() => {
- if (filter.key === "all" || isAgentStatus(filter.key)) {
- setActiveStatus(filter.key);
- }
- }}
- >
- {filter.label}
- </button>
- ))}
- </div>
- </div>
- <div className="agents-monitor-strip">
- <div className="agents-monitor-strip__item">工作中 {counts.working}</div>
- <div className="agents-monitor-strip__item">待命 {counts.idle}</div>
- <div className="agents-monitor-strip__item">待确认 {counts.warning}</div>
- <div className="agents-monitor-strip__item">离线 {counts.offline}</div>
- <div className="agents-monitor-strip__item">总计 {agents.length}</div>
- </div>
- <div className="agents-worklist">
- {agents.map((agent) => (
- <Link className="agents-worklist__link" href={`/agents/${agent.id}`} key={agent.id}>
- <article className="agents-workitem">
- <div className="agents-workitem__head">
- <div className="agents-workitem__identity">
- <AgentAvatar kind={agent.avatarKind} label={agent.name} />
- <div>
- <div className="agents-workitem__name-row">
- <strong>{agent.name}</strong>
- <span className={statusClassMap[agent.status]}>{agent.statusLabel}</span>
- </div>
- <div className="agents-workitem__role">{agent.role}</div>
- </div>
- </div>
- <div className="agents-workitem__metrics">
- <span>心跳 {agent.lastHeartbeat}</span>
- <span>队列 {agent.queueDepth}</span>
- <span>今日完成 {agent.todayCompleted}</span>
- </div>
- </div>
- <div className="agents-workitem__task">
- <div className="agents-workitem__label">当前任务</div>
- <div className="agents-workitem__value">{agent.currentTask}</div>
- </div>
- <div className="agents-workitem__meta">
- <span>任务ID {agent.taskId}</span>
- <span>阶段 {agent.taskStage}</span>
- <span>主机 {agent.host}</span>
- <span>Owner {agent.owner}</span>
- <span>运行时长 {agent.uptime}</span>
- <span>更新时间 {agent.updatedAt}</span>
- </div>
- <div className="agents-workitem__output">
- <div className="agents-workitem__label">最近输出</div>
- <div className="agents-workitem__value">{agent.lastOutput}</div>
- {agent.lastError ? <div className="agents-workitem__error">异常: {agent.lastError}</div> : null}
- </div>
- </article>
- </Link>
- ))}
- </div>
- </section>
- );
- }
|