Q162: the workload chart counts every agent and groups the rest into one bar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
58871fbaed
commit
c78045d523
4 files changed
+51
-9
No files matched your search
@@ -54,6 +54,7 @@ export const zh: Record<string, string> = {
|
||||
"Cancel run": "取消运行",
|
||||
"Show older runs": "显示更早的运行",
|
||||
"Target date": "目标日期",
|
||||
"{n} others": "其余 {n} 人",
|
||||
"Work accepted (7 days)": "工作验收(近 7 天)",
|
||||
"{accepted} done · {returned} sent back · sessions: {ok} ended, {failed} failed": "{accepted} 项完成 · {returned} 项退回 · 会话:{ok} 次结束,{failed} 次失败",
|
||||
"no target date": "未设目标日期",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { workloadBars } from "./workload-bars";
|
||||
import type { UsageStatsRecord } from "../../../../shared/usage-stats";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import type { AgentRecord, TaskRecord } from "../../../../shared/domain";
|
||||
import { TASK_STATUS } from "../../../../shared/domain";
|
||||
import { t } from "../../i18n/i18n";
|
||||
|
||||
const CHART_H = 116;
|
||||
@@ -22,16 +22,15 @@ function ChartCard({ title, readout, children }: { title: string; readout: strin
|
||||
);
|
||||
}
|
||||
|
||||
/** Agents beyond this share one bar, so a large team still fits and is still counted. */
|
||||
const MAX_WORKLOAD_BARS = 8;
|
||||
|
||||
export function AgentWorkloadChart({ agents, tasks }: { agents: AgentRecord[]; tasks: TaskRecord[] }) {
|
||||
const data = agents.slice(0, 8).map((a) => ({
|
||||
name: a.name.split(/\s+/)[0] ?? a.name,
|
||||
count: tasks.filter((t) => t.assigneeAgentId === a.id && t.status !== TASK_STATUS.DONE).length,
|
||||
}));
|
||||
const total = data.reduce((s, d) => s + d.count, 0);
|
||||
const { bars: data, total } = workloadBars(agents, tasks, MAX_WORKLOAD_BARS);
|
||||
const max = Math.max(1, ...data.map((d) => d.count));
|
||||
const barW = data.length > 0 ? (CHART_W - (data.length - 1) * BAR_GAP) / data.length : 0;
|
||||
const [hover, setHover] = useState<number | null>(null);
|
||||
const readout = hover !== null && data[hover] ? `${data[hover]!.name} · ${data[hover]!.count}` : t("{n} open", { n: total });
|
||||
const readout = hover !== null && data[hover] ? `${data[hover]!.label} · ${data[hover]!.count}` : t("{n} open", { n: total });
|
||||
|
||||
return (
|
||||
<ChartCard title={t("Agent workload")} readout={readout}>
|
||||
@@ -40,7 +39,7 @@ export function AgentWorkloadChart({ agents, tasks }: { agents: AgentRecord[]; t
|
||||
const h = (d.count / max) * (CHART_H - 22);
|
||||
const x = i * (barW + BAR_GAP);
|
||||
return (
|
||||
<g key={d.name} onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}>
|
||||
<g key={d.key} onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}>
|
||||
<rect x={x} y={0} width={barW} height={CHART_H - 16} fill="transparent" />
|
||||
<rect
|
||||
x={x}
|
||||
@@ -52,7 +51,7 @@ export function AgentWorkloadChart({ agents, tasks }: { agents: AgentRecord[]; t
|
||||
opacity={hover === null || hover === i ? 0.85 : 0.32}
|
||||
style={{ transition: "opacity var(--motion-fast) var(--ease-out)" }}
|
||||
/>
|
||||
<text x={x + barW / 2} y={CHART_H - 2} textAnchor="middle" fontSize="9" fill="var(--text-3)">{d.name}</text>
|
||||
<text x={x + barW / 2} y={CHART_H - 2} textAnchor="middle" fontSize="9" fill="var(--text-3)">{d.label}</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { TASK_STATUS, type AgentRecord, type TaskRecord } from "../../../../shared/domain";
|
||||
import { t } from "../../i18n/i18n";
|
||||
|
||||
/** Open work per agent for the chart: the busiest get their own bar, the rest share one labelled bar, and the total counts everyone. */
|
||||
export function workloadBars(agents: AgentRecord[], tasks: TaskRecord[], maxBars: number): { bars: Array<{ key: string; label: string; count: number }>; total: number } {
|
||||
const counts = agents.map((agent) => ({
|
||||
key: agent.id,
|
||||
label: agent.name.split(/\s+/)[0] ?? agent.name,
|
||||
count: tasks.filter((task) => task.assigneeAgentId === agent.id && task.status !== TASK_STATUS.DONE).length,
|
||||
}));
|
||||
const total = counts.reduce((sum, item) => sum + item.count, 0);
|
||||
if (counts.length <= maxBars) return { bars: counts, total };
|
||||
const ranked = [...counts].sort((a, b) => b.count - a.count);
|
||||
const shown = ranked.slice(0, maxBars - 1);
|
||||
const rest = ranked.slice(maxBars - 1);
|
||||
const others = { key: "others", label: t("{n} others", { n: rest.length }), count: rest.reduce((sum, item) => sum + item.count, 0) };
|
||||
return { bars: [...shown, others], total };
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { workloadBars } from "../../src/renderer/src/pages/home/workload-bars";
|
||||
import type { AgentRecord, TaskRecord } from "../../src/shared/domain";
|
||||
|
||||
const agent = (id: string, name: string) => ({ id, name }) as AgentRecord;
|
||||
const open = (assignee: string, count: number) => Array.from({ length: count }, () => ({ assigneeAgentId: assignee, status: "in_progress" }) as TaskRecord);
|
||||
|
||||
describe("team workload", () => {
|
||||
test("counts every agent: the busiest show, the rest are one labelled bar, and the total is everyone's", () => {
|
||||
const agents = Array.from({ length: 10 }, (_, index) => agent(`agt_${index}`, `Agent ${index}`));
|
||||
const tasks = agents.flatMap((item, index) => open(item.id, index));
|
||||
const { bars, total } = workloadBars(agents, tasks, 8);
|
||||
expect(total).toBe(45);
|
||||
expect(bars.map((bar) => [bar.label, bar.count])).toEqual([
|
||||
["Agent", 9], ["Agent", 8], ["Agent", 7], ["Agent", 6], ["Agent", 5], ["Agent", 4], ["Agent", 3], ["3 others", 3],
|
||||
]);
|
||||
expect(new Set(bars.map((bar) => bar.key)).size).toBe(8);
|
||||
});
|
||||
|
||||
test("a small team shows everyone", () => {
|
||||
const { bars, total } = workloadBars([agent("a", "Ada Lovelace"), agent("b", "Ben")], [...open("a", 2)], 8);
|
||||
expect([bars.map((bar) => bar.label), total]).toEqual([["Ada", "Ben"], 2]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user