agents-dashboard.tsx 5.7 KB

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