TeamOverview.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import type { ReactNode } from "react";
  2. import { useTranslation } from "react-i18next";
  3. import { Activity, AlertTriangle, CalendarClock, GitBranch, ShieldCheck, Target, UserRound, Users } from "lucide-react";
  4. import { Badge } from "@/components/ui/badge";
  5. import type { JSONObject, TeamConfig, TeamDefinition, TeamRun } from "@/types";
  6. import { formatDateTime } from "@/lib/utils";
  7. export function TeamOverview({
  8. team,
  9. activeConfig,
  10. runCount,
  11. failedRunCount,
  12. latestRun,
  13. }: {
  14. team: TeamDefinition;
  15. activeConfig?: TeamConfig;
  16. runCount: number;
  17. failedRunCount: number;
  18. latestRun?: TeamRun;
  19. }) {
  20. const { t } = useTranslation();
  21. const memberCount = activeConfig?.member_refs_json.length ?? 0;
  22. return (
  23. <div className="min-w-0 space-y-4">
  24. <div className="grid min-w-0 gap-3 md:grid-cols-4">
  25. <Signal icon={<Activity className="h-4 w-4" />} label={t("common.runs")} value={runCount} />
  26. <Signal icon={<AlertTriangle className="h-4 w-4" />} label={t("teams.failedRuns")} value={failedRunCount} />
  27. <Signal icon={<Users className="h-4 w-4" />} label={t("teams.members")} value={memberCount} />
  28. <Signal icon={<CalendarClock className="h-4 w-4" />} label={t("teams.lastRun")} value={latestRun ? formatDateTime(latestRun.created_time) : t("teams.noRuns")} />
  29. </div>
  30. <section className="min-w-0 rounded-md border border-border bg-surface-base p-4">
  31. <div className="flex min-w-0 items-start gap-3">
  32. <div className="grid h-10 w-10 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">
  33. <Target className="h-5 w-5" />
  34. </div>
  35. <div className="min-w-0 flex-1">
  36. <div className="flex min-w-0 flex-wrap items-center gap-2">
  37. <h3 className="truncate text-sm font-semibold">{t("teams.currentObjective")}</h3>
  38. <Badge className="border-border bg-muted/40 text-muted-foreground">{readableLabel(activeConfig?.coordination_mode ?? team.team_type)}</Badge>
  39. </div>
  40. <p className="mt-2 break-words text-sm leading-6 text-muted-foreground">
  41. {activeConfig?.objective || team.description || t("teams.noObjectiveProvided")}
  42. </p>
  43. </div>
  44. </div>
  45. </section>
  46. {activeConfig ? (
  47. <div className="grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1fr)_280px]">
  48. <section className="min-w-0 rounded-md border border-border bg-surface-base p-4">
  49. <SectionHeader icon={<Users className="h-4 w-4" />} title={t("teams.members")} />
  50. {activeConfig.member_refs_json.length ? (
  51. <div className="mt-3 grid min-w-0 gap-2">
  52. {activeConfig.member_refs_json.map((member, index) => (
  53. <MemberCard key={index} member={member} index={index} />
  54. ))}
  55. </div>
  56. ) : (
  57. <p className="mt-3 text-sm text-muted-foreground">{t("teams.noMembersConfigured")}</p>
  58. )}
  59. </section>
  60. <section className="min-w-0 space-y-3 rounded-md border border-border bg-muted/20 p-4">
  61. <SectionHeader icon={<GitBranch className="h-4 w-4" />} title={t("teams.policy")} />
  62. <PolicyRow icon={<GitBranch className="h-4 w-4" />} label={t("teams.coordination")} value={readableLabel(activeConfig.coordination_mode)} />
  63. <PolicyRow icon={<Activity className="h-4 w-4" />} label={t("teams.maxRoundsField")} value={getJsonString(activeConfig.policy_json, "max_rounds") ?? t("teams.none")} />
  64. <PolicyRow icon={<ShieldCheck className="h-4 w-4" />} label={t("teams.handoff")} value={readableLabel(getJsonString(activeConfig.policy_json, "handoff") ?? t("teams.none"))} />
  65. <div className="rounded-md border border-border bg-surface-elevated p-3">
  66. <p className="text-xs text-muted-foreground">{t("common.created")}</p>
  67. <p className="mt-1 truncate text-sm font-medium">{formatDateTime(activeConfig.created_time)}</p>
  68. </div>
  69. </section>
  70. </div>
  71. ) : (
  72. <div className="rounded-md border border-dashed border-border bg-muted/20 p-5 text-sm text-muted-foreground">
  73. {t("teams.noTeamConfiguration")}
  74. </div>
  75. )}
  76. </div>
  77. );
  78. }
  79. function Signal({ icon, label, value }: { icon: ReactNode; label: string; value: string | number }) {
  80. return (
  81. <div className="min-w-0 rounded-md border border-border bg-muted/25 p-3">
  82. <div className="flex min-w-0 items-center gap-2 text-muted-foreground">
  83. <span className="shrink-0 text-primary">{icon}</span>
  84. <span className="truncate text-xs">{label}</span>
  85. </div>
  86. <p className="mt-2 truncate text-xl font-semibold tabular-nums">{value}</p>
  87. </div>
  88. );
  89. }
  90. function MemberCard({ member, index }: { member: JSONObject; index: number }) {
  91. const { t } = useTranslation();
  92. const name = getJsonString(member, "name") ?? getJsonString(member, "agent_name") ?? `${t("teams.member")} ${index + 1}`;
  93. const role = getJsonString(member, "role") ?? t("teams.member");
  94. const responsibility = getJsonString(member, "description") ?? getJsonString(member, "responsibility") ?? t("teams.noResponsibilitySummary");
  95. return (
  96. <div className="min-w-0 rounded-md border border-border bg-muted/20 p-3">
  97. <div className="flex min-w-0 items-start justify-between gap-3">
  98. <div className="flex min-w-0 items-start gap-2">
  99. <div className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">
  100. <UserRound className="h-4 w-4" />
  101. </div>
  102. <div className="min-w-0">
  103. <p className="truncate text-sm font-semibold">{name}</p>
  104. <p className="mt-0.5 truncate text-xs text-muted-foreground">
  105. {getJsonString(member, "agent_id") ?? getJsonString(member, "id") ?? t("teams.unboundAgent")}
  106. </p>
  107. </div>
  108. </div>
  109. <Badge className="shrink-0 border-border bg-surface-elevated text-muted-foreground">{readableLabel(role)}</Badge>
  110. </div>
  111. <p className="mt-2 line-clamp-2 break-words text-sm leading-6 text-muted-foreground">{responsibility}</p>
  112. </div>
  113. );
  114. }
  115. function PolicyRow({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
  116. return (
  117. <div className="flex min-w-0 items-center gap-3 rounded-md border border-border bg-surface-elevated p-3">
  118. <span className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">{icon}</span>
  119. <div className="min-w-0">
  120. <p className="truncate text-xs text-muted-foreground">{label}</p>
  121. <p className="mt-0.5 truncate text-sm font-medium">{value}</p>
  122. </div>
  123. </div>
  124. );
  125. }
  126. function SectionHeader({ icon, title }: { icon: ReactNode; title: string }) {
  127. return (
  128. <div className="flex min-w-0 items-center gap-2">
  129. <span className="grid h-7 w-7 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">{icon}</span>
  130. <h3 className="truncate text-sm font-semibold">{title}</h3>
  131. </div>
  132. );
  133. }
  134. function readableLabel(value: string) {
  135. return value.split(/[_-]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
  136. }
  137. function getJsonString(value: JSONObject, key: string) {
  138. const item = value[key];
  139. if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") return String(item);
  140. return undefined;
  141. }