mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
Simplify community compute visibility
Signed-off-by: Alessandro Joabar <sandro@squareup.com>
This commit is contained in:
@@ -8,7 +8,6 @@ import {
|
||||
SMALL_COMPANY_COMMUNITY_COMPUTE_FIXTURE,
|
||||
} from "../communityComputeFixtures.ts";
|
||||
import { deriveCommunityComputeMapModel } from "../communityComputeMapModel.ts";
|
||||
import { CommunityComputeHealthScorecard } from "./CommunityComputeHealthScorecard.tsx";
|
||||
import { CommunityComputeKpiRow } from "./CommunityComputeKpiRow.tsx";
|
||||
import { CommunityComputeTerritoryMap } from "./CommunityComputeTerritoryMap.tsx";
|
||||
|
||||
@@ -17,7 +16,7 @@ const smallModel = deriveCommunityComputeMapModel(
|
||||
);
|
||||
|
||||
describe("community compute visual components", () => {
|
||||
it("renders contract-backed KPI values", () => {
|
||||
it("renders contract-backed headline values", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(CommunityComputeKpiRow, { kpis: smallModel.kpis }),
|
||||
);
|
||||
@@ -28,18 +27,7 @@ describe("community compute visual components", () => {
|
||||
assert.match(html, />2,744 GB</);
|
||||
});
|
||||
|
||||
it("renders only health states production can derive", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(CommunityComputeHealthScorecard, {
|
||||
health: smallModel.health,
|
||||
}),
|
||||
);
|
||||
assert.match(html, /data-status="healthy"/);
|
||||
assert.match(html, /No reported issues/);
|
||||
assert.match(html, /data-testid="community-compute-health-healthy"/);
|
||||
});
|
||||
|
||||
it("renders one accessible territory and list item per ready model", () => {
|
||||
it("renders a simple accessible territory map without dashboard modules", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(CommunityComputeTerritoryMap, { model: smallModel }),
|
||||
);
|
||||
@@ -52,37 +40,14 @@ describe("community compute visual components", () => {
|
||||
].length,
|
||||
smallModel.deployments.length,
|
||||
);
|
||||
assert.equal(
|
||||
[
|
||||
...html.matchAll(
|
||||
/data-testid="community-compute-deployment-deployment-/g,
|
||||
),
|
||||
].length,
|
||||
smallModel.deployments.length,
|
||||
);
|
||||
assert.match(html, /tabindex="0"/);
|
||||
assert.match(html, /Search models or devices/);
|
||||
assert.match(html, /Model footprint/);
|
||||
assert.doesNotMatch(html, /Utilization|Region|Split across machines/);
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/Search models or devices|community-compute-deployment-list|community-compute-contributor-avatar|animate-mesh-inference|Map legend|Gold|selected/,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the production map static and supports explicit tutorial motion", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(CommunityComputeTerritoryMap, { model: smallModel }),
|
||||
);
|
||||
assert.doesNotMatch(html, /animate-mesh-inference/);
|
||||
|
||||
const tutorialHtml = renderToStaticMarkup(
|
||||
React.createElement(CommunityComputeTerritoryMap, {
|
||||
model: smallModel,
|
||||
inferenceDeploymentIds: [smallModel.deployments[0].id],
|
||||
}),
|
||||
);
|
||||
assert.match(tutorialHtml, /animate-mesh-inference/);
|
||||
assert.doesNotMatch(html, /community-compute-split-boundary/);
|
||||
});
|
||||
|
||||
it("renders a static empty state without a deployment list", () => {
|
||||
it("renders a static empty state without extra controls", () => {
|
||||
const emptyModel = deriveCommunityComputeMapModel(
|
||||
EMPTY_COMMUNITY_COMPUTE_FIXTURE.snapshot,
|
||||
);
|
||||
@@ -90,6 +55,6 @@ describe("community compute visual components", () => {
|
||||
React.createElement(CommunityComputeTerritoryMap, { model: emptyModel }),
|
||||
);
|
||||
assert.match(html, /data-testid="community-compute-map-empty"/);
|
||||
assert.doesNotMatch(html, /community-compute-deployment-list/);
|
||||
assert.doesNotMatch(html, /community-compute-deployment-list|Search/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
CircleHelp,
|
||||
MinusCircle,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import type {
|
||||
CommunityComputeDeploymentHealth,
|
||||
CommunityComputeHealthSummary,
|
||||
} from "../communityComputeMapModel";
|
||||
|
||||
export type CommunityComputeHealthScorecardProps = {
|
||||
health: CommunityComputeHealthSummary;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const HEALTH_ROWS: readonly {
|
||||
status: CommunityComputeDeploymentHealth;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ status: "healthy", label: "Healthy" },
|
||||
{ status: "warming", label: "Warming" },
|
||||
{ status: "idle", label: "Idle" },
|
||||
{ status: "degraded", label: "Degraded" },
|
||||
{ status: "offline", label: "Offline" },
|
||||
{ status: "unknown", label: "Unknown" },
|
||||
];
|
||||
|
||||
/** Health summary with icon, copy, and count labels that do not rely on color. */
|
||||
export function CommunityComputeHealthScorecard({
|
||||
health,
|
||||
className,
|
||||
}: CommunityComputeHealthScorecardProps) {
|
||||
const Icon = statusIcon(health.status);
|
||||
const total = Object.values(health.counts).reduce(
|
||||
(sum, count) => sum + count,
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="community-compute-health-title"
|
||||
className={cn(
|
||||
"rounded-xl border border-border/70 bg-background p-4",
|
||||
className,
|
||||
)}
|
||||
data-status={health.status}
|
||||
data-testid="community-compute-health-scorecard"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"flex size-9 shrink-0 items-center justify-center rounded-full border bg-muted/30",
|
||||
health.status === "healthy" && "border-foreground/30",
|
||||
health.status === "attention" &&
|
||||
"border-dashed border-foreground/50",
|
||||
health.status === "critical" && "border-2 border-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className="text-sm font-semibold"
|
||||
data-testid="community-compute-health-label"
|
||||
id="community-compute-health-title"
|
||||
>
|
||||
{health.label}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs leading-snug text-muted-foreground">
|
||||
{health.reason ?? healthDescription(health.status, total)}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 text-xs font-medium tabular-nums text-muted-foreground">
|
||||
{total} total
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<dl className="mt-4 grid grid-cols-3 gap-2 sm:grid-cols-6">
|
||||
{HEALTH_ROWS.map(({ status, label }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border border-border/60 px-2 py-2 text-center",
|
||||
health.counts[status] > 0 && healthClass(status),
|
||||
)}
|
||||
data-testid={`community-compute-health-${status}`}
|
||||
key={status}
|
||||
>
|
||||
<dt className="truncate text-2xs text-muted-foreground">{label}</dt>
|
||||
<dd className="mt-0.5 text-sm font-semibold tabular-nums">
|
||||
{health.counts[status]}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function statusIcon(status: CommunityComputeHealthSummary["status"]) {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return CheckCircle2;
|
||||
case "attention":
|
||||
return AlertTriangle;
|
||||
case "critical":
|
||||
return XCircle;
|
||||
case "empty":
|
||||
return MinusCircle;
|
||||
case "unknown":
|
||||
return CircleHelp;
|
||||
}
|
||||
}
|
||||
|
||||
function healthClass(status: CommunityComputeDeploymentHealth): string {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return "border-solid bg-foreground/[0.04]";
|
||||
case "warming":
|
||||
return "border-dashed bg-foreground/[0.03]";
|
||||
case "idle":
|
||||
return "border-dotted";
|
||||
case "degraded":
|
||||
return "border-dashed border-foreground/50";
|
||||
case "offline":
|
||||
return "border-foreground/50 bg-muted/50 line-through decoration-muted-foreground/50";
|
||||
case "unknown":
|
||||
return "border-dotted bg-muted/20";
|
||||
}
|
||||
}
|
||||
|
||||
function healthDescription(
|
||||
status: CommunityComputeHealthSummary["status"],
|
||||
total: number,
|
||||
): string {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return "No reporting device needs attention; some may still be warming or idle.";
|
||||
case "attention":
|
||||
return "Some deployments may have reduced availability.";
|
||||
case "critical":
|
||||
return "Shared compute is currently unavailable.";
|
||||
case "empty":
|
||||
return "No deployments have joined this community yet.";
|
||||
case "unknown":
|
||||
return total === 0
|
||||
? "Waiting for deployment health reports."
|
||||
: "Deployment health could not be determined.";
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export function CommunityComputeKpiRow({
|
||||
{
|
||||
label: "Contributed VRAM",
|
||||
value: formatCapacity(kpis.sharedCapacityGb),
|
||||
detail: `${formatCount(kpis.deploymentCount)} deployments`,
|
||||
detail: `${formatCount(kpis.deploymentCount)} models available`,
|
||||
},
|
||||
{
|
||||
label: "VRAM allocated",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { getAvatarSnapshotUrl } from "@/shared/lib/animatedAvatar";
|
||||
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
|
||||
import {
|
||||
hexTerritoryBoundaryEdges,
|
||||
layoutCommunityComputeHexTerritories,
|
||||
@@ -10,18 +8,12 @@ import {
|
||||
} from "../communityComputeHexLayout";
|
||||
import type {
|
||||
CommunityComputeDeployment,
|
||||
CommunityComputeDeploymentHealth,
|
||||
CommunityComputeMapModel,
|
||||
} from "../communityComputeMapModel";
|
||||
|
||||
export type CommunityComputeTerritoryMapProps = {
|
||||
model: CommunityComputeMapModel;
|
||||
selectedDeploymentId?: string | null;
|
||||
onSelectedDeploymentChange?: (deploymentId: string) => void;
|
||||
className?: string;
|
||||
/** Tutorial-only simulated activity; live snapshots cannot attribute requests. */
|
||||
inferenceDeploymentIds?: readonly string[];
|
||||
contributorProfiles?: ContributorProfiles;
|
||||
};
|
||||
|
||||
type Point = { x: number; y: number };
|
||||
@@ -45,15 +37,8 @@ const SQRT_THREE = Math.sqrt(3);
|
||||
*/
|
||||
export function CommunityComputeTerritoryMap({
|
||||
model,
|
||||
selectedDeploymentId,
|
||||
onSelectedDeploymentChange,
|
||||
className,
|
||||
inferenceDeploymentIds = [],
|
||||
contributorProfiles = {},
|
||||
}: CommunityComputeTerritoryMapProps) {
|
||||
const [internalSelection, setInternalSelection] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const deploymentById = React.useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
@@ -75,29 +60,14 @@ export function CommunityComputeTerritoryMap({
|
||||
React.useEffect(() => {
|
||||
previousLayoutRef.current = layout;
|
||||
}, [layout]);
|
||||
const fallbackId = model.deployments[0]?.id ?? null;
|
||||
const requestedId = selectedDeploymentId ?? internalSelection;
|
||||
const activeId =
|
||||
requestedId !== null && deploymentById.has(requestedId)
|
||||
? requestedId
|
||||
: fallbackId;
|
||||
const activeDeployment = activeId
|
||||
? (deploymentById.get(activeId) ?? null)
|
||||
: null;
|
||||
const viewBox = computeViewBox(layout.cells);
|
||||
const inferenceIds = React.useMemo(
|
||||
() => new Set(inferenceDeploymentIds),
|
||||
[inferenceDeploymentIds],
|
||||
);
|
||||
const [hoveredId, setHoveredId] = React.useState<string | null>(null);
|
||||
const lowerTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
);
|
||||
const [deploymentSearch, setDeploymentSearch] = React.useState("");
|
||||
const hoveredDeployment = hoveredId
|
||||
? (deploymentById.get(hoveredId) ?? null)
|
||||
: null;
|
||||
const isHeroDeployment = model.deployments.length === 1;
|
||||
const isDenseMap = model.deployments.length >= 50;
|
||||
|
||||
React.useEffect(
|
||||
@@ -109,26 +79,18 @@ export function CommunityComputeTerritoryMap({
|
||||
|
||||
function raiseTerritory(deploymentId: string, territoryElement: SVGGElement) {
|
||||
if (lowerTimerRef.current) clearTimeout(lowerTimerRef.current);
|
||||
// SVG paints in DOM order. Move the existing node rather than sorting the
|
||||
// React children: sorting reconstructs animated SVG content and restarts
|
||||
// its breathing phase whenever hover changes.
|
||||
// SVG paints in DOM order. Move the existing node instead of rebuilding
|
||||
// children so hover stays smooth as territories scale.
|
||||
territoryElement.parentNode?.appendChild(territoryElement);
|
||||
setHoveredId(deploymentId);
|
||||
}
|
||||
|
||||
function lowerTerritory() {
|
||||
if (lowerTimerRef.current) clearTimeout(lowerTimerRef.current);
|
||||
// Delay only the contributor overlay; the CSS scale settles independently.
|
||||
// Let the detail card linger briefly while the territory settles.
|
||||
lowerTimerRef.current = setTimeout(() => setHoveredId(null), 280);
|
||||
}
|
||||
|
||||
function selectDeployment(deploymentId: string) {
|
||||
if (selectedDeploymentId === undefined) {
|
||||
setInternalSelection(deploymentId);
|
||||
}
|
||||
onSelectedDeploymentChange?.(deploymentId);
|
||||
}
|
||||
|
||||
if (model.status === "loading") {
|
||||
return (
|
||||
<section
|
||||
@@ -142,7 +104,7 @@ export function CommunityComputeTerritoryMap({
|
||||
>
|
||||
<div className="h-48 animate-pulse rounded-lg bg-muted/50 motion-reduce:animate-none" />
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
Mapping community deployments…
|
||||
Mapping community compute…
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
@@ -161,7 +123,8 @@ export function CommunityComputeTerritoryMap({
|
||||
<EmptyHexes />
|
||||
<p className="mt-3 text-sm font-medium">No compute territories yet</p>
|
||||
<p className="mt-1 max-w-sm text-xs leading-relaxed text-muted-foreground">
|
||||
Deployments appear here when members share compute with the community.
|
||||
Shared compute appears here when members make it available to the
|
||||
community.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
@@ -177,9 +140,8 @@ export function CommunityComputeTerritoryMap({
|
||||
data-testid="community-compute-territory-map"
|
||||
>
|
||||
<div className="relative flex min-h-72 min-w-0 flex-col items-center justify-center gap-4 overflow-hidden rounded-lg border border-border/50 bg-background/70 p-3 sm:min-h-96">
|
||||
<MapLegend dense={isDenseMap} />
|
||||
<svg
|
||||
aria-label={`${model.deployments.length} community compute deployments. Select a territory for details.`}
|
||||
aria-label={`${model.deployments.length} community compute territories. Focus or hover a territory for details.`}
|
||||
className="h-auto max-h-[28rem] w-full overflow-visible"
|
||||
data-testid="community-compute-map-svg"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
@@ -189,28 +151,16 @@ export function CommunityComputeTerritoryMap({
|
||||
{layout.territories.map((territory) => {
|
||||
const deployment = deploymentById.get(territory.deploymentId);
|
||||
if (!deployment) return null;
|
||||
const selected = deployment.id === activeId;
|
||||
const inferenceActive = inferenceIds.has(deployment.id);
|
||||
return (
|
||||
// SVG groups have no semantic button equivalent; keyboard selection remains in the adjacent deployment list.
|
||||
// biome-ignore lint/a11y/useSemanticElements: SVG has no native button element.
|
||||
// SVG groups have no semantic element for hover/focus detail disclosure.
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: keyboard focus mirrors hover without making the territory an action.
|
||||
<g
|
||||
aria-label={territoryAccessibleLabel(deployment)}
|
||||
className="cursor-pointer origin-center outline-hidden [transform-box:fill-box] transition-[opacity,transform,filter] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] hover:scale-[var(--mesh-hover-scale)] hover:drop-shadow-[0_0_0.22rem_rgba(255,216,77,0.42)] focus-visible:scale-[var(--mesh-hover-scale)] focus-visible:opacity-90 motion-reduce:transition-none"
|
||||
className="origin-center outline-hidden [transform-box:fill-box] transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] hover:scale-[var(--mesh-hover-scale)] focus-visible:scale-[var(--mesh-hover-scale)] focus-visible:opacity-90 motion-reduce:transition-none"
|
||||
data-deployment-id={deployment.id}
|
||||
data-health={deployment.health}
|
||||
data-cell-count={deployment.cellCount}
|
||||
data-inference-active={inferenceActive}
|
||||
data-selected={selected}
|
||||
data-testid={`community-compute-territory-${testId(deployment.id)}`}
|
||||
key={deployment.id}
|
||||
onClick={() => selectDeployment(deployment.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
selectDeployment(deployment.id);
|
||||
}
|
||||
}}
|
||||
onBlur={lowerTerritory}
|
||||
onFocus={(event) =>
|
||||
raiseTerritory(deployment.id, event.currentTarget)
|
||||
@@ -219,7 +169,6 @@ export function CommunityComputeTerritoryMap({
|
||||
raiseTerritory(deployment.id, event.currentTarget)
|
||||
}
|
||||
onMouseLeave={lowerTerritory}
|
||||
role="button"
|
||||
style={territoryHoverStyle(
|
||||
territory.cells,
|
||||
viewBox,
|
||||
@@ -228,31 +177,11 @@ export function CommunityComputeTerritoryMap({
|
||||
tabIndex={0}
|
||||
>
|
||||
<title>{territoryAccessibleLabel(deployment)}</title>
|
||||
<TerritoryShape
|
||||
avatarUrl={contributorAvatarUrl(
|
||||
deployment.source.memberPubkey,
|
||||
contributorProfiles,
|
||||
)}
|
||||
cells={territory.cells}
|
||||
contributorLabel={contributorLabel(
|
||||
deployment,
|
||||
contributorProfiles,
|
||||
)}
|
||||
inferenceActive={inferenceActive}
|
||||
selected={selected}
|
||||
showContributor={!isDenseMap || hoveredId === deployment.id}
|
||||
territoryCenter={territory.center}
|
||||
/>
|
||||
<TerritoryShape cells={territory.cells} />
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
{isHeroDeployment && activeDeployment ? (
|
||||
<HeroDeploymentOverlay
|
||||
deployment={activeDeployment}
|
||||
inferenceActive={inferenceIds.has(activeDeployment.id)}
|
||||
/>
|
||||
) : null}
|
||||
{hoveredDeployment ? (
|
||||
<div
|
||||
className="pointer-events-none absolute bottom-3 left-3 max-w-64 rounded-xl border border-border/70 bg-popover/95 px-3 py-2 shadow-lg backdrop-blur-sm"
|
||||
@@ -268,224 +197,43 @@ export function CommunityComputeTerritoryMap({
|
||||
{hoveredDeployment.deviceLabel} ·{" "}
|
||||
{formatCapacity(hoveredDeployment.capacityGb)}
|
||||
</p>
|
||||
<p className="mt-1 text-2xs capitalize text-muted-foreground">
|
||||
{inferenceIds.has(hoveredDeployment.id)
|
||||
? "Simulated inference"
|
||||
: hoveredDeployment.health}{" "}
|
||||
· {hoveredDeployment.cellCount}{" "}
|
||||
{hoveredDeployment.cellCount === 1 ? "cell" : "cells"}
|
||||
<p className="mt-1 text-2xs text-muted-foreground">
|
||||
Available to the community
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-0 gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(16rem,0.36fr)]">
|
||||
<DeploymentList
|
||||
activeId={activeId}
|
||||
deployments={model.deployments}
|
||||
onSearchChange={setDeploymentSearch}
|
||||
onSelect={selectDeployment}
|
||||
search={deploymentSearch}
|
||||
/>
|
||||
{activeDeployment ? (
|
||||
<DeploymentDetails deployment={activeDeployment} />
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MapLegend({ dense }: { dense: boolean }) {
|
||||
return (
|
||||
<div
|
||||
aria-label="Map legend"
|
||||
role="note"
|
||||
className="pointer-events-none z-10 flex w-fit max-w-full flex-wrap items-center justify-center gap-x-3 gap-y-1 self-end rounded-lg border border-border/60 bg-background/90 px-2.5 py-2 text-2xs text-muted-foreground shadow-sm backdrop-blur-sm"
|
||||
data-testid="community-compute-map-legend"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-2.5 rotate-45 bg-foreground" />
|
||||
Area = model memory
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-2.5 animate-pulse rounded-full bg-[#f5b800] motion-reduce:animate-none" />
|
||||
Gold breath = serving
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-full border border-border bg-muted" />
|
||||
{dense ? "Hover = contributor" : "Avatar = contributor"}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-2.5 rotate-45 border-2 border-[#ffd84d]" />
|
||||
Gold border = selected
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroDeploymentOverlay({
|
||||
deployment,
|
||||
inferenceActive,
|
||||
}: {
|
||||
deployment: CommunityComputeDeployment;
|
||||
inferenceActive: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute bottom-3 left-1/2 w-[min(19rem,calc(100%-2rem))] -translate-x-1/2 rounded-xl border border-background/20 bg-background/90 p-3 text-center shadow-xl backdrop-blur-md"
|
||||
data-testid="community-compute-hero-deployment"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"size-2 rounded-full",
|
||||
deployment.health === "healthy"
|
||||
? "bg-emerald-500"
|
||||
: "bg-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
<span className="text-2xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{inferenceActive ? "Serving now" : deployment.health}
|
||||
</span>
|
||||
</div>
|
||||
<h3
|
||||
className="mt-1 truncate text-sm font-semibold"
|
||||
title={deployment.modelId}
|
||||
>
|
||||
{shortModelName(deployment.modelId)}
|
||||
</h3>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{deployment.deviceLabel}
|
||||
{deployment.isSelf ? " · This machine" : ""}
|
||||
</p>
|
||||
<dl className="mt-2 grid grid-cols-3 divide-x divide-border/60 rounded-lg border border-border/60 bg-background/70 py-2">
|
||||
<HeroMetric
|
||||
label="Shared memory"
|
||||
value={formatCapacity(deployment.capacityGb)}
|
||||
/>
|
||||
<HeroMetric
|
||||
label="Model footprint"
|
||||
value={formatCapacity(deployment.source.modelSizeGb ?? null)}
|
||||
/>
|
||||
<HeroMetric label="Availability" value="Community ready" />
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroMetric({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="min-w-0 px-2">
|
||||
<dt className="truncate text-3xs uppercase tracking-wide text-muted-foreground">
|
||||
{label}
|
||||
</dt>
|
||||
<dd className="mt-0.5 truncate text-2xs font-semibold" title={value}>
|
||||
{value}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TerritoryShape({
|
||||
avatarUrl,
|
||||
cells,
|
||||
contributorLabel,
|
||||
inferenceActive,
|
||||
selected,
|
||||
showContributor,
|
||||
territoryCenter,
|
||||
}: {
|
||||
avatarUrl: string | null;
|
||||
cells: readonly CommunityComputeHexCell[];
|
||||
contributorLabel: string;
|
||||
inferenceActive: boolean;
|
||||
selected: boolean;
|
||||
showContributor: boolean;
|
||||
territoryCenter: { q: number; r: number };
|
||||
}) {
|
||||
const boundary = hexTerritoryBoundaryEdges(cells);
|
||||
const fillClass = "fill-foreground";
|
||||
const territoryAnimationStyle = inferenceActive
|
||||
? ({
|
||||
"--mesh-breath-duration": `${5.8 + (stableVisualHash(cells[0]?.deploymentId ?? "") % 7) * 0.24}s`,
|
||||
animationDelay: `${-((stableVisualHash(`${cells[0]?.deploymentId}:phase`) % 37) / 10)}s`,
|
||||
} as React.CSSProperties)
|
||||
: undefined;
|
||||
const center = axialToPoint(territoryCenter.q, territoryCenter.r);
|
||||
const avatarRadius = 0.42;
|
||||
const avatarClipId = `contributor-${testId(cells[0]?.deploymentId ?? "unknown")}`;
|
||||
return (
|
||||
<g>
|
||||
<defs>
|
||||
<clipPath id={avatarClipId}>
|
||||
<circle cx={center.x} cy={center.y} r={avatarRadius} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
{cells.map((cell) => {
|
||||
const center = axialToPoint(cell.q, cell.r);
|
||||
return (
|
||||
<polygon
|
||||
className={cn(
|
||||
fillClass,
|
||||
"stroke-transparent",
|
||||
inferenceActive &&
|
||||
"animate-mesh-inference motion-reduce:animate-none",
|
||||
)}
|
||||
className="fill-foreground stroke-transparent"
|
||||
data-testid="community-compute-tile"
|
||||
key={cell.id}
|
||||
points={hexPoints(center, FUSED_TILE_RADIUS)}
|
||||
style={territoryAnimationStyle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<path
|
||||
className={cn(
|
||||
"fill-none transition-[opacity,stroke-width] duration-200 motion-reduce:transition-none",
|
||||
selected ? "stroke-[#f5b800]" : "stroke-background",
|
||||
)}
|
||||
className="fill-none stroke-background"
|
||||
d={edgesToPath(boundary)}
|
||||
data-testid="community-compute-territory-boundary"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
// Selection uses Buzz gold while the normal perimeter cuts a clean gutter.
|
||||
strokeWidth={selected ? TILE_EDGE_WIDTH * 2.1 : TILE_EDGE_WIDTH}
|
||||
strokeWidth={TILE_EDGE_WIDTH}
|
||||
/>
|
||||
{showContributor ? (
|
||||
<circle
|
||||
className={cn(
|
||||
"fill-background stroke-background",
|
||||
selected && "stroke-[#f5b800]",
|
||||
)}
|
||||
cx={center.x}
|
||||
cy={center.y}
|
||||
data-testid="community-compute-contributor-avatar"
|
||||
r={avatarRadius + 0.09}
|
||||
strokeWidth={0.09}
|
||||
/>
|
||||
) : null}
|
||||
{showContributor && avatarUrl ? (
|
||||
<image
|
||||
aria-label={`${contributorLabel} avatar`}
|
||||
clipPath={`url(#${avatarClipId})`}
|
||||
height={avatarRadius * 2}
|
||||
href={avatarUrl}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
width={avatarRadius * 2}
|
||||
x={center.x - avatarRadius}
|
||||
y={center.y - avatarRadius}
|
||||
/>
|
||||
) : showContributor ? (
|
||||
<text
|
||||
className="pointer-events-none fill-foreground font-bold"
|
||||
dominantBaseline="central"
|
||||
fontSize={0.42}
|
||||
textAnchor="middle"
|
||||
x={center.x}
|
||||
y={center.y}
|
||||
>
|
||||
{contributorInitials(contributorLabel)}
|
||||
</text>
|
||||
) : null}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
@@ -521,169 +269,6 @@ function edgesToPath(
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function DeploymentList({
|
||||
deployments,
|
||||
activeId,
|
||||
onSearchChange,
|
||||
onSelect,
|
||||
search,
|
||||
}: {
|
||||
deployments: readonly CommunityComputeDeployment[];
|
||||
activeId: string | null;
|
||||
onSearchChange: (value: string) => void;
|
||||
onSelect: (id: string) => void;
|
||||
search: string;
|
||||
}) {
|
||||
const query = search.trim().toLowerCase();
|
||||
const visibleDeployments = query
|
||||
? deployments.filter((deployment) =>
|
||||
[deployment.modelId, deployment.deviceLabel]
|
||||
.filter(Boolean)
|
||||
.some((value) => value?.toLowerCase().includes(query)),
|
||||
)
|
||||
: deployments;
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<p className="text-xs font-medium text-muted-foreground">Deployments</p>
|
||||
<span className="text-2xs tabular-nums text-muted-foreground">
|
||||
{visibleDeployments.length} of {deployments.length}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
aria-label="Search deployments"
|
||||
className="mb-2 h-9 w-full rounded-lg border border-input/60 bg-background px-3 text-xs outline-hidden placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onChange={(event) => onSearchChange(event.target.value)}
|
||||
placeholder="Search models or devices"
|
||||
type="search"
|
||||
value={search}
|
||||
/>
|
||||
<ul
|
||||
aria-label="Community compute deployments"
|
||||
className="max-h-72 space-y-1 overflow-y-auto pr-1"
|
||||
data-testid="community-compute-deployment-list"
|
||||
>
|
||||
{visibleDeployments.map((deployment) => {
|
||||
const selected = deployment.id === activeId;
|
||||
return (
|
||||
<li key={deployment.id}>
|
||||
<button
|
||||
aria-pressed={selected}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 rounded-lg border border-transparent px-2.5 py-2 text-left text-xs transition-colors hover:bg-muted/60 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
|
||||
selected && "border-border bg-muted/70 text-foreground",
|
||||
)}
|
||||
data-testid={`community-compute-deployment-${testId(deployment.id)}`}
|
||||
onClick={() => onSelect(deployment.id)}
|
||||
type="button"
|
||||
>
|
||||
<HealthGlyph health={deployment.health} />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate font-medium">
|
||||
{shortModelName(deployment.modelId)}
|
||||
</span>
|
||||
<span className="block truncate text-2xs text-muted-foreground">
|
||||
{deployment.deviceLabel}
|
||||
</span>
|
||||
</span>
|
||||
<span className="shrink-0 text-2xs capitalize text-muted-foreground">
|
||||
{deployment.health}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeploymentDetails({
|
||||
deployment,
|
||||
}: {
|
||||
deployment: CommunityComputeDeployment;
|
||||
}) {
|
||||
return (
|
||||
<article
|
||||
aria-live="polite"
|
||||
className="min-w-0 rounded-lg border border-border/60 bg-background/80 p-3"
|
||||
data-testid="community-compute-deployment-details"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<HealthGlyph health={deployment.health} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3
|
||||
className="truncate text-sm font-semibold"
|
||||
title={deployment.modelId}
|
||||
>
|
||||
{shortModelName(deployment.modelId)}
|
||||
</h3>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{deployment.deviceLabel}
|
||||
{deployment.isSelf ? " · This machine" : ""}
|
||||
</p>
|
||||
<p className="mt-0.5 text-2xs capitalize text-muted-foreground">
|
||||
{deployment.health} · {deployment.cellCount}{" "}
|
||||
{deployment.cellCount === 1 ? "cell" : "cells"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{deployment.healthReason ? (
|
||||
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
|
||||
{deployment.healthReason}
|
||||
</p>
|
||||
) : null}
|
||||
<dl className="mt-3 grid grid-cols-2 gap-x-3 gap-y-2 text-xs">
|
||||
<Detail
|
||||
label="Shared memory"
|
||||
value={formatCapacity(deployment.capacityGb)}
|
||||
/>
|
||||
<Detail label="Model" value={deployment.modelId} />
|
||||
<Detail
|
||||
label="Reported"
|
||||
value={formatReportedAt(deployment.source.reportedAt)}
|
||||
/>
|
||||
<Detail
|
||||
label="Model footprint"
|
||||
value={formatCapacity(deployment.source.modelSizeGb ?? null)}
|
||||
/>
|
||||
</dl>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function Detail({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<dt className="text-2xs text-muted-foreground">{label}</dt>
|
||||
<dd className="mt-0.5 truncate font-medium" title={value}>
|
||||
{value}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HealthGlyph({ health }: { health: CommunityComputeDeploymentHealth }) {
|
||||
const cue = healthCue(health);
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"flex size-4 shrink-0 items-center justify-center rounded-full border border-foreground/45 text-3xs font-bold leading-none",
|
||||
health === "warming" &&
|
||||
"animate-pulse border-dashed motion-reduce:animate-none",
|
||||
health === "idle" && "border-dotted text-muted-foreground",
|
||||
health === "degraded" && "border-dashed",
|
||||
health === "offline" &&
|
||||
"border-muted-foreground/60 text-muted-foreground",
|
||||
health === "unknown" && "border-dotted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{cue.glyph}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyHexes() {
|
||||
const centers = [
|
||||
{ x: 2, y: 1.75 },
|
||||
@@ -734,85 +319,16 @@ function computeViewBox(
|
||||
};
|
||||
}
|
||||
|
||||
function healthCue(health: CommunityComputeDeploymentHealth): {
|
||||
dashArray?: string;
|
||||
glyph: string;
|
||||
strokeWidth: number;
|
||||
} {
|
||||
switch (health) {
|
||||
case "healthy":
|
||||
return { glyph: "✓", strokeWidth: 0.075 };
|
||||
case "warming":
|
||||
return { glyph: "↻", dashArray: "0.2 0.12", strokeWidth: 0.085 };
|
||||
case "idle":
|
||||
return { glyph: "–", dashArray: "0.04 0.14", strokeWidth: 0.08 };
|
||||
case "degraded":
|
||||
return { glyph: "!", dashArray: "0.3 0.12", strokeWidth: 0.11 };
|
||||
case "offline":
|
||||
return { glyph: "×", dashArray: "0.08 0.1", strokeWidth: 0.075 };
|
||||
case "unknown":
|
||||
return { glyph: "?", dashArray: "0.03 0.12", strokeWidth: 0.075 };
|
||||
}
|
||||
}
|
||||
|
||||
function stableVisualHash(value: string): number {
|
||||
let hash = 2166136261;
|
||||
for (const character of value) {
|
||||
hash ^= character.codePointAt(0) ?? 0;
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
function shortModelName(modelId: string): string {
|
||||
const tail = modelId.split("/").at(-1) ?? modelId;
|
||||
return tail.length > 34 ? `${tail.slice(0, 31)}…` : tail;
|
||||
}
|
||||
|
||||
type ContributorProfiles = Record<
|
||||
string,
|
||||
{ avatarUrl: string | null; displayName: string | null }
|
||||
>;
|
||||
|
||||
function contributorProfile(
|
||||
pubkey: string | null | undefined,
|
||||
profiles: ContributorProfiles,
|
||||
) {
|
||||
return pubkey ? profiles[pubkey.trim().toLowerCase()] : undefined;
|
||||
}
|
||||
|
||||
function contributorAvatarUrl(
|
||||
pubkey: string | null | undefined,
|
||||
profiles: ContributorProfiles,
|
||||
): string | null {
|
||||
const avatarUrl = contributorProfile(pubkey, profiles)?.avatarUrl ?? null;
|
||||
const snapshotUrl = getAvatarSnapshotUrl(avatarUrl);
|
||||
return snapshotUrl ? rewriteRelayUrl(snapshotUrl) : null;
|
||||
}
|
||||
|
||||
function contributorLabel(
|
||||
deployment: CommunityComputeDeployment,
|
||||
profiles: ContributorProfiles,
|
||||
): string {
|
||||
return (
|
||||
contributorProfile(deployment.source.memberPubkey, profiles)?.displayName ??
|
||||
(deployment.isSelf ? "You" : deployment.deviceLabel)
|
||||
);
|
||||
}
|
||||
|
||||
function contributorInitials(label: string): string {
|
||||
const words = label.trim().split(/\s+/).filter(Boolean);
|
||||
return words
|
||||
.slice(0, 2)
|
||||
.map((word) => word[0]?.toUpperCase() ?? "")
|
||||
.join("");
|
||||
}
|
||||
|
||||
function territoryAccessibleLabel(
|
||||
deployment: CommunityComputeDeployment,
|
||||
): string {
|
||||
const capacity = formatCapacity(deployment.capacityGb);
|
||||
return `${deployment.modelId} on ${deployment.deviceLabel}, ${deployment.health}, ${capacity}, ${deployment.cellCount} ${deployment.cellCount === 1 ? "cell" : "cells"}`;
|
||||
return `${deployment.modelId} on ${deployment.deviceLabel}, ${capacity}`;
|
||||
}
|
||||
|
||||
function formatCapacity(value: number | null): string {
|
||||
@@ -821,12 +337,6 @@ function formatCapacity(value: number | null): string {
|
||||
: `${Math.round(value)} GB shared`;
|
||||
}
|
||||
|
||||
function formatReportedAt(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) return "Not reported";
|
||||
const milliseconds = value < 10_000_000_000 ? value * 1000 : value;
|
||||
return new Date(milliseconds).toLocaleString();
|
||||
}
|
||||
|
||||
function testId(value: string): string {
|
||||
return encodeURIComponent(value).replaceAll("%", "-");
|
||||
}
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import { ArrowLeft, ArrowRight, FlaskConical, X } from "lucide-react";
|
||||
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import type { CommunityComputeTutorialFixtureId } from "../communityComputeFixtures";
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
title: "Every territory is a model deployment",
|
||||
body: "Each connected group is one ready model on one contributed device. If one device serves multiple models, each model gets its own territory; another device serving the same model appears separately.",
|
||||
},
|
||||
{
|
||||
title: "Area represents memory",
|
||||
body: "Models snap to four coarse footprints: 1, 2, 4, or 7 cells. The footprint comes from reported model size, or shared device capacity when model size is unavailable.",
|
||||
},
|
||||
{
|
||||
title: "Motion illustrates inference in flight",
|
||||
body: "For this tutorial, breathing territories simulate models handling requests. The live community snapshot cannot yet identify the deployment serving a request, so the live map remains static.",
|
||||
},
|
||||
{
|
||||
title: "The headline measures the pool",
|
||||
body: "Members sharing counts contributors, contributed VRAM is the memory they offered, and VRAM allocated is the reported model footprint—not GPU processing utilization.",
|
||||
},
|
||||
{
|
||||
title: "Health summarizes reported state",
|
||||
body: "The scorecard counts ready, warming, and idle deployments from current status reports. It does not infer utilization, offline devices, replica resilience, or capacity headroom.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function CommunityComputeTutorialPanel({
|
||||
fixtureId,
|
||||
onExit,
|
||||
onFixtureChange,
|
||||
onStepChange,
|
||||
step,
|
||||
}: {
|
||||
fixtureId: CommunityComputeTutorialFixtureId;
|
||||
onExit: () => void;
|
||||
onFixtureChange: (fixture: CommunityComputeTutorialFixtureId) => void;
|
||||
onStepChange: (step: number) => void;
|
||||
step: number;
|
||||
}) {
|
||||
const current = STEPS[step] ?? STEPS[0];
|
||||
const showDeveloperControls =
|
||||
import.meta.env.DEV || import.meta.env.MODE === "e2e";
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="mesh-tutorial-title"
|
||||
className="rounded-2xl border border-foreground/20 bg-foreground/[0.035] p-4"
|
||||
data-testid="community-compute-tutorial-panel"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-2xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Tutorial · {step + 1} of {STEPS.length}
|
||||
</p>
|
||||
<h2 className="mt-1 text-base font-semibold" id="mesh-tutorial-title">
|
||||
{current.title}
|
||||
</h2>
|
||||
<p className="mt-1 max-w-3xl text-sm leading-relaxed text-muted-foreground">
|
||||
{current.body}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Exit mesh tutorial"
|
||||
onClick={onExit}
|
||||
size="icon-xs"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
disabled={step === 0}
|
||||
onClick={() => onStepChange(step - 1)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowLeft /> Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
step === STEPS.length - 1 ? onStepChange(0) : onStepChange(step + 1)
|
||||
}
|
||||
size="sm"
|
||||
type="button"
|
||||
>
|
||||
{step === STEPS.length - 1 ? "Replay" : "Next"}
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
<fieldset className="ml-1 flex gap-1" aria-label="Tutorial progress">
|
||||
{STEPS.map((item, index) => (
|
||||
<button
|
||||
aria-label={`Go to tutorial step ${index + 1}: ${item.title}`}
|
||||
className={cn(
|
||||
"size-2 rounded-full bg-muted-foreground/25 transition-colors",
|
||||
index === step && "bg-foreground",
|
||||
)}
|
||||
key={item.title}
|
||||
onClick={() => onStepChange(index)}
|
||||
type="button"
|
||||
/>
|
||||
))}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
{showDeveloperControls ? (
|
||||
<div
|
||||
className="mt-4 flex flex-wrap items-center gap-2 border-border/60 border-t pt-3"
|
||||
data-testid="community-compute-dev-controls"
|
||||
>
|
||||
<FlaskConical className="size-3.5 text-muted-foreground" />
|
||||
<label
|
||||
className="text-xs font-medium"
|
||||
htmlFor="mesh-tutorial-scenario"
|
||||
>
|
||||
Preview scenario
|
||||
</label>
|
||||
<select
|
||||
className="h-8 rounded-lg border border-input/50 bg-background px-2 text-xs focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
|
||||
id="mesh-tutorial-scenario"
|
||||
onChange={(event) =>
|
||||
onFixtureChange(
|
||||
event.target.value as CommunityComputeTutorialFixtureId,
|
||||
)
|
||||
}
|
||||
value={fixtureId}
|
||||
>
|
||||
<option value="empty">Empty community</option>
|
||||
<option value="single-device">Single device</option>
|
||||
<option value="small-company">Small company</option>
|
||||
<option value="large-dense-500">500 deployments</option>
|
||||
</select>
|
||||
<span className="text-2xs text-muted-foreground">
|
||||
Development-only · deterministic frontend data
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
import { Info, RefreshCw } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
import { useUsersBatchQuery } from "@/features/profile/hooks";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import type { MeshSnapshot } from "@/shared/api/tauriMesh";
|
||||
import {
|
||||
deriveCommunityComputeMapModel,
|
||||
type CommunityComputeSnapshotInput,
|
||||
} from "../communityComputeMapModel";
|
||||
import { CommunityComputeHealthScorecard } from "./CommunityComputeHealthScorecard";
|
||||
import { CommunityComputeKpiRow } from "./CommunityComputeKpiRow";
|
||||
import { CommunityComputeTerritoryMap } from "./CommunityComputeTerritoryMap";
|
||||
|
||||
@@ -16,53 +13,19 @@ export function MeshComputeCommunityView({
|
||||
communityName,
|
||||
error,
|
||||
onRefresh,
|
||||
simulated,
|
||||
snapshot,
|
||||
tutorialInference = false,
|
||||
}: {
|
||||
communityName: string;
|
||||
error: string | null;
|
||||
onRefresh: () => void;
|
||||
simulated: boolean;
|
||||
snapshot: MeshSnapshot | CommunityComputeSnapshotInput | null;
|
||||
tutorialInference?: boolean;
|
||||
}) {
|
||||
const model = deriveCommunityComputeMapModel(snapshot);
|
||||
const inferenceDeploymentIds = tutorialInference
|
||||
? model.deployments
|
||||
.filter(
|
||||
(deployment, index) =>
|
||||
deployment.cellCount > 1 || index % 3 === 0 || index % 7 === 2,
|
||||
)
|
||||
.map((deployment) => deployment.id)
|
||||
: [];
|
||||
const observedAt = (snapshot as MeshSnapshot | null)?.observedAt;
|
||||
const contributorPubkeys = React.useMemo(
|
||||
() =>
|
||||
model.deployments
|
||||
.map((deployment) => deployment.source.memberPubkey?.trim() ?? "")
|
||||
.filter(Boolean),
|
||||
[model.deployments],
|
||||
);
|
||||
const contributorProfiles =
|
||||
useUsersBatchQuery(contributorPubkeys).data?.profiles ?? {};
|
||||
|
||||
return (
|
||||
<div className="space-y-5" data-testid="community-compute-view">
|
||||
{simulated ? (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-xl border border-dashed border-foreground/30 bg-foreground/[0.035] px-3 py-2 text-xs"
|
||||
data-testid="community-compute-simulated-banner"
|
||||
>
|
||||
<Info className="size-3.5" />
|
||||
<span>
|
||||
Simulated mesh for learning and development. Live community data is
|
||||
unchanged.
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error && !simulated ? (
|
||||
{error ? (
|
||||
<div className="flex items-center justify-between gap-3 rounded-xl border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
||||
<span>
|
||||
Couldn’t load {communityName} compute: {error}
|
||||
@@ -81,51 +44,32 @@ export function MeshComputeCommunityView({
|
||||
<h2 className="text-lg font-semibold tracking-tight">
|
||||
{model.deployments.length === 1
|
||||
? "Your community’s first compute node"
|
||||
: "Model mesh"}
|
||||
: "Community mesh"}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{model.deployments.length === 1
|
||||
? "This machine is the foundation of the mesh. More contributors add capacity, model choice, and resilience."
|
||||
: "Each territory is one ready model on one device. Area reflects its reported model-memory tier."}
|
||||
: "See the compute currently available to your community. Larger territories offer more model memory."}
|
||||
</p>
|
||||
</div>
|
||||
{!simulated ? (
|
||||
<Button
|
||||
aria-label="Refresh community compute"
|
||||
onClick={onRefresh}
|
||||
size="icon-xs"
|
||||
title="Refresh community compute"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<RefreshCw />
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
aria-label="Refresh community compute"
|
||||
onClick={onRefresh}
|
||||
size="icon-xs"
|
||||
title="Refresh community compute"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<RefreshCw />
|
||||
</Button>
|
||||
</div>
|
||||
<CommunityComputeTerritoryMap
|
||||
contributorProfiles={contributorProfiles}
|
||||
inferenceDeploymentIds={inferenceDeploymentIds}
|
||||
model={model}
|
||||
/>
|
||||
<CommunityComputeTerritoryMap model={model} />
|
||||
<p className="mt-2 text-2xs text-muted-foreground">
|
||||
{simulated
|
||||
? "Deterministic tutorial snapshot"
|
||||
: observedAt
|
||||
? `Recently reported · updated ${formatAge(observedAt)}`
|
||||
: "Recently reported by community members · presence may take up to two minutes to expire"}
|
||||
{observedAt
|
||||
? `Recently reported · updated ${formatAge(observedAt)}`
|
||||
: "Recently reported by community members · presence may take up to two minutes to expire"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<h2 className="text-lg font-semibold tracking-tight">Mesh health</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Current ready, warming, and idle states from community status
|
||||
reports.
|
||||
</p>
|
||||
</div>
|
||||
<CommunityComputeHealthScorecard health={model.health} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Activity, Cpu, Network, Sparkles } from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import type { useMeshComputeState } from "../hooks/useMeshComputeState";
|
||||
|
||||
export function MeshComputeImpactReceipt({
|
||||
mesh,
|
||||
}: {
|
||||
mesh: ReturnType<typeof useMeshComputeState>;
|
||||
}) {
|
||||
const { memory, toggle, usage } = mesh;
|
||||
const isSharing = toggle.isSharing;
|
||||
const tokensProcessed = usage?.tokensServed ?? 0;
|
||||
const peers = usage?.peers ?? 0;
|
||||
const capacity = memory.totalGb == null ? "—" : formatGb(memory.totalGb);
|
||||
|
||||
const items = [
|
||||
{
|
||||
icon: Cpu,
|
||||
label: "Capacity offered",
|
||||
value: isSharing ? capacity : "Not sharing",
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
label: "Tokens served",
|
||||
value: isSharing ? formatCount(tokensProcessed) : "—",
|
||||
},
|
||||
{
|
||||
icon: Network,
|
||||
label: "Mesh peers",
|
||||
value: isSharing ? formatCount(peers) : "—",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"overflow-hidden rounded-2xl border border-border/70 bg-background",
|
||||
!isSharing && "bg-muted/10",
|
||||
)}
|
||||
data-testid="compute-impact-receipt"
|
||||
>
|
||||
<div className="flex items-start gap-3 border-border/60 border-b px-4 py-3">
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-foreground text-background">
|
||||
<Activity className="size-4" />
|
||||
</span>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">Your contribution</h2>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{isSharing
|
||||
? usage?.inflight
|
||||
? `Helping run ${usage.inflight} request${usage.inflight === 1 ? "" : "s"} right now.`
|
||||
: "Available when your community needs more compute."
|
||||
: "Turn on automatic contribution to start building your impact."}
|
||||
</p>
|
||||
</div>
|
||||
{isSharing ? (
|
||||
<span className="ml-auto flex shrink-0 items-center gap-1.5 text-xs font-medium">
|
||||
<span className="size-2 rounded-full bg-emerald-500" /> Live
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<dl className="grid grid-cols-3 divide-x divide-border/60">
|
||||
{items.map(({ icon: Icon, label, value }) => (
|
||||
<div className="min-w-0 px-4 py-3" key={label}>
|
||||
<dt className="flex items-center gap-1.5 truncate text-2xs text-muted-foreground">
|
||||
<Icon className="size-3" /> {label}
|
||||
</dt>
|
||||
<dd className="mt-1 truncate text-lg font-semibold tabular-nums">
|
||||
{value}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<p className="border-border/60 border-t px-4 py-2 text-2xs text-muted-foreground">
|
||||
Session totals from this machine. Request attribution to other members
|
||||
is coming soon.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function formatCount(value: number): string {
|
||||
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
function formatGb(value: number): string {
|
||||
return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(value)} GB`;
|
||||
}
|
||||
@@ -1,67 +1,28 @@
|
||||
import * as React from "react";
|
||||
import { BookOpen } from "lucide-react";
|
||||
|
||||
import { useCommunities } from "@/features/communities/useCommunities";
|
||||
import { SettingsSectionHeader } from "@/features/settings/ui/SettingsSectionHeader";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
||||
import {
|
||||
COMMUNITY_COMPUTE_TUTORIAL_FIXTURES,
|
||||
type CommunityComputeTutorialFixtureId,
|
||||
} from "../communityComputeFixtures";
|
||||
import { useMeshComputeState } from "../hooks/useMeshComputeState";
|
||||
import { CommunityComputeTutorialPanel } from "./CommunityComputeTutorialPanel";
|
||||
import { MeshComputeCommunityView } from "./MeshComputeCommunityView";
|
||||
import { MeshComputeImpactReceipt } from "./MeshComputeImpactReceipt";
|
||||
import { MeshComputeSettingsCard } from "./MeshComputeSettingsCard";
|
||||
import { MeshComputeShareBanner } from "./MeshComputeShareBanner";
|
||||
import { MeshComputeTokenLeaderboard } from "./MeshComputeTokenLeaderboard";
|
||||
|
||||
export function MeshComputePage() {
|
||||
const { activeCommunity } = useCommunities();
|
||||
const mesh = useMeshComputeState();
|
||||
const [tab, setTab] = React.useState("community");
|
||||
const [tutorialOpen, setTutorialOpen] = React.useState(false);
|
||||
const [tutorialStep, setTutorialStep] = React.useState(0);
|
||||
const [fixtureId, setFixtureId] =
|
||||
React.useState<CommunityComputeTutorialFixtureId>("small-company");
|
||||
const communityName = activeCommunity?.name ?? "this community";
|
||||
const tutorialFixture = COMMUNITY_COMPUTE_TUTORIAL_FIXTURES[fixtureId];
|
||||
|
||||
function toggleTutorial() {
|
||||
if (tutorialOpen) {
|
||||
setTutorialOpen(false);
|
||||
return;
|
||||
}
|
||||
setTab("community");
|
||||
setTutorialOpen(true);
|
||||
setTutorialStep(0);
|
||||
setFixtureId("small-company");
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="min-w-0" data-testid="settings-mesh-compute-page">
|
||||
<SettingsSectionHeader
|
||||
action={
|
||||
<Button
|
||||
aria-pressed={tutorialOpen}
|
||||
data-testid="community-compute-tutorial-button"
|
||||
onClick={toggleTutorial}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={tutorialOpen ? "secondary" : "outline"}
|
||||
>
|
||||
<BookOpen /> Tutorial
|
||||
</Button>
|
||||
}
|
||||
description={`Contribute this machine’s spare capacity to unlock more models and keep ${communityName}’s agents available when demand grows.`}
|
||||
title="Compute"
|
||||
/>
|
||||
|
||||
<div className="space-y-5">
|
||||
{tutorialOpen ? null : (
|
||||
<MeshComputeShareBanner communityName={communityName} mesh={mesh} />
|
||||
)}
|
||||
<MeshComputeShareBanner communityName={communityName} mesh={mesh} />
|
||||
|
||||
<Tabs onValueChange={setTab} value={tab}>
|
||||
<TabsList className="grid w-full grid-cols-2 sm:w-72">
|
||||
@@ -74,27 +35,11 @@ export function MeshComputePage() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent className="mt-5 space-y-5" value="community">
|
||||
{tutorialOpen ? (
|
||||
<CommunityComputeTutorialPanel
|
||||
fixtureId={fixtureId}
|
||||
onExit={() => setTutorialOpen(false)}
|
||||
onFixtureChange={setFixtureId}
|
||||
onStepChange={setTutorialStep}
|
||||
step={tutorialStep}
|
||||
/>
|
||||
) : (
|
||||
<MeshComputeImpactReceipt mesh={mesh} />
|
||||
)}
|
||||
<MeshComputeTokenLeaderboard mesh={mesh} simulated={tutorialOpen} />
|
||||
<MeshComputeCommunityView
|
||||
communityName={
|
||||
tutorialOpen ? tutorialFixture.title : communityName
|
||||
}
|
||||
communityName={communityName}
|
||||
error={mesh.error}
|
||||
onRefresh={mesh.refreshSnapshot}
|
||||
simulated={tutorialOpen}
|
||||
snapshot={tutorialOpen ? tutorialFixture.snapshot : mesh.snapshot}
|
||||
tutorialInference={tutorialOpen}
|
||||
snapshot={mesh.snapshot}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { Medal, Trophy } from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import type { useMeshComputeState } from "../hooks/useMeshComputeState";
|
||||
|
||||
const PREVIEW_LEADERS = [
|
||||
{ name: "Maya", tokens: 2_840_912 },
|
||||
{ name: "Jon", tokens: 1_967_401 },
|
||||
{ name: "Priya", tokens: 1_204_885 },
|
||||
] as const;
|
||||
|
||||
export function MeshComputeTokenLeaderboard({
|
||||
mesh,
|
||||
simulated = false,
|
||||
}: {
|
||||
mesh: ReturnType<typeof useMeshComputeState>;
|
||||
simulated?: boolean;
|
||||
}) {
|
||||
const yourTokens = simulated ? 1_638_240 : (mesh.usage?.tokensServed ?? 0);
|
||||
const communityLeaders = simulated ? PREVIEW_LEADERS : [];
|
||||
const rows = [...communityLeaders, { name: "You", tokens: yourTokens }]
|
||||
.sort((a, b) => b.tokens - a.tokens)
|
||||
.map((entry, index) => ({ ...entry, rank: index + 1 }));
|
||||
const yourRank =
|
||||
rows.find((entry) => entry.name === "You")?.rank ?? rows.length;
|
||||
|
||||
return (
|
||||
<section
|
||||
className="overflow-hidden rounded-2xl border border-border/70 bg-background"
|
||||
data-testid="compute-token-leaderboard"
|
||||
>
|
||||
<div className="flex items-start gap-3 border-border/60 border-b px-4 py-3.5">
|
||||
<span className="flex size-9 shrink-0 items-center justify-center rounded-xl bg-amber-500/15 text-amber-700 dark:text-amber-300">
|
||||
<Trophy className="size-4" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-sm font-semibold">
|
||||
Community token contributors
|
||||
</h2>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{simulated
|
||||
? "Tokenmaxxing, inverted: celebrate the tokens you serve—not the tokens you consume."
|
||||
: "Your locally reported total. Community rankings will appear when aggregate reporting is available."}
|
||||
</p>
|
||||
</div>
|
||||
{simulated ? (
|
||||
<div className="shrink-0 text-right">
|
||||
<p className="text-2xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Your rank
|
||||
</p>
|
||||
<p className="mt-0.5 text-lg font-semibold tabular-nums">
|
||||
#{yourRank}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<ol className="divide-y divide-border/60">
|
||||
{rows.map((entry) => (
|
||||
<li
|
||||
className={cn(
|
||||
"grid grid-cols-[2.5rem_1fr_auto] items-center gap-2 px-4 py-2.5",
|
||||
entry.name === "You" && "bg-foreground/[0.045]",
|
||||
)}
|
||||
key={entry.name}
|
||||
>
|
||||
<span className="flex items-center gap-1 text-xs font-semibold tabular-nums text-muted-foreground">
|
||||
{entry.rank <= 3 ? <Medal className="size-3.5" /> : null}#
|
||||
{entry.rank}
|
||||
</span>
|
||||
<span className="truncate text-sm font-medium">
|
||||
{entry.name}
|
||||
{entry.name === "You" ? (
|
||||
<span className="ml-2 rounded-full bg-foreground px-2 py-0.5 text-3xs font-semibold text-background">
|
||||
YOU
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
{formatTokens(entry.tokens)}
|
||||
<span className="ml-1 text-2xs font-normal text-muted-foreground">
|
||||
tokens
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<p className="border-border/60 border-t bg-muted/10 px-4 py-2 text-2xs text-muted-foreground">
|
||||
{simulated
|
||||
? "Tutorial preview · example contribution totals"
|
||||
: "Community rankings preview · aggregate contribution reporting is coming soon."}
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTokens(value: number): string {
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
notation: value >= 10_000 ? "compact" : "standard",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(value);
|
||||
}
|
||||
@@ -53,18 +53,10 @@ export default {
|
||||
"0%, 100%": { opacity: "1" },
|
||||
"50%": { opacity: "0.25" },
|
||||
},
|
||||
"mesh-breath": {
|
||||
"0%, 100%": { fill: "hsl(var(--foreground))" },
|
||||
"50%": {
|
||||
fill: "color-mix(in oklab, hsl(var(--foreground)) 18%, #ffd84d)",
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
// Work in flight right now.
|
||||
"mesh-activity": "mesh-dim 1s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
||||
"mesh-inference":
|
||||
"mesh-breath var(--mesh-breath-duration, 4.8s) ease-in-out infinite",
|
||||
// A start or stop in flight. Slower: nothing is wrong, and nothing needs
|
||||
// a decision.
|
||||
"mesh-progress": "mesh-dim 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
||||
|
||||
@@ -4,33 +4,80 @@ import { installMockBridge } from "../helpers/bridge";
|
||||
import { openSettings } from "../helpers/settings";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
|
||||
test("captures the Community Compute tutorial", async ({ page }) => {
|
||||
type E2eWindow = Window & {
|
||||
__BUZZ_E2E_SET_MESH__?: (mesh: {
|
||||
snapshotMemberCount?: number;
|
||||
snapshotDevices?: Array<{
|
||||
deviceId: string;
|
||||
label: string;
|
||||
capacityGb: number;
|
||||
models: string[];
|
||||
state: "serving";
|
||||
isSelf: boolean;
|
||||
memberPubkey?: string;
|
||||
modelSizeGb?: number;
|
||||
}>;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
test("captures the focused Community Compute topology", async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
await page.waitForFunction(
|
||||
() => typeof (window as E2eWindow).__BUZZ_E2E_SET_MESH__ === "function",
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
(window as E2eWindow).__BUZZ_E2E_SET_MESH__?.({
|
||||
snapshotMemberCount: 18,
|
||||
snapshotDevices: [
|
||||
{
|
||||
deviceId: "design-studio",
|
||||
label: "Design Studio",
|
||||
capacityGb: 64,
|
||||
models: ["qwen-32b"],
|
||||
state: "serving",
|
||||
isSelf: true,
|
||||
memberPubkey: "design-member",
|
||||
modelSizeGb: 28,
|
||||
},
|
||||
{
|
||||
deviceId: "engineering-workstation",
|
||||
label: "Engineering Workstation",
|
||||
capacityGb: 128,
|
||||
models: ["gemma-27b"],
|
||||
state: "serving",
|
||||
isSelf: false,
|
||||
memberPubkey: "engineering-member",
|
||||
modelSizeGb: 64,
|
||||
},
|
||||
{
|
||||
deviceId: "research-host",
|
||||
label: "Research Host",
|
||||
capacityGb: 512,
|
||||
models: ["community/research-140b"],
|
||||
state: "serving",
|
||||
isSelf: false,
|
||||
memberPubkey: "research-member",
|
||||
modelSizeGb: 350,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
await openSettings(page, "compute");
|
||||
await page.getByTestId("community-compute-tutorial-button").click();
|
||||
await expect(
|
||||
page.getByTestId("community-compute-territory-map"),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("compute-share-banner")).toHaveCount(0);
|
||||
await waitForAnimations(page);
|
||||
await page.getByTestId("settings-panel-compute").screenshot({
|
||||
path: "test-results/mesh-compute/community-tutorial.png",
|
||||
});
|
||||
await page.getByTestId("community-compute-territory-map").screenshot({
|
||||
path: "test-results/mesh-compute/community-map.png",
|
||||
path: "test-results/mesh-compute/community-compute.png",
|
||||
});
|
||||
|
||||
await page.getByLabel("Preview scenario").selectOption("large-dense-500");
|
||||
await page.locator('[data-deployment-id*="research-host"]').hover();
|
||||
await expect(
|
||||
page.getByTestId("community-compute-kpi-members-sharing"),
|
||||
).toContainText("458");
|
||||
await expect(page.locator("[data-cell-count]")).toHaveCount(458);
|
||||
await expect(
|
||||
page.locator('[data-inference-active="true"]').first(),
|
||||
).toBeVisible();
|
||||
page.getByTestId("community-compute-hover-details"),
|
||||
).toContainText("Research Host");
|
||||
await waitForAnimations(page);
|
||||
await page.getByTestId("settings-panel-compute").screenshot({
|
||||
path: "test-results/mesh-compute/community-dense.png",
|
||||
await page.getByTestId("community-compute-territory-map").screenshot({
|
||||
path: "test-results/mesh-compute/community-map-hover.png",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,13 +166,12 @@ test("Shared Compute is only available from Settings", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("sidebar-mesh-compute-row")).toHaveCount(0);
|
||||
await page.getByTestId("open-settings").click();
|
||||
await page.getByTestId("settings-nav-compute").click();
|
||||
await openSettings(page, "compute");
|
||||
|
||||
await expect(page.getByTestId("settings-mesh-compute-page")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Community Compute shows live KPIs and the tutorial simulation", async ({
|
||||
test("Community Compute keeps the live community topology focused", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page);
|
||||
@@ -222,66 +221,26 @@ test("Community Compute shows live KPIs and the tutorial simulation", async ({
|
||||
await expect(
|
||||
page.getByTestId("community-compute-territory-map"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("community-compute-deployment-list"),
|
||||
).toContainText("Alpha Studio");
|
||||
await page.getByLabel("Search deployments").fill("Beta");
|
||||
await expect(
|
||||
page.getByTestId("community-compute-deployment-list"),
|
||||
).toContainText("Beta Workstation");
|
||||
await expect(
|
||||
page.getByTestId("community-compute-deployment-list"),
|
||||
).not.toContainText("Alpha Studio");
|
||||
await page.getByLabel("Search deployments").fill("");
|
||||
|
||||
const tutorialButton = page.getByTestId("community-compute-tutorial-button");
|
||||
await tutorialButton.click();
|
||||
await expect(tutorialButton).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(
|
||||
page.getByTestId("community-compute-simulated-banner"),
|
||||
).toBeVisible();
|
||||
page.getByTestId("community-compute-tutorial-button"),
|
||||
).toHaveCount(0);
|
||||
await expect(page.getByTestId("compute-token-leaderboard")).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId("community-compute-tutorial-panel"),
|
||||
).toContainText("Every territory is a model deployment");
|
||||
await expect(page.getByTestId("compute-share-banner")).toHaveCount(0);
|
||||
const tutorialTile = page
|
||||
.locator('[data-inference-active="true"]')
|
||||
.first()
|
||||
.getByTestId("community-compute-tile")
|
||||
.first();
|
||||
await expect(tutorialTile).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
tutorialTile.evaluate(
|
||||
(element) => getComputedStyle(element).animationName,
|
||||
),
|
||||
)
|
||||
.toBe("mesh-breath");
|
||||
await page.getByLabel("Preview scenario").selectOption("single-device");
|
||||
page.getByTestId("community-compute-deployment-list"),
|
||||
).toHaveCount(0);
|
||||
await expect(page.getByText("Mesh health", { exact: true })).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId("community-compute-kpi-members-sharing"),
|
||||
).toContainText("1");
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await expect(
|
||||
page.getByTestId("community-compute-tutorial-panel"),
|
||||
).toContainText("Area represents memory");
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await expect(
|
||||
page.getByTestId("community-compute-tutorial-panel"),
|
||||
).toContainText("Motion illustrates inference in flight");
|
||||
page.getByTestId("community-compute-contributor-avatar"),
|
||||
).toHaveCount(0);
|
||||
await expect(page.locator('[data-inference-active="true"]')).toHaveCount(0);
|
||||
|
||||
const territory = page.locator("[data-cell-count]").first();
|
||||
await territory.click();
|
||||
await expect(territory).toHaveAttribute("data-selected", "true");
|
||||
await territory.hover();
|
||||
await expect(
|
||||
page.getByTestId("community-compute-hover-details"),
|
||||
).toContainText("Simulated inference");
|
||||
await tutorialButton.click();
|
||||
await expect(tutorialButton).toHaveAttribute("aria-pressed", "false");
|
||||
).toContainText("Alpha Studio");
|
||||
await expect(
|
||||
page.getByTestId("community-compute-simulated-banner"),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId("community-compute-kpi-members-sharing"),
|
||||
).toContainText("2");
|
||||
page.getByTestId("community-compute-hover-details"),
|
||||
).toContainText("Available to the community");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user