agents-dashboard.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. "use client";
  2. import Link from "next/link";
  3. import { startTransition, useEffect, useState } from "react";
  4. import { AgentAvatar } from "@/components/agents/agent-avatar";
  5. import { agentStatusFilters } from "@/data/demo-agents";
  6. import { AgentFeed, AgentStatus } from "@/types/agent";
  7. const statusClassMap = {
  8. working: "status-pill status-pill--working",
  9. idle: "status-pill status-pill--idle",
  10. warning: "status-pill status-pill--warning",
  11. offline: "status-pill status-pill--offline"
  12. } as const;
  13. type AgentsDashboardProps = {
  14. initialFeed: AgentFeed;
  15. initialStatus: "all" | AgentStatus;
  16. };
  17. function isAgentStatus(value: string): value is AgentStatus {
  18. return value === "working" || value === "idle" || value === "warning" || value === "offline";
  19. }
  20. function formatTimeLabel(value: string) {
  21. return new Date(value).toLocaleTimeString("zh-CN", {
  22. hour: "2-digit",
  23. minute: "2-digit",
  24. second: "2-digit"
  25. });
  26. }
  27. export function AgentsDashboard({ initialFeed, initialStatus }: AgentsDashboardProps) {
  28. const [feed, setFeed] = useState(initialFeed);
  29. const [activeStatus, setActiveStatus] = useState<"all" | AgentStatus>(initialStatus);
  30. useEffect(() => {
  31. let mounted = true;
  32. async function loadAgents() {
  33. const query = activeStatus === "all" ? "" : `?status=${activeStatus}`;
  34. const response = await fetch(`/api/agents${query}`, { cache: "no-store" });
  35. if (!response.ok) {
  36. throw new Error("Failed to load agents");
  37. }
  38. const data = (await response.json()) as AgentFeed;
  39. if (!mounted) {
  40. return;
  41. }
  42. startTransition(() => {
  43. setFeed(data);
  44. });
  45. }
  46. loadAgents().catch(() => undefined);
  47. const interval = window.setInterval(() => {
  48. loadAgents().catch(() => undefined);
  49. }, 10000);
  50. return () => {
  51. mounted = false;
  52. window.clearInterval(interval);
  53. };
  54. }, [activeStatus]);
  55. const agents = feed.agents;
  56. const counts = {
  57. working: agents.filter((agent) => agent.status === "working").length,
  58. idle: agents.filter((agent) => agent.status === "idle").length,
  59. warning: agents.filter((agent) => agent.status === "warning").length,
  60. offline: agents.filter((agent) => agent.status === "offline").length
  61. };
  62. return (
  63. <section className="agents-shell agents-shell--compact">
  64. <div className="agents-monitor-top">
  65. <div className="agents-monitor-source">
  66. <strong>{feed.source === "file" ? "已接入真实数据源" : "当前使用演示数据"}</strong>
  67. <span>
  68. 来源 {feed.sourceLabel} · 最近刷新 {formatTimeLabel(feed.fetchedAt)}
  69. </span>
  70. </div>
  71. <div className="chip-row">
  72. {agentStatusFilters.map((filter) => (
  73. <button
  74. key={filter.key}
  75. type="button"
  76. className={`filter-chip${activeStatus === filter.key ? " filter-chip--active" : ""}`}
  77. onClick={() => {
  78. if (filter.key === "all" || isAgentStatus(filter.key)) {
  79. setActiveStatus(filter.key);
  80. }
  81. }}
  82. >
  83. {filter.label}
  84. </button>
  85. ))}
  86. </div>
  87. </div>
  88. <div className="agents-monitor-strip">
  89. <div className="agents-monitor-strip__item">工作中 {counts.working}</div>
  90. <div className="agents-monitor-strip__item">待命 {counts.idle}</div>
  91. <div className="agents-monitor-strip__item">待确认 {counts.warning}</div>
  92. <div className="agents-monitor-strip__item">离线 {counts.offline}</div>
  93. <div className="agents-monitor-strip__item">总计 {agents.length}</div>
  94. </div>
  95. <div className="agents-worklist">
  96. {agents.map((agent) => (
  97. <Link className="agents-worklist__link" href={`/agents/${agent.id}`} key={agent.id}>
  98. <article className="agents-workitem">
  99. <div className="agents-workitem__head">
  100. <div className="agents-workitem__identity">
  101. <AgentAvatar kind={agent.avatarKind} label={agent.name} />
  102. <div>
  103. <div className="agents-workitem__name-row">
  104. <strong>{agent.name}</strong>
  105. <span className={statusClassMap[agent.status]}>{agent.statusLabel}</span>
  106. </div>
  107. <div className="agents-workitem__role">{agent.role}</div>
  108. </div>
  109. </div>
  110. <div className="agents-workitem__metrics">
  111. <span>心跳 {agent.lastHeartbeat}</span>
  112. <span>队列 {agent.queueDepth}</span>
  113. <span>今日完成 {agent.todayCompleted}</span>
  114. </div>
  115. </div>
  116. <div className="agents-workitem__task">
  117. <div className="agents-workitem__label">当前任务</div>
  118. <div className="agents-workitem__value">{agent.currentTask}</div>
  119. </div>
  120. <div className="agents-workitem__meta">
  121. <span>任务ID {agent.taskId}</span>
  122. <span>阶段 {agent.taskStage}</span>
  123. <span>主机 {agent.host}</span>
  124. <span>Owner {agent.owner}</span>
  125. <span>运行时长 {agent.uptime}</span>
  126. <span>更新时间 {agent.updatedAt}</span>
  127. </div>
  128. <div className="agents-workitem__output">
  129. <div className="agents-workitem__label">最近输出</div>
  130. <div className="agents-workitem__value">{agent.lastOutput}</div>
  131. {agent.lastError ? <div className="agents-workitem__error">异常: {agent.lastError}</div> : null}
  132. </div>
  133. </article>
  134. </Link>
  135. ))}
  136. </div>
  137. </section>
  138. );
  139. }