diff --git a/apps/web/src/app.css b/apps/web/src/app.css index c6a6c02f7..304ae68e8 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -4557,6 +4557,51 @@ tr.insights-row-clickable:hover { height: 0.9rem; } +/* Bench drawer Insights and Members tabs */ +.drawer-stack { + display: flex; + flex-direction: column; + gap: 1rem; +} +.drawer-link { + align-self: flex-start; + font-size: 0.85rem; + color: var(--foreground); + text-decoration: underline; +} +.drawer-list { + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + list-style: none; +} +.drawer-list > li, +.drawer-list-link { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0; +} +.drawer-list-link { + width: 100%; + color: inherit; + text-decoration: none; +} +.drawer-list-text { + display: flex; + flex-direction: column; + min-width: 0; + font-size: 0.85rem; +} +.drawer-list-text > span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.78rem; + color: var(--muted-foreground); +} + .stage-crumb-back { display: inline-flex; flex-shrink: 0; diff --git a/apps/web/src/bench/insights-tab.tsx b/apps/web/src/bench/insights-tab.tsx new file mode 100644 index 000000000..ab2358d1f --- /dev/null +++ b/apps/web/src/bench/insights-tab.tsx @@ -0,0 +1,57 @@ +import { RichEmptyState, Skeleton } from "@corbits/react-ui"; + +import { useAPIQuery } from "../api"; +import { insightsTopLevelRunsPath, TopLevelRunsSchema } from "../insights-api"; +import { computeBenchInsights, durationLabel, formatCount } from "../insights-stats"; +import { Link } from "../navigation"; +import { INSIGHTS_PATH_PREFIX } from "../path-ids"; +import { OutcomeChart } from "../pages/bench-insights"; + +/** The bench Insights page in miniature: the same runs query and + * `computeBenchInsights`, fixed to the last 7 days. */ +export function InsightsTab({ workbenchTenantId }: { readonly workbenchTenantId: string }) { + const runs = useAPIQuery(insightsTopLevelRunsPath(workbenchTenantId), TopLevelRunsSchema); + + if (runs.kind === "loading") return ; + if (runs.kind !== "ready") { + return ( + + ); + } + + const stats = computeBenchInsights(runs.data.data, 7); + const finished = stats.ok + stats.fail; + const tiles: readonly (readonly [string, string])[] = [ + ["Runs", formatCount(stats.total)], + ["Succeeded", finished === 0 ? "—" : `${Math.round((stats.ok / finished) * 100)}%`], + ["Median run", stats.medianMs === null ? "—" : durationLabel(stats.medianMs)], + ]; + const id = encodeURIComponent(workbenchTenantId); + + return ( +
+
+

Last 7 days

+
+ {tiles.map(([label, value]) => ( +
+
{value}
+
{label}
+
+ ))} +
+
+
+

Runs per day

+ +
+ + Open Insights + +
+ ); +} diff --git a/apps/web/src/bench/members-tab.tsx b/apps/web/src/bench/members-tab.tsx new file mode 100644 index 000000000..101428f90 --- /dev/null +++ b/apps/web/src/bench/members-tab.tsx @@ -0,0 +1,80 @@ +import { RichEmptyState, Skeleton } from "@corbits/react-ui"; +import { useQuery } from "@tanstack/react-query"; + +import { IdentityAvatar, WorkbenchAvatar } from "@/chat/avatar"; +import type { WorkbenchParticipant } from "@/chat/threads-api"; +import { Link } from "../navigation"; +import { tenantKeys } from "../query-client"; +import { principalLabel } from "../settings/identity"; +import { listPrincipals } from "../settings/tenancy-api"; + +/** Humans are the bench tenant's user principals; agents are the bench's + * workers, already listed as participants. */ +export function MembersTab({ + workbenchTenantId, + participants, +}: { + readonly workbenchTenantId: string; + readonly participants: readonly WorkbenchParticipant[]; +}) { + // A distinct key from Settings -> People, which caches a different shape + // under the bare principals key; prefix invalidation still reaches both. + const people = useQuery({ + queryKey: [...tenantKeys.principals(workbenchTenantId), "members"], + queryFn: async () => (await listPrincipals(workbenchTenantId)).filter((p) => p.kind === "user"), + }); + const agents = participants.filter((p) => p.kind === "agent"); + + return ( +
+
+

People

+ {people.isPending ? : null} + {people.isError ? ( + void people.refetch() }]} + /> + ) : null} + {people.data?.length === 0 ? ( +

No people yet.

+ ) : null} +
    + {(people.data ?? []).map((person) => { + const { label } = principalLabel(person.displayName); + const detail = [person.email, person.roles.map((r) => r.name).join(", ")] + .filter((s): s is string => s !== undefined && s !== "") + .join(" · "); + return ( +
  • + + + {label} + {detail} + +
  • + ); + })} +
+
+
+

Agents

+ {agents.length === 0 ?

No agents yet.

: null} +
    + {agents.map((agent) => ( +
  • + + + + {agent.name} + {agent.address === "" ? "Starting" : "Live"} + + +
  • + ))} +
+
+
+ ); +} diff --git a/apps/web/src/pages/bench-insights.tsx b/apps/web/src/pages/bench-insights.tsx index e9385da68..fe7ec6255 100644 --- a/apps/web/src/pages/bench-insights.tsx +++ b/apps/web/src/pages/bench-insights.tsx @@ -37,7 +37,7 @@ function dayLabel(date: Date): string { }); } -function OutcomeChart({ days }: { readonly days: readonly BenchDay[] }) { +export function OutcomeChart({ days }: { readonly days: readonly BenchDay[] }) { const [active, setActive] = useState(null); const max = Math.max(...days.map((d) => d.ok + d.fail), 1); const every = days.length > 30 ? 14 : days.length > 7 ? 5 : 1; diff --git a/apps/web/src/pages/workbench-page.tsx b/apps/web/src/pages/workbench-page.tsx index c33eeb8be..40affe61e 100644 --- a/apps/web/src/pages/workbench-page.tsx +++ b/apps/web/src/pages/workbench-page.tsx @@ -27,6 +27,8 @@ import { BenchDrawer } from "../bench/bench-drawer"; import { BenchPill } from "../bench/bench-pill"; import { GrantsTab } from "../bench/grants-tab"; import { InformationTab } from "../bench/information-tab"; +import { InsightsTab } from "../bench/insights-tab"; +import { MembersTab } from "../bench/members-tab"; import { ToolsTab } from "../bench/tools-tab"; import { useBench } from "../bench-context"; import { createFetchStockHub } from "../needs-converge"; @@ -316,6 +318,13 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } ), Tools: , Grants: , + Insights: , + Members: ( + + ), }} />