mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
compute view mesh map
Signed-off-by: Alessandro Joabar <sandro@squareup.com>
This commit is contained in:
@@ -33,6 +33,7 @@ export default defineConfig({
|
||||
"**/add-community-screenshots.spec.ts",
|
||||
"**/hosted-communities-settings-screenshots.spec.ts",
|
||||
"**/invites-settings-screenshots.spec.ts",
|
||||
"**/mesh-compute-screenshots.spec.ts",
|
||||
"**/messaging.spec.ts",
|
||||
"**/message-feedback-snapshots.spec.ts",
|
||||
"**/custom-emoji.spec.ts",
|
||||
|
||||
@@ -218,7 +218,9 @@ async fn query_mesh_discovery_events_at(
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_mesh_discovery_events(state: &AppState) -> Result<Vec<nostr::Event>, String> {
|
||||
pub(super) async fn query_mesh_discovery_events(
|
||||
state: &AppState,
|
||||
) -> Result<Vec<nostr::Event>, String> {
|
||||
query_mesh_discovery_events_at(state, &relay::relay_ws_url_with_override(state)).await
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,12 @@ type CmdResult<T> = Result<T, String>;
|
||||
/// state. A genuine relay/transport failure still returns `Err`.
|
||||
#[tauri::command]
|
||||
pub async fn mesh_snapshot(state: State<'_, AppState>) -> CmdResult<mesh_llm::MeshSnapshot> {
|
||||
let events = crate::relay::query_relay(
|
||||
&state,
|
||||
&[
|
||||
mesh_llm::mesh_status_filter(),
|
||||
mesh_llm::relay_membership_filter(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.map_err(|error| format!("Shared compute status query failed: {error}"))?;
|
||||
// Reuse discovery's author-scoped, composite-cursor pagination. A direct
|
||||
// two-filter query stops at the status filter's 100-event page size and
|
||||
// silently undercounts larger communities.
|
||||
let events = super::mesh_llm::query_mesh_discovery_events(&state)
|
||||
.await
|
||||
.map_err(|error| format!("Shared compute status query failed: {error}"))?;
|
||||
|
||||
// Identify this member's own device so the card can say "including yours".
|
||||
// A missing/locked identity is not fatal here — the snapshot is still
|
||||
|
||||
@@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::discovery::{
|
||||
device_name_from_status, endpoint_binding_is_valid, endpoint_id_from_status,
|
||||
latest_membership_list, owner_id_from_status_event, status_is_fresh,
|
||||
latest_membership_list, owner_id_from_status_event, status_is_fresh, STATUS_FRESHNESS_SECS,
|
||||
};
|
||||
use super::{MeshServeTarget, MESH_STATUS_KIND};
|
||||
|
||||
@@ -62,6 +62,13 @@ pub struct MeshSnapshotDevice {
|
||||
pub state: MeshDeviceState,
|
||||
/// Whether this is the local member's own device.
|
||||
pub is_self: bool,
|
||||
/// Community member who signed this status note. Safe for profile lookup;
|
||||
/// endpoint tokens, owner signatures, and raw hardware remain excluded.
|
||||
pub member_pubkey: Option<String>,
|
||||
/// Relay event creation time, in Unix seconds.
|
||||
pub reported_at: Option<u64>,
|
||||
/// Approximate hosted-model footprint reported by MeshLLM, when available.
|
||||
pub model_size_gb: Option<f64>,
|
||||
}
|
||||
|
||||
/// What the community's shared compute looks like right now.
|
||||
@@ -70,10 +77,15 @@ pub struct MeshSnapshotDevice {
|
||||
pub struct MeshSnapshot {
|
||||
/// Devices advertising at least one routable model.
|
||||
pub sharing_device_count: usize,
|
||||
/// Unique current members with at least one ready serving device.
|
||||
pub contributor_member_count: usize,
|
||||
/// Summed shared capacity across sharing devices. `None` when no sharing
|
||||
/// device published a figure — the UI then shows a count with no GB, never
|
||||
/// a misleading `0 GB`.
|
||||
pub shared_capacity_gb: Option<f64>,
|
||||
/// Summed model footprint only when every sharing device reports it.
|
||||
/// `None` preserves mixed-version honesty instead of presenting a partial sum.
|
||||
pub allocated_capacity_gb: Option<f64>,
|
||||
/// Distinct models ready to use, deduped across devices.
|
||||
pub models: Vec<String>,
|
||||
/// Per-device rows, for the topology view. Sharing devices first.
|
||||
@@ -87,6 +99,10 @@ pub struct MeshSnapshot {
|
||||
/// note, so their hardware is unknown by design — they never consented to
|
||||
/// disclose it. Ghost nodes in the topology are a count, never GB.
|
||||
pub member_count: usize,
|
||||
/// When this projection was evaluated, in Unix seconds.
|
||||
pub observed_at: u64,
|
||||
/// Maximum age of a relay-reported serving status.
|
||||
pub freshness_seconds: u64,
|
||||
/// Why the snapshot is empty, when it is. Never a hard error: an empty
|
||||
/// mesh is a normal, expected state.
|
||||
pub reason: Option<String>,
|
||||
@@ -95,6 +111,8 @@ pub struct MeshSnapshot {
|
||||
impl MeshSnapshot {
|
||||
fn empty(reason: impl Into<String>) -> Self {
|
||||
Self {
|
||||
observed_at: nostr::Timestamp::now().as_secs(),
|
||||
freshness_seconds: STATUS_FRESHNESS_SECS,
|
||||
reason: Some(reason.into()),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -204,6 +222,9 @@ pub fn snapshot_from_events(
|
||||
state: device_state(&content, !models.is_empty()),
|
||||
models,
|
||||
is_self,
|
||||
member_pubkey: Some(event.pubkey.to_hex()),
|
||||
reported_at: Some(event.created_at.as_secs()),
|
||||
model_size_gb: f64_field(&content, "model_size_gb"),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -224,6 +245,11 @@ pub fn snapshot_from_events(
|
||||
.filter(|device| device.state == MeshDeviceState::Serving)
|
||||
.collect::<Vec<_>>();
|
||||
let sharing_device_count = sharing.len();
|
||||
let contributor_member_count = sharing
|
||||
.iter()
|
||||
.filter_map(|device| device.member_pubkey.as_ref())
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.len();
|
||||
let includes_self = sharing.iter().any(|device| device.is_self);
|
||||
// Sum only reported figures; keep `None` when nobody reported one so the
|
||||
// UI can drop the number instead of printing `0 GB`.
|
||||
@@ -231,6 +257,14 @@ pub fn snapshot_from_events(
|
||||
.iter()
|
||||
.filter_map(|device| device.capacity_gb)
|
||||
.fold(None::<f64>, |acc, gb| Some(acc.unwrap_or(0.0) + gb));
|
||||
let allocated_capacity_gb = (!sharing.is_empty()
|
||||
&& sharing.iter().all(|device| device.model_size_gb.is_some()))
|
||||
.then(|| {
|
||||
sharing
|
||||
.iter()
|
||||
.filter_map(|device| device.model_size_gb)
|
||||
.sum()
|
||||
});
|
||||
let mut models = sharing
|
||||
.iter()
|
||||
.flat_map(|device| device.models.iter().cloned())
|
||||
@@ -246,11 +280,15 @@ pub fn snapshot_from_events(
|
||||
|
||||
MeshSnapshot {
|
||||
member_count: members.len(),
|
||||
contributor_member_count,
|
||||
sharing_device_count,
|
||||
shared_capacity_gb,
|
||||
allocated_capacity_gb,
|
||||
models,
|
||||
devices,
|
||||
includes_self,
|
||||
observed_at: now,
|
||||
freshness_seconds: STATUS_FRESHNESS_SECS,
|
||||
reason,
|
||||
}
|
||||
}
|
||||
@@ -265,6 +303,8 @@ mod tests {
|
||||
assert_eq!(snapshot.sharing_device_count, 0);
|
||||
assert_eq!(snapshot.shared_capacity_gb, None);
|
||||
assert!(snapshot.reason.is_some());
|
||||
assert_eq!(snapshot.freshness_seconds, STATUS_FRESHNESS_SECS);
|
||||
assert!(snapshot.observed_at > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
234
desktop/src/features/mesh-compute/communityComputeFixtures.ts
Normal file
234
desktop/src/features/mesh-compute/communityComputeFixtures.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import type {
|
||||
CommunityComputeDeviceInput,
|
||||
CommunityComputeSnapshotInput,
|
||||
} from "./communityComputeMapModel";
|
||||
|
||||
export type CommunityComputeTutorialFixtureId =
|
||||
| "empty"
|
||||
| "single-device"
|
||||
| "small-company"
|
||||
| "large-dense-500";
|
||||
|
||||
export type CommunityComputeTutorialFixture = {
|
||||
id: CommunityComputeTutorialFixtureId;
|
||||
title: string;
|
||||
description: string;
|
||||
snapshot: CommunityComputeSnapshotInput;
|
||||
};
|
||||
|
||||
const MODEL_REFS = [
|
||||
"unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M",
|
||||
"Qwen/Qwen3.5-35B-A3B-GGUF:Q4_K_M",
|
||||
"mistralai/Ministral-3-14B-Instruct-GGUF:Q4_K_M",
|
||||
"bartowski/Llama-3.3-70B-Instruct-GGUF:Q4_K_M",
|
||||
] as const;
|
||||
|
||||
export const EMPTY_COMMUNITY_COMPUTE_FIXTURE: CommunityComputeTutorialFixture =
|
||||
{
|
||||
id: "empty",
|
||||
title: "New community",
|
||||
description: "No member has shared compute yet.",
|
||||
snapshot: {
|
||||
sharingDeviceCount: 0,
|
||||
contributorMemberCount: 0,
|
||||
sharedCapacityGb: null,
|
||||
allocatedCapacityGb: null,
|
||||
models: [],
|
||||
devices: [],
|
||||
includesSelf: false,
|
||||
memberCount: 8,
|
||||
reason: "No sharing devices found",
|
||||
},
|
||||
};
|
||||
|
||||
export const SINGLE_DEVICE_COMMUNITY_COMPUTE_FIXTURE: CommunityComputeTutorialFixture =
|
||||
{
|
||||
id: "single-device",
|
||||
title: "First deployment",
|
||||
description: "One member is sharing a single workstation.",
|
||||
snapshot: snapshotFromDevices(
|
||||
[
|
||||
device({
|
||||
index: 0,
|
||||
id: "studio-workstation",
|
||||
label: "Morgan’s Studio",
|
||||
capacityGb: 24,
|
||||
modelSizeGb: 18,
|
||||
isSelf: true,
|
||||
}),
|
||||
],
|
||||
1,
|
||||
),
|
||||
};
|
||||
|
||||
export const SMALL_COMPANY_COMMUNITY_COMPUTE_FIXTURE: CommunityComputeTutorialFixture =
|
||||
{
|
||||
id: "small-company",
|
||||
title: "Small company",
|
||||
description: "A mixed fleet across a small distributed team.",
|
||||
snapshot: snapshotFromDevices(
|
||||
[
|
||||
...[
|
||||
{ capacityGb: 24, modelSizeGb: 18 },
|
||||
{ capacityGb: 24, modelSizeGb: 18 },
|
||||
{ capacityGb: 36, modelSizeGb: 28 },
|
||||
{ capacityGb: 36, modelSizeGb: 28 },
|
||||
{ capacityGb: 48, modelSizeGb: 36 },
|
||||
{ capacityGb: 48, modelSizeGb: 36 },
|
||||
{ capacityGb: 64, modelSizeGb: 48 },
|
||||
{ capacityGb: 64, modelSizeGb: 48 },
|
||||
{ capacityGb: 96, modelSizeGb: 72 },
|
||||
{ capacityGb: 96, modelSizeGb: 72 },
|
||||
].map(({ capacityGb, modelSizeGb }, index) =>
|
||||
device({
|
||||
index,
|
||||
id: `team-macbook-${String(index + 1).padStart(2, "0")}`,
|
||||
label: `Team MacBook ${String(index + 1).padStart(2, "0")}`,
|
||||
capacityGb,
|
||||
modelSizeGb,
|
||||
isSelf: index === 0,
|
||||
}),
|
||||
),
|
||||
device({
|
||||
index: 10,
|
||||
id: "intensive-model-host-a",
|
||||
label: "Intensive model host A",
|
||||
capacityGb: 768,
|
||||
modelRef: "unsloth/Kimi-K2-Thinking-GGUF:UD-Q4_K_XL",
|
||||
modelSizeGb: 646,
|
||||
}),
|
||||
device({
|
||||
index: 11,
|
||||
id: "intensive-model-host-b",
|
||||
label: "Intensive model host B",
|
||||
capacityGb: 1_440,
|
||||
modelRef: "unsloth/DeepSeek-V3.2-GGUF:UD-Q4_K_XL",
|
||||
modelSizeGb: 1_000,
|
||||
}),
|
||||
],
|
||||
24,
|
||||
),
|
||||
};
|
||||
|
||||
/** Deterministic stress fixture using only the production snapshot contract. */
|
||||
export const LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE: CommunityComputeTutorialFixture =
|
||||
{
|
||||
id: "large-dense-500",
|
||||
title: "Large company",
|
||||
description:
|
||||
"Five hundred deterministic reporting devices for density testing.",
|
||||
snapshot: createLargeDenseSnapshot(500),
|
||||
};
|
||||
|
||||
export const COMMUNITY_COMPUTE_TUTORIAL_FIXTURES: Readonly<
|
||||
Record<CommunityComputeTutorialFixtureId, CommunityComputeTutorialFixture>
|
||||
> = {
|
||||
empty: EMPTY_COMMUNITY_COMPUTE_FIXTURE,
|
||||
"single-device": SINGLE_DEVICE_COMMUNITY_COMPUTE_FIXTURE,
|
||||
"small-company": SMALL_COMPANY_COMMUNITY_COMPUTE_FIXTURE,
|
||||
"large-dense-500": LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE,
|
||||
};
|
||||
|
||||
export function createLargeDenseSnapshot(
|
||||
deviceCount = 500,
|
||||
): CommunityComputeSnapshotInput {
|
||||
const count = Math.max(0, Math.floor(deviceCount));
|
||||
const capacities = [8, 16, 24, 32, 48, 64, 96, 128] as const;
|
||||
const devices = Array.from({ length: count }, (_, index) => {
|
||||
const capacityGb = capacities[index % capacities.length];
|
||||
const state =
|
||||
index % 41 === 0 ? "loading" : index % 17 === 0 ? "standby" : "serving";
|
||||
const largeIndex = [49, 149, 249, 349, 449, 499].indexOf(index);
|
||||
const largeSizes = [170, 230, 350, 480, 700, 1_000] as const;
|
||||
const largeCapacities = [192, 256, 384, 512, 768, 1_440] as const;
|
||||
const modelSizeGb =
|
||||
largeIndex >= 0
|
||||
? largeSizes[largeIndex]
|
||||
: roundCapacity(capacityGb * 0.72);
|
||||
return device({
|
||||
index,
|
||||
id: `dense-device-${String(index + 1).padStart(4, "0")}`,
|
||||
label:
|
||||
largeIndex >= 0
|
||||
? `${largeCapacities[largeIndex]} GB model host`
|
||||
: `Compute ${String(index + 1).padStart(4, "0")}`,
|
||||
capacityGb: largeIndex >= 0 ? largeCapacities[largeIndex] : capacityGb,
|
||||
modelRef:
|
||||
largeIndex >= 0
|
||||
? `community/Large-${modelSizeGb}GB-Instruct:FP8`
|
||||
: undefined,
|
||||
modelSizeGb,
|
||||
state,
|
||||
models: state === "serving" ? undefined : [],
|
||||
isSelf: index === 0,
|
||||
});
|
||||
});
|
||||
return snapshotFromDevices(devices, Math.ceil(count * 1.37));
|
||||
}
|
||||
|
||||
type DeviceOptions = {
|
||||
index: number;
|
||||
id: string;
|
||||
label: string;
|
||||
capacityGb: number;
|
||||
modelRef?: string;
|
||||
modelSizeGb?: number;
|
||||
models?: string[];
|
||||
state?: CommunityComputeDeviceInput["state"];
|
||||
isSelf?: boolean;
|
||||
};
|
||||
|
||||
function device(options: DeviceOptions): CommunityComputeDeviceInput {
|
||||
const model =
|
||||
options.modelRef ?? MODEL_REFS[options.index % MODEL_REFS.length];
|
||||
return {
|
||||
deviceId: options.id,
|
||||
label: options.label,
|
||||
capacityGb: options.capacityGb,
|
||||
models: options.models ?? [model],
|
||||
state: options.state ?? "serving",
|
||||
isSelf: options.isSelf ?? false,
|
||||
memberPubkey: `tutorial-member-${String(options.index + 1).padStart(4, "0")}`,
|
||||
reportedAt: 1_786_032_000,
|
||||
modelSizeGb: options.modelSizeGb ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function roundCapacity(value: number): number {
|
||||
return Math.round(value * 10) / 10;
|
||||
}
|
||||
|
||||
function snapshotFromDevices(
|
||||
devices: CommunityComputeDeviceInput[],
|
||||
memberCount: number,
|
||||
): CommunityComputeSnapshotInput {
|
||||
const serving = devices.filter(
|
||||
(item) => item.state === "serving" && item.models.length > 0,
|
||||
);
|
||||
const models = [...new Set(serving.flatMap((item) => item.models))].sort();
|
||||
const sharedCapacityGb = completeSum(serving.map((item) => item.capacityGb));
|
||||
const allocatedCapacityGb = completeSum(
|
||||
serving.map((item) => item.modelSizeGb ?? null),
|
||||
);
|
||||
return {
|
||||
sharingDeviceCount: serving.length,
|
||||
contributorMemberCount: new Set(
|
||||
serving.map((item) => item.memberPubkey ?? item.deviceId),
|
||||
).size,
|
||||
sharedCapacityGb,
|
||||
allocatedCapacityGb,
|
||||
models,
|
||||
devices,
|
||||
includesSelf: serving.some((item) => item.isSelf),
|
||||
memberCount,
|
||||
observedAt: 1_786_032_000,
|
||||
freshnessSeconds: 120,
|
||||
reason: serving.length === 0 ? "No sharing devices found" : null,
|
||||
};
|
||||
}
|
||||
|
||||
function completeSum(values: readonly (number | null)[]): number | null {
|
||||
return values.length > 0 && values.every((value) => value !== null)
|
||||
? values.reduce<number>((total, value) => total + (value ?? 0), 0)
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
hexCoordinateKey,
|
||||
hexDistance,
|
||||
hexNeighbours,
|
||||
hexTerritoryBoundaryEdges,
|
||||
hexTerritoryInteriorEdges,
|
||||
isContiguousHexTerritory,
|
||||
layoutCommunityComputeHexTerritories,
|
||||
} from "./communityComputeHexLayout.ts";
|
||||
import { LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE } from "./communityComputeFixtures.ts";
|
||||
import { deriveCommunityComputeMapModel } from "./communityComputeMapModel.ts";
|
||||
|
||||
function assertStructuralInvariants(layout) {
|
||||
const occupied = new Set(layout.cells.map(hexCoordinateKey));
|
||||
assert.equal(occupied.size, layout.cells.length, "cells must not overlap");
|
||||
assert.ok(
|
||||
layout.territories.every((territory) =>
|
||||
isContiguousHexTerritory(territory.cells),
|
||||
),
|
||||
"every territory must be contiguous",
|
||||
);
|
||||
if (layout.cells.length > 0) {
|
||||
const reached = new Set([hexCoordinateKey(layout.cells[0])]);
|
||||
const queue = [layout.cells[0]];
|
||||
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
||||
for (const neighbour of hexNeighbours(queue[cursor])) {
|
||||
const key = hexCoordinateKey(neighbour);
|
||||
if (occupied.has(key) && !reached.has(key)) {
|
||||
reached.add(key);
|
||||
queue.push(neighbour);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.equal(
|
||||
reached.size,
|
||||
occupied.size,
|
||||
"occupied mesh must be connected",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function organicGaps(layout) {
|
||||
const occupied = new Set(layout.cells.map(hexCoordinateKey));
|
||||
const empty = new Map();
|
||||
for (const cell of layout.cells) {
|
||||
for (const neighbour of hexNeighbours(cell)) {
|
||||
const key = hexCoordinateKey(neighbour);
|
||||
if (!occupied.has(key)) empty.set(key, neighbour);
|
||||
}
|
||||
}
|
||||
return [...empty.values()].filter(
|
||||
(cell) =>
|
||||
hexNeighbours(cell).filter((neighbour) =>
|
||||
occupied.has(hexCoordinateKey(neighbour)),
|
||||
).length >= 2,
|
||||
);
|
||||
}
|
||||
|
||||
function assertBalancedSparseFrontier(layout, deploymentCount) {
|
||||
const points = layout.cells.map((cell) => ({
|
||||
x: Math.sqrt(3) * (cell.q + cell.r / 2),
|
||||
y: 1.5 * cell.r,
|
||||
}));
|
||||
const width =
|
||||
Math.max(...points.map(({ x }) => x)) -
|
||||
Math.min(...points.map(({ x }) => x)) +
|
||||
Math.sqrt(3);
|
||||
const height =
|
||||
Math.max(...points.map(({ y }) => y)) -
|
||||
Math.min(...points.map(({ y }) => y)) +
|
||||
2;
|
||||
const aspectRatio = width / height;
|
||||
assert.ok(
|
||||
aspectRatio >= 0.8 && aspectRatio <= 1.3,
|
||||
`${deploymentCount} deployments have unbalanced aspect ratio ${aspectRatio}`,
|
||||
);
|
||||
|
||||
const radius = Math.max(
|
||||
...layout.cells.map((cell) => hexDistance(cell, { q: 0, r: 0 })),
|
||||
);
|
||||
const directionalRadii = Array(6).fill(0);
|
||||
for (const cell of layout.cells) {
|
||||
const projections = [
|
||||
cell.q,
|
||||
-cell.r,
|
||||
-cell.q - cell.r,
|
||||
-cell.q,
|
||||
cell.r,
|
||||
cell.q + cell.r,
|
||||
];
|
||||
for (const [index, projection] of projections.entries()) {
|
||||
directionalRadii[index] = Math.max(directionalRadii[index], projection);
|
||||
}
|
||||
}
|
||||
assert.ok(
|
||||
Math.max(...directionalRadii) - Math.min(...directionalRadii) <= 2,
|
||||
`${deploymentCount} deployments must reach every side evenly`,
|
||||
);
|
||||
|
||||
const outerOccupancy =
|
||||
layout.cells.filter((cell) => hexDistance(cell, { q: 0, r: 0 }) === radius)
|
||||
.length /
|
||||
(6 * radius);
|
||||
assert.ok(
|
||||
outerOccupancy >= 0.1 && outerOccupancy <= 0.55,
|
||||
`${deploymentCount} deployments need a sparse, notched outer edge (${outerOccupancy})`,
|
||||
);
|
||||
}
|
||||
|
||||
function deployments(count) {
|
||||
const buckets = [1, 2, 4, 7];
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
id: `deployment-${String(index + 1).padStart(4, "0")}`,
|
||||
cellCount: buckets[index % buckets.length],
|
||||
}));
|
||||
}
|
||||
|
||||
test("layout is deterministic and independent of input order", () => {
|
||||
const input = deployments(80);
|
||||
const first = layoutCommunityComputeHexTerritories(input);
|
||||
const second = layoutCommunityComputeHexTerritories([...input].reverse());
|
||||
assert.deepEqual(first, second);
|
||||
});
|
||||
|
||||
test("growing layouts stay balanced with sparse outer frontiers", () => {
|
||||
for (const count of [24, 250, 1_000]) {
|
||||
const layout = layoutCommunityComputeHexTerritories(deployments(count));
|
||||
assertStructuralInvariants(layout);
|
||||
assertBalancedSparseFrontier(layout, count);
|
||||
assert.ok(
|
||||
organicGaps(layout).length > 0,
|
||||
`${count} deployments should leave deterministic pockets or edge gaps`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("canonical territory order remains stable when previous placement is reused", () => {
|
||||
const input = deployments(60);
|
||||
const initial = layoutCommunityComputeHexTerritories(input);
|
||||
const next = layoutCommunityComputeHexTerritories([...input].reverse(), {
|
||||
previous: initial,
|
||||
});
|
||||
assert.deepEqual(
|
||||
next.territories.map(({ deploymentId }) => deploymentId),
|
||||
initial.territories.map(({ deploymentId }) => deploymentId),
|
||||
);
|
||||
});
|
||||
|
||||
test("payload changes preserve positions because IDs own the slots", () => {
|
||||
const initial = deployments(40);
|
||||
const changed = initial.map((deployment) => ({
|
||||
...deployment,
|
||||
label: `Changed ${deployment.id}`,
|
||||
health: "degraded",
|
||||
}));
|
||||
assert.deepEqual(
|
||||
layoutCommunityComputeHexTerritories(initial),
|
||||
layoutCommunityComputeHexTerritories(changed),
|
||||
);
|
||||
});
|
||||
|
||||
test("surviving IDs keep positions as deployments are added and removed", () => {
|
||||
const initialInput = deployments(40);
|
||||
const initial = layoutCommunityComputeHexTerritories(initialInput);
|
||||
const nextInput = [
|
||||
...initialInput.filter((_, index) => index % 4 !== 0),
|
||||
...deployments(6).map((item, index) => ({
|
||||
...item,
|
||||
id: `new-deployment-${index}`,
|
||||
})),
|
||||
];
|
||||
const next = layoutCommunityComputeHexTerritories(nextInput, {
|
||||
previous: initial,
|
||||
});
|
||||
const initialCenters = new Map(
|
||||
initial.territories.map((territory) => [
|
||||
territory.deploymentId,
|
||||
territory.center,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const territory of next.territories) {
|
||||
const oldCenter = initialCenters.get(territory.deploymentId);
|
||||
if (oldCenter !== undefined) {
|
||||
assert.deepEqual(territory.center, oldCenter);
|
||||
}
|
||||
}
|
||||
assert.equal(
|
||||
new Set(next.cells.map(hexCoordinateKey)).size,
|
||||
next.cells.length,
|
||||
);
|
||||
});
|
||||
|
||||
test("every supported territory shape is contiguous", () => {
|
||||
const layout = layoutCommunityComputeHexTerritories([
|
||||
{ id: "one", cellCount: 1 },
|
||||
{ id: "two", cellCount: 2 },
|
||||
{ id: "four", cellCount: 4 },
|
||||
{ id: "seven", cellCount: 7 },
|
||||
]);
|
||||
const expected = new Map([
|
||||
["one", 1],
|
||||
["two", 2],
|
||||
["four", 4],
|
||||
["seven", 7],
|
||||
]);
|
||||
|
||||
for (const territory of layout.territories) {
|
||||
assert.equal(territory.cells.length, expected.get(territory.deploymentId));
|
||||
assert.ok(
|
||||
isContiguousHexTerritory(territory.cells),
|
||||
`${territory.deploymentId} must be contiguous`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("fused boundaries omit interior edges for 1, 2, 4, and 7 cells", () => {
|
||||
const shapes = [
|
||||
[{ q: 0, r: 0 }],
|
||||
[
|
||||
{ q: 0, r: 0 },
|
||||
{ q: 1, r: 0 },
|
||||
],
|
||||
[
|
||||
{ q: 0, r: 0 },
|
||||
{ q: 1, r: 0 },
|
||||
{ q: 1, r: -1 },
|
||||
{ q: 0, r: -1 },
|
||||
],
|
||||
[{ q: 0, r: 0 }, ...hexNeighbours({ q: 0, r: 0 })],
|
||||
];
|
||||
|
||||
for (const [index, cells] of shapes.entries()) {
|
||||
const sharedPairs =
|
||||
cells.reduce(
|
||||
(total, cell) =>
|
||||
total +
|
||||
hexNeighbours(cell).filter((neighbour) =>
|
||||
cells.some(
|
||||
(candidate) =>
|
||||
hexCoordinateKey(candidate) === hexCoordinateKey(neighbour),
|
||||
),
|
||||
).length,
|
||||
0,
|
||||
) / 2;
|
||||
const boundary = hexTerritoryBoundaryEdges(cells);
|
||||
assert.equal(boundary.length, 6 * cells.length - 2 * sharedPairs);
|
||||
|
||||
const endpointDegree = new Map();
|
||||
for (const edge of boundary) {
|
||||
assert.notDeepEqual(edge.start, edge.end);
|
||||
for (const point of [edge.start, edge.end]) {
|
||||
const key = `${point.x.toFixed(8)},${point.y.toFixed(8)}`;
|
||||
endpointDegree.set(key, (endpointDegree.get(key) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
assert.ok(
|
||||
[...endpointDegree.values()].every((degree) => degree === 2),
|
||||
`shape ${index} boundary must form closed loops without dangling edges`,
|
||||
);
|
||||
}
|
||||
|
||||
const edgeKey = (edge) =>
|
||||
[edge.start, edge.end]
|
||||
.map((point) => `${point.x.toFixed(8)},${point.y.toFixed(8)}`)
|
||||
.sort()
|
||||
.join("|");
|
||||
assert.deepEqual(
|
||||
hexTerritoryBoundaryEdges(shapes[3]).map(edgeKey).sort(),
|
||||
hexTerritoryBoundaryEdges([...shapes[3]].reverse())
|
||||
.map(edgeKey)
|
||||
.sort(),
|
||||
"edge membership must not depend on cell order",
|
||||
);
|
||||
});
|
||||
|
||||
test("interior edges contain each shared device boundary exactly once", () => {
|
||||
const cells = [{ q: 0, r: 0 }, ...hexNeighbours({ q: 0, r: 0 })];
|
||||
const interior = hexTerritoryInteriorEdges(cells);
|
||||
const edgeKey = (edge) =>
|
||||
[edge.start, edge.end]
|
||||
.map((point) => `${point.x.toFixed(8)},${point.y.toFixed(8)}`)
|
||||
.sort()
|
||||
.join("|");
|
||||
|
||||
assert.equal(interior.length, 12);
|
||||
assert.equal(new Set(interior.map(edgeKey)).size, interior.length);
|
||||
assert.deepEqual(
|
||||
interior.map(edgeKey).sort(),
|
||||
hexTerritoryInteriorEdges([...cells].reverse())
|
||||
.map(edgeKey)
|
||||
.sort(),
|
||||
"shared edge membership must not depend on cell order",
|
||||
);
|
||||
assert.deepEqual(hexTerritoryInteriorEdges([{ q: 0, r: 0 }]), []);
|
||||
});
|
||||
|
||||
test("territories never emit duplicate occupied cells", () => {
|
||||
const layout = layoutCommunityComputeHexTerritories(deployments(250));
|
||||
const coordinates = layout.cells.map(hexCoordinateKey);
|
||||
assert.equal(new Set(coordinates).size, coordinates.length);
|
||||
assert.equal(
|
||||
new Set(layout.cells.map((cell) => cell.id)).size,
|
||||
layout.cells.length,
|
||||
);
|
||||
});
|
||||
|
||||
test("duplicate deployment IDs are rejected rather than overlapped", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
layoutCommunityComputeHexTerritories([
|
||||
{ id: "same", cellCount: 1 },
|
||||
{ id: "same", cellCount: 7 },
|
||||
]),
|
||||
/Duplicate community compute deployment ID: same/,
|
||||
);
|
||||
});
|
||||
|
||||
test("empty layout has no fabricated bounds", () => {
|
||||
assert.deepEqual(layoutCommunityComputeHexTerritories([]), {
|
||||
territories: [],
|
||||
cells: [],
|
||||
bounds: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("500-deployment fixture lays out uniquely and contiguously", () => {
|
||||
const model = deriveCommunityComputeMapModel(
|
||||
LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE.snapshot,
|
||||
);
|
||||
const input = model.deployments.map(({ id, cellCount }) => ({
|
||||
id,
|
||||
cellCount,
|
||||
}));
|
||||
const layout = layoutCommunityComputeHexTerritories(input);
|
||||
|
||||
assert.equal(layout.territories.length, model.deployments.length);
|
||||
assert.equal(
|
||||
layout.cells.length,
|
||||
model.deployments.reduce(
|
||||
(total, deployment) => total + deployment.cellCount,
|
||||
0,
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
new Set(layout.cells.map(hexCoordinateKey)).size,
|
||||
layout.cells.length,
|
||||
);
|
||||
assert.ok(
|
||||
layout.territories.every((territory) =>
|
||||
isContiguousHexTerritory(territory.cells),
|
||||
),
|
||||
);
|
||||
assert.ok(layout.bounds !== null);
|
||||
});
|
||||
448
desktop/src/features/mesh-compute/communityComputeHexLayout.ts
Normal file
448
desktop/src/features/mesh-compute/communityComputeHexLayout.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
import type { HexCellCount } from "./communityComputeMapModel";
|
||||
|
||||
/** Axial coordinates for a pointy-top hex grid. */
|
||||
export type HexCoordinate = {
|
||||
q: number;
|
||||
r: number;
|
||||
};
|
||||
|
||||
/** Cartesian point in pointy-top hex geometry. */
|
||||
export type HexPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
/** One exposed side of a fused hex territory boundary. */
|
||||
export type HexBoundaryEdge = {
|
||||
start: HexPoint;
|
||||
end: HexPoint;
|
||||
};
|
||||
|
||||
export type CommunityComputeLayoutInput = {
|
||||
id: string;
|
||||
cellCount: HexCellCount;
|
||||
};
|
||||
|
||||
export type CommunityComputeHexCell = HexCoordinate & {
|
||||
id: string;
|
||||
deploymentId: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type CommunityComputeHexTerritory = {
|
||||
deploymentId: string;
|
||||
center: HexCoordinate;
|
||||
cells: readonly CommunityComputeHexCell[];
|
||||
};
|
||||
|
||||
export type CommunityComputeHexBounds = {
|
||||
minQ: number;
|
||||
maxQ: number;
|
||||
minR: number;
|
||||
maxR: number;
|
||||
};
|
||||
|
||||
export type CommunityComputeHexLayout = {
|
||||
territories: readonly CommunityComputeHexTerritory[];
|
||||
cells: readonly CommunityComputeHexCell[];
|
||||
bounds: CommunityComputeHexBounds | null;
|
||||
};
|
||||
|
||||
export type CommunityComputeHexLayoutOptions = {
|
||||
/**
|
||||
* Reuse centres for IDs that are still present. Supplying the last layout
|
||||
* keeps a live map steady while deployments arrive, leave, or change size.
|
||||
*/
|
||||
previous?: CommunityComputeHexLayout | null;
|
||||
};
|
||||
|
||||
const HEX_DIRECTIONS: readonly HexCoordinate[] = [
|
||||
{ q: 1, r: 0 },
|
||||
{ q: 1, r: -1 },
|
||||
{ q: 0, r: -1 },
|
||||
{ q: -1, r: 0 },
|
||||
{ q: -1, r: 1 },
|
||||
{ q: 0, r: 1 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Lay deployments out as one organic, edge-connected honeycomb.
|
||||
*
|
||||
* Input order never affects the result. Stable hashes choose territory
|
||||
* orientation, while a deterministic low-discrepancy permutation spreads each
|
||||
* ring's placements around the whole current frontier. Completed inner rings
|
||||
* remain dense; only the growing outer layers carry deliberate notches. With a
|
||||
* previous layout, collision-free surviving shapes retain their exact cells;
|
||||
* new shapes take the first non-overlapping frontier position that shares an
|
||||
* edge with the occupied mesh.
|
||||
*/
|
||||
export function layoutCommunityComputeHexTerritories(
|
||||
deployments: readonly CommunityComputeLayoutInput[],
|
||||
options: CommunityComputeHexLayoutOptions = {},
|
||||
): CommunityComputeHexLayout {
|
||||
const seenIds = new Set<string>();
|
||||
for (const deployment of deployments) {
|
||||
if (seenIds.has(deployment.id)) {
|
||||
throw new Error(
|
||||
`Duplicate community compute deployment ID: ${deployment.id}`,
|
||||
);
|
||||
}
|
||||
seenIds.add(deployment.id);
|
||||
}
|
||||
|
||||
const ordered = [...deployments].sort(
|
||||
(left, right) =>
|
||||
stableHash(left.id) - stableHash(right.id) ||
|
||||
left.id.localeCompare(right.id),
|
||||
);
|
||||
// Previous cells are retained only when they remain collision-free. New
|
||||
// territories search the compact spiral for the first candidate that both
|
||||
// fits and touches the occupied frontier.
|
||||
const previousById = new Map(
|
||||
(options.previous?.territories ?? []).map((territory) => [
|
||||
territory.deploymentId,
|
||||
territory,
|
||||
]),
|
||||
);
|
||||
const retainableIds = connectedRetainedTerritoryIds(ordered, previousById);
|
||||
const occupied = new Set<string>();
|
||||
const territories: CommunityComputeHexTerritory[] = [];
|
||||
const placementOrder = [...ordered].sort(
|
||||
(left, right) =>
|
||||
Number(retainableIds.has(right.id)) -
|
||||
Number(retainableIds.has(left.id)) ||
|
||||
ordered.indexOf(left) - ordered.indexOf(right),
|
||||
);
|
||||
|
||||
for (const deployment of placementOrder) {
|
||||
const previous = retainableIds.has(deployment.id)
|
||||
? previousById.get(deployment.id)
|
||||
: undefined;
|
||||
const retained =
|
||||
previous?.cells.length === deployment.cellCount
|
||||
? previous.cells.map(({ q, r }) => ({ q, r }))
|
||||
: null;
|
||||
const retainedFits = retained?.every(
|
||||
(cell) => !occupied.has(hexCoordinateKey(cell)),
|
||||
);
|
||||
const orientation = stableHash(`${deployment.id}:orientation`) % 6;
|
||||
let center = retainedFits ? previous?.center : undefined;
|
||||
let coordinates = retainedFits ? retained : null;
|
||||
|
||||
if (!coordinates || !center) {
|
||||
for (let slot = 0; ; slot += 1) {
|
||||
const candidate = balancedSpiralCoordinate(slot);
|
||||
const offsets = territoryOffsets(deployment.cellCount, orientation);
|
||||
const candidateCells = offsets.map((offset) => ({
|
||||
q: candidate.q + offset.q,
|
||||
r: candidate.r + offset.r,
|
||||
}));
|
||||
const fits = candidateCells.every(
|
||||
(cell) => !occupied.has(hexCoordinateKey(cell)),
|
||||
);
|
||||
const touches =
|
||||
occupied.size === 0 ||
|
||||
candidateCells.some((cell) =>
|
||||
hexNeighbours(cell).some((neighbour) =>
|
||||
occupied.has(hexCoordinateKey(neighbour)),
|
||||
),
|
||||
);
|
||||
if (fits && touches) {
|
||||
center = candidate;
|
||||
coordinates = candidateCells;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cells = coordinates.map(({ q, r }, index) => {
|
||||
occupied.add(hexCoordinateKey({ q, r }));
|
||||
return {
|
||||
id: `${deployment.id}@${q},${r}`,
|
||||
deploymentId: deployment.id,
|
||||
index,
|
||||
q,
|
||||
r,
|
||||
};
|
||||
});
|
||||
territories.push({ deploymentId: deployment.id, center, cells });
|
||||
}
|
||||
territories.sort(
|
||||
(left, right) =>
|
||||
ordered.findIndex((item) => item.id === left.deploymentId) -
|
||||
ordered.findIndex((item) => item.id === right.deploymentId),
|
||||
);
|
||||
const cells = territories.flatMap((territory) => territory.cells);
|
||||
|
||||
return { territories, cells, bounds: findBounds(cells) };
|
||||
}
|
||||
|
||||
/** The six axial neighbours of a cell, in stable clockwise order. */
|
||||
export function hexNeighbours(coordinate: HexCoordinate): HexCoordinate[] {
|
||||
return HEX_DIRECTIONS.map((direction) => ({
|
||||
q: coordinate.q + direction.q,
|
||||
r: coordinate.r + direction.r,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only the exposed unit-hex sides of a territory.
|
||||
*
|
||||
* Coordinates use pointy-top axial geometry. Shared sides are omitted, so the
|
||||
* result can be stroked as one fused territory rather than as overlapping
|
||||
* individual hexagons. Edge direction is stable and follows each cell's
|
||||
* perimeter; callers may chain matching endpoints when an ordered path is
|
||||
* required.
|
||||
*/
|
||||
export function hexTerritoryBoundaryEdges(
|
||||
cells: readonly HexCoordinate[],
|
||||
): HexBoundaryEdge[] {
|
||||
const occupied = new Set(cells.map(hexCoordinateKey));
|
||||
const edges: HexBoundaryEdge[] = [];
|
||||
|
||||
for (const cell of cells) {
|
||||
for (let direction = 0; direction < HEX_DIRECTIONS.length; direction += 1) {
|
||||
const neighbour = {
|
||||
q: cell.q + HEX_DIRECTIONS[direction].q,
|
||||
r: cell.r + HEX_DIRECTIONS[direction].r,
|
||||
};
|
||||
if (!occupied.has(hexCoordinateKey(neighbour))) {
|
||||
edges.push(hexEdge(cell, direction));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return each side shared by two cells in a territory exactly once.
|
||||
*
|
||||
* Fused model territories do not render these edges. Split deployments use
|
||||
* them to show the device boundaries without drawing inset outlines around
|
||||
* every cell or accidentally doubling a shared stroke.
|
||||
*/
|
||||
export function hexTerritoryInteriorEdges(
|
||||
cells: readonly HexCoordinate[],
|
||||
): HexBoundaryEdge[] {
|
||||
const occupied = new Set(cells.map(hexCoordinateKey));
|
||||
const edges: HexBoundaryEdge[] = [];
|
||||
|
||||
for (const cell of cells) {
|
||||
const cellKey = hexCoordinateKey(cell);
|
||||
for (let direction = 0; direction < HEX_DIRECTIONS.length; direction += 1) {
|
||||
const neighbour = {
|
||||
q: cell.q + HEX_DIRECTIONS[direction].q,
|
||||
r: cell.r + HEX_DIRECTIONS[direction].r,
|
||||
};
|
||||
const neighbourKey = hexCoordinateKey(neighbour);
|
||||
if (occupied.has(neighbourKey) && cellKey < neighbourKey) {
|
||||
edges.push(hexEdge(cell, direction));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
function hexEdge(cell: HexCoordinate, direction: number): HexBoundaryEdge {
|
||||
const center = {
|
||||
x: Math.sqrt(3) * (cell.q + cell.r / 2),
|
||||
y: 1.5 * cell.r,
|
||||
};
|
||||
const corners = Array.from({ length: 6 }, (_, index) => {
|
||||
const angle = ((-30 + index * 60) * Math.PI) / 180;
|
||||
return {
|
||||
x: normalizeHexPointComponent(center.x + Math.cos(angle)),
|
||||
y: normalizeHexPointComponent(center.y + Math.sin(angle)),
|
||||
};
|
||||
});
|
||||
return {
|
||||
start: corners[(6 - direction) % 6],
|
||||
end: corners[(7 - direction) % 6],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHexPointComponent(value: number): number {
|
||||
return Math.abs(value) < Number.EPSILON * 8 ? 0 : value;
|
||||
}
|
||||
|
||||
/** Axial hex distance, useful to consumers for borders and transitions. */
|
||||
export function hexDistance(left: HexCoordinate, right: HexCoordinate): number {
|
||||
const dq = left.q - right.q;
|
||||
const dr = left.r - right.r;
|
||||
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(dq + dr));
|
||||
}
|
||||
|
||||
/** True when every cell can be reached from every other through shared edges. */
|
||||
export function isContiguousHexTerritory(
|
||||
cells: readonly HexCoordinate[],
|
||||
): boolean {
|
||||
if (cells.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const remaining = new Set(cells.map(hexCoordinateKey));
|
||||
const queue: HexCoordinate[] = [cells[0]];
|
||||
remaining.delete(hexCoordinateKey(cells[0]));
|
||||
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
||||
for (const neighbour of hexNeighbours(queue[cursor])) {
|
||||
const key = hexCoordinateKey(neighbour);
|
||||
if (remaining.delete(key)) {
|
||||
queue.push(neighbour);
|
||||
}
|
||||
}
|
||||
}
|
||||
return remaining.size === 0;
|
||||
}
|
||||
|
||||
export function hexCoordinateKey(coordinate: HexCoordinate): string {
|
||||
return `${coordinate.q},${coordinate.r}`;
|
||||
}
|
||||
|
||||
/** Keep only the edge-connected component of valid surviving territories. */
|
||||
function connectedRetainedTerritoryIds(
|
||||
ordered: readonly CommunityComputeLayoutInput[],
|
||||
previousById: ReadonlyMap<string, CommunityComputeHexTerritory>,
|
||||
): Set<string> {
|
||||
const valid = ordered
|
||||
.map((deployment) => previousById.get(deployment.id))
|
||||
.filter(
|
||||
(territory): territory is CommunityComputeHexTerritory =>
|
||||
territory !== undefined &&
|
||||
territory.cells.length ===
|
||||
ordered.find((item) => item.id === territory.deploymentId)?.cellCount,
|
||||
);
|
||||
if (valid.length === 0) return new Set();
|
||||
|
||||
const owner = new Map<string, string>();
|
||||
for (const territory of valid) {
|
||||
for (const cell of territory.cells) {
|
||||
const key = hexCoordinateKey(cell);
|
||||
if (owner.has(key)) return new Set();
|
||||
owner.set(key, territory.deploymentId);
|
||||
}
|
||||
}
|
||||
const retained = new Set([valid[0].deploymentId]);
|
||||
const queue = [valid[0].deploymentId];
|
||||
const byId = new Map(
|
||||
valid.map((territory) => [territory.deploymentId, territory]),
|
||||
);
|
||||
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
||||
const territory = byId.get(queue[cursor]);
|
||||
if (!territory) continue;
|
||||
for (const cell of territory.cells) {
|
||||
for (const neighbour of hexNeighbours(cell)) {
|
||||
const neighbourOwner = owner.get(hexCoordinateKey(neighbour));
|
||||
if (neighbourOwner && !retained.has(neighbourOwner)) {
|
||||
retained.add(neighbourOwner);
|
||||
queue.push(neighbourOwner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return retained;
|
||||
}
|
||||
|
||||
function territoryOffsets(
|
||||
count: HexCellCount,
|
||||
orientation: number,
|
||||
): HexCoordinate[] {
|
||||
const center = { q: 0, r: 0 };
|
||||
if (count === 1) {
|
||||
return [center];
|
||||
}
|
||||
if (count === 2) {
|
||||
return [center, HEX_DIRECTIONS[orientation]];
|
||||
}
|
||||
if (count === 4) {
|
||||
return [
|
||||
center,
|
||||
HEX_DIRECTIONS[orientation],
|
||||
HEX_DIRECTIONS[(orientation + 1) % 6],
|
||||
HEX_DIRECTIONS[(orientation + 2) % 6],
|
||||
];
|
||||
}
|
||||
return [center, ...HEX_DIRECTIONS];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero-based spiral slot: origin, then complete radius-one, radius-two, … rings.
|
||||
* Runtime is O(radius), which is O(sqrt(n)); even at dense fixture sizes the
|
||||
* largest radius remains small.
|
||||
*/
|
||||
function balancedSpiralCoordinate(index: number): HexCoordinate {
|
||||
if (index === 0) {
|
||||
return { q: 0, r: 0 };
|
||||
}
|
||||
|
||||
let radius = 1;
|
||||
let ringStart = 1;
|
||||
while (index >= ringStart + 6 * radius) {
|
||||
ringStart += 6 * radius;
|
||||
radius += 1;
|
||||
}
|
||||
|
||||
const ringLength = 6 * radius;
|
||||
const ringIndex = index - ringStart;
|
||||
// Multiplication by a number coprime with the ring length is a permutation,
|
||||
// not sampling: every slot is eventually visited. A step near half a ring
|
||||
// alternates opposite sides, preventing a partially filled layer from
|
||||
// producing the one-sided growth of a clockwise spiral.
|
||||
let step = Math.max(1, Math.floor(ringLength / 2) - 1);
|
||||
while (greatestCommonDivisor(step, ringLength) !== 1) step -= 1;
|
||||
const offsetSeed = stableHash(`ring:${radius}`) % ringLength;
|
||||
let offset = (offsetSeed + ringIndex * step) % ringLength;
|
||||
let coordinate = { q: -radius, r: radius };
|
||||
for (const direction of HEX_DIRECTIONS) {
|
||||
const steps = Math.min(radius, offset);
|
||||
coordinate = {
|
||||
q: coordinate.q + direction.q * steps,
|
||||
r: coordinate.r + direction.r * steps,
|
||||
};
|
||||
offset -= steps;
|
||||
if (offset === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return coordinate;
|
||||
}
|
||||
|
||||
function greatestCommonDivisor(left: number, right: number): number {
|
||||
let a = left;
|
||||
let b = right;
|
||||
while (b !== 0) {
|
||||
const remainder = a % b;
|
||||
a = b;
|
||||
b = remainder;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function findBounds(
|
||||
cells: readonly HexCoordinate[],
|
||||
): CommunityComputeHexBounds | null {
|
||||
if (cells.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let minQ = cells[0].q;
|
||||
let maxQ = cells[0].q;
|
||||
let minR = cells[0].r;
|
||||
let maxR = cells[0].r;
|
||||
for (const cell of cells.slice(1)) {
|
||||
minQ = Math.min(minQ, cell.q);
|
||||
maxQ = Math.max(maxQ, cell.q);
|
||||
minR = Math.min(minR, cell.r);
|
||||
maxR = Math.max(maxR, cell.r);
|
||||
}
|
||||
return { minQ, maxQ, minR, maxR };
|
||||
}
|
||||
|
||||
/** FNV-1a over UTF-16 code units; deterministic on every JS runtime. */
|
||||
function stableHash(value: string): number {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
hash ^= value.charCodeAt(index);
|
||||
hash = Math.imul(hash, 0x01000193);
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
bucketHexCellCount,
|
||||
capacityGbToHexCellCount,
|
||||
deriveCommunityComputeMapModel,
|
||||
} from "./communityComputeMapModel.ts";
|
||||
import {
|
||||
COMMUNITY_COMPUTE_TUTORIAL_FIXTURES,
|
||||
LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE,
|
||||
createLargeDenseSnapshot,
|
||||
} from "./communityComputeFixtures.ts";
|
||||
|
||||
function device(overrides = {}) {
|
||||
return {
|
||||
deviceId: "device-1",
|
||||
label: "Studio",
|
||||
capacityGb: 32,
|
||||
models: ["gemma"],
|
||||
state: "serving",
|
||||
isSelf: false,
|
||||
memberPubkey: "member-1",
|
||||
reportedAt: 1_786_032_000,
|
||||
modelSizeGb: 18,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function snapshot(overrides = {}) {
|
||||
return {
|
||||
sharingDeviceCount: 1,
|
||||
contributorMemberCount: 1,
|
||||
sharedCapacityGb: 32,
|
||||
allocatedCapacityGb: 18,
|
||||
models: ["gemma"],
|
||||
devices: [device()],
|
||||
includesSelf: false,
|
||||
memberCount: 4,
|
||||
reason: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("cell buckets are exactly 1, 2, 4, and 7", () => {
|
||||
assert.deepEqual(
|
||||
[-1, 1, 1.1, 2, 2.1, 4, 4.1, 999, Number.NaN].map(bucketHexCellCount),
|
||||
[1, 1, 2, 2, 4, 4, 7, 7, 1],
|
||||
);
|
||||
assert.deepEqual(
|
||||
[null, 128, 128.1, 256, 256.1, 512, 512.1, 1000].map(
|
||||
capacityGbToHexCellCount,
|
||||
),
|
||||
[1, 1, 2, 2, 4, 4, 7, 7],
|
||||
);
|
||||
});
|
||||
|
||||
test("only ready serving models become deployments", () => {
|
||||
const model = deriveCommunityComputeMapModel(
|
||||
snapshot({
|
||||
sharingDeviceCount: 1,
|
||||
devices: [
|
||||
device({ deviceId: "a", models: ["gemma", "qwen"] }),
|
||||
device({ deviceId: "b", state: "loading", models: [] }),
|
||||
device({ deviceId: "c", state: "standby", models: [] }),
|
||||
device({ deviceId: "d", state: "consuming", models: [] }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
assert.equal(model.status, "ready");
|
||||
assert.deepEqual(
|
||||
model.deployments.map((deployment) => deployment.modelId),
|
||||
["gemma", "qwen"],
|
||||
);
|
||||
assert.equal(model.kpis.deploymentCount, 2);
|
||||
assert.equal(model.kpis.contributorMemberCount, 1);
|
||||
assert.equal(model.health.counts.healthy, 1);
|
||||
assert.equal(model.health.counts.warming, 1);
|
||||
assert.equal(model.health.counts.idle, 1);
|
||||
});
|
||||
|
||||
test("one territory is produced per model and per device replica", () => {
|
||||
const model = deriveCommunityComputeMapModel(
|
||||
snapshot({
|
||||
devices: [
|
||||
device({ deviceId: "a", models: ["qwen", "gemma"] }),
|
||||
device({ deviceId: "b", memberPubkey: "member-2", models: ["gemma"] }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
assert.equal(model.deployments.length, 3);
|
||||
assert.equal(
|
||||
model.deployments.filter((deployment) => deployment.modelId === "gemma")
|
||||
.length,
|
||||
2,
|
||||
);
|
||||
assert.equal(new Set(model.deployments.map(({ id }) => id)).size, 3);
|
||||
});
|
||||
|
||||
test("reported model footprint controls area without fixture overrides", () => {
|
||||
const model = deriveCommunityComputeMapModel(
|
||||
snapshot({ devices: [device({ capacityGb: 768, modelSizeGb: 96 })] }),
|
||||
);
|
||||
assert.equal(model.deployments[0].cellCount, 1);
|
||||
|
||||
const large = deriveCommunityComputeMapModel(
|
||||
snapshot({ devices: [device({ capacityGb: 768, modelSizeGb: 646 })] }),
|
||||
);
|
||||
assert.equal(large.deployments[0].cellCount, 7);
|
||||
});
|
||||
|
||||
test("loading, consumer-only, and empty snapshots render no territories", () => {
|
||||
for (const state of ["loading", "standby", "consuming"]) {
|
||||
const model = deriveCommunityComputeMapModel(
|
||||
snapshot({ devices: [device({ state, models: [] })] }),
|
||||
);
|
||||
assert.equal(model.status, "empty");
|
||||
assert.equal(model.deployments.length, 0);
|
||||
}
|
||||
assert.equal(deriveCommunityComputeMapModel(null).status, "loading");
|
||||
assert.equal(
|
||||
deriveCommunityComputeMapModel(
|
||||
COMMUNITY_COMPUTE_TUTORIAL_FIXTURES.empty.snapshot,
|
||||
).status,
|
||||
"empty",
|
||||
);
|
||||
});
|
||||
|
||||
test("projection is deterministic and independent of device/model order", () => {
|
||||
const devices = [
|
||||
device({ deviceId: "z", label: "Zulu", models: ["qwen", "gemma"] }),
|
||||
device({ deviceId: "a", label: "Alpha", models: ["mistral"] }),
|
||||
];
|
||||
const forward = deriveCommunityComputeMapModel(snapshot({ devices }));
|
||||
const reverse = deriveCommunityComputeMapModel(
|
||||
snapshot({
|
||||
devices: [...devices]
|
||||
.reverse()
|
||||
.map((item) => ({ ...item, models: [...item.models].reverse() })),
|
||||
}),
|
||||
);
|
||||
assert.deepEqual(forward, reverse);
|
||||
});
|
||||
|
||||
test("tutorial fixtures use only production fields and production sizing", () => {
|
||||
assert.deepEqual(Object.keys(COMMUNITY_COMPUTE_TUTORIAL_FIXTURES).sort(), [
|
||||
"empty",
|
||||
"large-dense-500",
|
||||
"single-device",
|
||||
"small-company",
|
||||
]);
|
||||
const allowed = new Set([
|
||||
"deviceId",
|
||||
"label",
|
||||
"capacityGb",
|
||||
"models",
|
||||
"state",
|
||||
"isSelf",
|
||||
"memberPubkey",
|
||||
"reportedAt",
|
||||
"modelSizeGb",
|
||||
]);
|
||||
for (const fixture of Object.values(COMMUNITY_COMPUTE_TUTORIAL_FIXTURES)) {
|
||||
for (const item of fixture.snapshot.devices) {
|
||||
assert.deepEqual(
|
||||
Object.keys(item).filter((key) => !allowed.has(key)),
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
const small = deriveCommunityComputeMapModel(
|
||||
COMMUNITY_COMPUTE_TUTORIAL_FIXTURES["small-company"].snapshot,
|
||||
);
|
||||
assert.equal(small.deployments.length, 12);
|
||||
assert.equal(
|
||||
small.deployments.filter(({ deviceLabel }) =>
|
||||
deviceLabel.startsWith("Team MacBook"),
|
||||
).length,
|
||||
10,
|
||||
);
|
||||
assert.equal(
|
||||
small.deployments.filter(({ cellCount }) => cellCount === 1).length,
|
||||
10,
|
||||
);
|
||||
assert.equal(
|
||||
small.deployments.filter(({ cellCount }) => cellCount === 7).length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
test("large dense fixture is deterministic and excludes non-serving rows", () => {
|
||||
const fixture = LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE.snapshot;
|
||||
assert.deepEqual(fixture, createLargeDenseSnapshot(500));
|
||||
assert.equal(fixture.devices.length, 500);
|
||||
const servingCount = fixture.devices.filter(
|
||||
(item) => item.state === "serving" && item.models.length > 0,
|
||||
).length;
|
||||
const model = deriveCommunityComputeMapModel(fixture);
|
||||
assert.equal(model.deployments.length, servingCount);
|
||||
assert.equal(model.kpis.deploymentCount, servingCount);
|
||||
assert.equal(
|
||||
new Set(model.deployments.map(({ id }) => id)).size,
|
||||
servingCount,
|
||||
);
|
||||
});
|
||||
365
desktop/src/features/mesh-compute/communityComputeMapModel.ts
Normal file
365
desktop/src/features/mesh-compute/communityComputeMapModel.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import type {
|
||||
MeshDeviceState,
|
||||
MeshSnapshot,
|
||||
MeshSnapshotDevice,
|
||||
} from "@/shared/api/tauriMesh";
|
||||
|
||||
/** The only territory sizes rendered by the community compute map. */
|
||||
export const HEX_CELL_BUCKETS = [1, 2, 4, 7] as const;
|
||||
|
||||
export type HexCellCount = (typeof HEX_CELL_BUCKETS)[number];
|
||||
|
||||
/**
|
||||
* The Community view intentionally consumes only fields in the production
|
||||
* snapshot contract. Tutorial fixtures use this same shape so examples cannot
|
||||
* quietly grow capabilities that live Compute does not have.
|
||||
*/
|
||||
export type CommunityComputeDeviceInput = MeshSnapshotDevice;
|
||||
|
||||
/** A readonly, structurally compatible form of the current mesh snapshot. */
|
||||
export type CommunityComputeSnapshotInput<
|
||||
TDevice extends MeshSnapshotDevice = CommunityComputeDeviceInput,
|
||||
> = Omit<MeshSnapshot, "devices"> & {
|
||||
readonly devices: readonly TDevice[];
|
||||
};
|
||||
|
||||
export type CommunityComputeDeploymentHealth =
|
||||
| "healthy"
|
||||
| "warming"
|
||||
| "idle"
|
||||
| "degraded"
|
||||
| "offline"
|
||||
| "unknown";
|
||||
|
||||
export type CommunityComputeMapHealth =
|
||||
| "healthy"
|
||||
| "attention"
|
||||
| "critical"
|
||||
| "empty"
|
||||
| "unknown";
|
||||
|
||||
export type CommunityComputeDeployment<
|
||||
TDevice extends MeshSnapshotDevice = CommunityComputeDeviceInput,
|
||||
> = {
|
||||
/** Stable map identity. Never derived from the device's array position. */
|
||||
id: string;
|
||||
/** Model shown as the territory's primary identity. */
|
||||
label: string;
|
||||
modelId: string;
|
||||
deviceLabel: string;
|
||||
capacityGb: number | null;
|
||||
models: readonly string[];
|
||||
state: MeshDeviceState;
|
||||
isSelf: boolean;
|
||||
health: CommunityComputeDeploymentHealth;
|
||||
healthReason: string | null;
|
||||
cellCount: HexCellCount;
|
||||
/** Preserves additive backend data for a future detail projection. */
|
||||
source: Readonly<TDevice>;
|
||||
};
|
||||
|
||||
export type CommunityComputeKpis = {
|
||||
memberCount: number;
|
||||
contributorMemberCount: number;
|
||||
deploymentCount: number;
|
||||
activeDeploymentCount: number;
|
||||
sharedCapacityGb: number | null;
|
||||
allocatedCapacityGb: number | null;
|
||||
allocatedPercent: number | null;
|
||||
modelCount: number;
|
||||
};
|
||||
|
||||
export type CommunityComputeHealthSummary = {
|
||||
status: CommunityComputeMapHealth;
|
||||
label: string;
|
||||
reason: string | null;
|
||||
counts: Record<CommunityComputeDeploymentHealth, number>;
|
||||
};
|
||||
|
||||
export type CommunityComputeMapModel<
|
||||
TDevice extends MeshSnapshotDevice = CommunityComputeDeviceInput,
|
||||
> = {
|
||||
status: "loading" | "ready" | "empty";
|
||||
kpis: CommunityComputeKpis;
|
||||
health: CommunityComputeHealthSummary;
|
||||
deployments: readonly CommunityComputeDeployment<TDevice>[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Round a desired territory footprint up to one of the four supported shapes.
|
||||
* Invalid and non-positive weights still get one cell so a deployment cannot
|
||||
* disappear from the map.
|
||||
*/
|
||||
export function bucketHexCellCount(
|
||||
weight: number | null | undefined,
|
||||
): HexCellCount {
|
||||
if (weight === null || weight === undefined || !Number.isFinite(weight)) {
|
||||
return 1;
|
||||
}
|
||||
const normalized = Math.max(1, Math.ceil(weight));
|
||||
return HEX_CELL_BUCKETS.find((bucket) => normalized <= bucket) ?? 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn model memory into a deliberately coarse footprint.
|
||||
*
|
||||
* A 128 GB Mac is the common mesh workhorse, so every model that fits on one is
|
||||
* one cell. Multi-cell territories are reserved for genuinely multi-GPU scale
|
||||
* deployments; this avoids making ordinary local models look fragmented.
|
||||
*/
|
||||
export function capacityGbToHexCellCount(
|
||||
capacityGb: number | null | undefined,
|
||||
): HexCellCount {
|
||||
if (capacityGb === null || capacityGb === undefined || capacityGb <= 128) {
|
||||
return 1;
|
||||
}
|
||||
if (capacityGb <= 256) {
|
||||
return 2;
|
||||
}
|
||||
if (capacityGb <= 512) {
|
||||
return 4;
|
||||
}
|
||||
return 7;
|
||||
}
|
||||
|
||||
const EMPTY_HEALTH_COUNTS: Record<CommunityComputeDeploymentHealth, number> = {
|
||||
healthy: 0,
|
||||
warming: 0,
|
||||
idle: 0,
|
||||
degraded: 0,
|
||||
offline: 0,
|
||||
unknown: 0,
|
||||
};
|
||||
|
||||
/** Project a backend snapshot into the complete, render-agnostic map model. */
|
||||
export function deriveCommunityComputeMapModel<
|
||||
TDevice extends MeshSnapshotDevice = CommunityComputeDeviceInput,
|
||||
>(
|
||||
snapshot: CommunityComputeSnapshotInput<TDevice> | null,
|
||||
): CommunityComputeMapModel<TDevice> {
|
||||
if (snapshot === null) {
|
||||
return {
|
||||
status: "loading",
|
||||
kpis: {
|
||||
memberCount: 0,
|
||||
contributorMemberCount: 0,
|
||||
deploymentCount: 0,
|
||||
activeDeploymentCount: 0,
|
||||
sharedCapacityGb: null,
|
||||
allocatedCapacityGb: null,
|
||||
allocatedPercent: null,
|
||||
modelCount: 0,
|
||||
},
|
||||
health: {
|
||||
status: "unknown",
|
||||
label: "Checking community compute",
|
||||
reason: null,
|
||||
counts: { ...EMPTY_HEALTH_COUNTS },
|
||||
},
|
||||
deployments: [],
|
||||
};
|
||||
}
|
||||
|
||||
const deployments = projectDeployments(snapshot.devices);
|
||||
const healthStates = snapshot.devices
|
||||
.filter((device) => device.state !== "consuming")
|
||||
.map((device) => healthFromState(device.state));
|
||||
const counts = countHealth(healthStates);
|
||||
const models = new Set(snapshot.models);
|
||||
for (const deployment of deployments) {
|
||||
for (const model of deployment.models) {
|
||||
models.add(model);
|
||||
}
|
||||
}
|
||||
|
||||
const contributorMemberCount =
|
||||
snapshot.contributorMemberCount ??
|
||||
new Set(
|
||||
deployments
|
||||
.filter((deployment) => deployment.state === "serving")
|
||||
.map(
|
||||
(deployment) =>
|
||||
deployment.source.memberPubkey?.trim() || deployment.id,
|
||||
),
|
||||
).size;
|
||||
const sharedCapacityGb = finiteNonNegative(snapshot.sharedCapacityGb);
|
||||
const reportedAllocated = finiteNonNegative(snapshot.allocatedCapacityGb);
|
||||
const servingDeployments = deployments.filter(
|
||||
(deployment) => deployment.state === "serving",
|
||||
);
|
||||
const inferredAllocations = servingDeployments.map((deployment) =>
|
||||
finiteNonNegative(deployment.source.modelSizeGb),
|
||||
);
|
||||
const allocatedCapacityGb =
|
||||
reportedAllocated ??
|
||||
(inferredAllocations.length > 0 &&
|
||||
inferredAllocations.every((value) => value !== null)
|
||||
? inferredAllocations.reduce((total, value) => total + (value ?? 0), 0)
|
||||
: null);
|
||||
|
||||
return {
|
||||
status: deployments.length === 0 ? "empty" : "ready",
|
||||
kpis: {
|
||||
memberCount: nonNegativeInteger(snapshot.memberCount),
|
||||
contributorMemberCount,
|
||||
deploymentCount: deployments.length,
|
||||
activeDeploymentCount: counts.healthy + counts.warming,
|
||||
sharedCapacityGb,
|
||||
allocatedCapacityGb,
|
||||
allocatedPercent:
|
||||
allocatedCapacityGb !== null &&
|
||||
sharedCapacityGb !== null &&
|
||||
sharedCapacityGb > 0
|
||||
? Math.min(100, (allocatedCapacityGb / sharedCapacityGb) * 100)
|
||||
: null,
|
||||
modelCount: models.size,
|
||||
},
|
||||
health: summarizeHealth(counts, healthStates, snapshot.reason),
|
||||
deployments,
|
||||
};
|
||||
}
|
||||
|
||||
function projectDeployments<TDevice extends MeshSnapshotDevice>(
|
||||
devices: readonly TDevice[],
|
||||
): CommunityComputeDeployment<TDevice>[] {
|
||||
const canonical = devices
|
||||
.filter((device) => device.state === "serving" && device.models.length > 0)
|
||||
.flatMap((device) =>
|
||||
[...new Set(device.models)].map((model) => ({
|
||||
device: { ...device, models: [model] } as TDevice,
|
||||
identity: deploymentIdentity(device, model),
|
||||
})),
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
left.identity.localeCompare(right.identity) ||
|
||||
deviceFingerprint(left.device).localeCompare(
|
||||
deviceFingerprint(right.device),
|
||||
),
|
||||
);
|
||||
const occurrences = new Map<string, number>();
|
||||
|
||||
return canonical.map(({ device, identity }) => {
|
||||
const occurrence = occurrences.get(identity) ?? 0;
|
||||
occurrences.set(identity, occurrence + 1);
|
||||
const models = [...new Set(device.models)].sort();
|
||||
return {
|
||||
id: occurrence === 0 ? identity : `${identity}#${occurrence + 1}`,
|
||||
label: models[0] ?? device.label,
|
||||
modelId: models[0] ?? "Model not reported",
|
||||
deviceLabel: device.label,
|
||||
capacityGb: finiteNonNegative(device.capacityGb),
|
||||
models,
|
||||
state: device.state,
|
||||
isSelf: device.isSelf,
|
||||
health: healthFromState(device.state),
|
||||
healthReason: null,
|
||||
cellCount: capacityGbToHexCellCount(
|
||||
device.modelSizeGb ?? device.capacityGb,
|
||||
),
|
||||
source: device,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function deploymentIdentity(
|
||||
device: MeshSnapshotDevice,
|
||||
modelId: string,
|
||||
): string {
|
||||
const deviceId = cleanOptionalText(device.deviceId);
|
||||
const model = encodeURIComponent(modelId);
|
||||
return deviceId === null
|
||||
? `derived:${encodeURIComponent(deviceFingerprint(device))}:${model}`
|
||||
: `deployment:${encodeURIComponent(deviceId)}:${model}`;
|
||||
}
|
||||
|
||||
function deviceFingerprint(device: MeshSnapshotDevice): string {
|
||||
return JSON.stringify([
|
||||
device.label,
|
||||
device.capacityGb,
|
||||
[...device.models].sort(),
|
||||
device.state,
|
||||
device.isSelf,
|
||||
]);
|
||||
}
|
||||
|
||||
function healthFromState(
|
||||
state: MeshDeviceState,
|
||||
): CommunityComputeDeploymentHealth {
|
||||
switch (state) {
|
||||
case "serving":
|
||||
case "consuming":
|
||||
return "healthy";
|
||||
case "loading":
|
||||
return "warming";
|
||||
case "standby":
|
||||
return "idle";
|
||||
}
|
||||
}
|
||||
|
||||
function countHealth(
|
||||
states: readonly CommunityComputeDeploymentHealth[],
|
||||
): Record<CommunityComputeDeploymentHealth, number> {
|
||||
const counts = { ...EMPTY_HEALTH_COUNTS };
|
||||
for (const state of states) {
|
||||
counts[state] += 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function summarizeHealth(
|
||||
counts: Record<CommunityComputeDeploymentHealth, number>,
|
||||
states: readonly CommunityComputeDeploymentHealth[],
|
||||
snapshotReason: string | null,
|
||||
): CommunityComputeHealthSummary {
|
||||
if (states.length === 0) {
|
||||
return {
|
||||
status: "empty",
|
||||
label: "No deployments",
|
||||
reason: cleanOptionalText(snapshotReason),
|
||||
counts,
|
||||
};
|
||||
}
|
||||
|
||||
const unavailable = counts.offline + counts.degraded;
|
||||
const reason = null;
|
||||
|
||||
if (counts.offline === states.length) {
|
||||
return { status: "critical", label: "Compute unavailable", reason, counts };
|
||||
}
|
||||
if (unavailable > 0) {
|
||||
return {
|
||||
status: "attention",
|
||||
label: `${unavailable} deployment${unavailable === 1 ? "" : "s"} need attention`,
|
||||
reason,
|
||||
counts,
|
||||
};
|
||||
}
|
||||
if (counts.unknown === states.length) {
|
||||
return { status: "unknown", label: "Health unknown", reason: null, counts };
|
||||
}
|
||||
return {
|
||||
status: "healthy",
|
||||
label: "No reported issues",
|
||||
reason: null,
|
||||
counts,
|
||||
};
|
||||
}
|
||||
|
||||
function finiteNonNegative(value: number | null | undefined): number | null {
|
||||
return value !== null &&
|
||||
value !== undefined &&
|
||||
Number.isFinite(value) &&
|
||||
value >= 0
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value: number): number {
|
||||
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
||||
}
|
||||
|
||||
function cleanOptionalText(value: string | null | undefined): string | null {
|
||||
const cleaned = value?.trim();
|
||||
return cleaned ? cleaned : null;
|
||||
}
|
||||
@@ -55,7 +55,11 @@ export function useMeshComputeState() {
|
||||
// transition, instead of waiting out the 30s poll.
|
||||
const [snapshotNonce, setSnapshotNonce] = React.useState(0);
|
||||
|
||||
const { snapshot, error: snapshotError } = useMeshSnapshot({
|
||||
const {
|
||||
snapshot,
|
||||
error: snapshotError,
|
||||
refresh: refreshSnapshot,
|
||||
} = useMeshSnapshot({
|
||||
refreshKey: snapshotNonce,
|
||||
});
|
||||
|
||||
@@ -243,6 +247,7 @@ export function useMeshComputeState() {
|
||||
shareSwitchChecked,
|
||||
modelToShare,
|
||||
setSharing,
|
||||
refreshSnapshot,
|
||||
// A transport failure is worth showing; an empty mesh is not an error and
|
||||
// is already expressed by the row tone.
|
||||
error: actionError ?? snapshotError,
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import {
|
||||
EMPTY_COMMUNITY_COMPUTE_FIXTURE,
|
||||
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";
|
||||
|
||||
const smallModel = deriveCommunityComputeMapModel(
|
||||
SMALL_COMPANY_COMMUNITY_COMPUTE_FIXTURE.snapshot,
|
||||
);
|
||||
|
||||
describe("community compute visual components", () => {
|
||||
it("renders contract-backed KPI values", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(CommunityComputeKpiRow, { kpis: smallModel.kpis }),
|
||||
);
|
||||
assert.match(html, /community-compute-kpi-members-sharing/);
|
||||
assert.match(html, /community-compute-kpi-contributed-vram/);
|
||||
assert.match(html, /community-compute-kpi-vram-allocated/);
|
||||
assert.match(html, /24 community members/);
|
||||
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", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(CommunityComputeTerritoryMap, { model: smallModel }),
|
||||
);
|
||||
assert.match(html, /data-testid="community-compute-map-svg"/);
|
||||
assert.equal(
|
||||
[
|
||||
...html.matchAll(
|
||||
/data-testid="community-compute-territory-deployment-/g,
|
||||
),
|
||||
].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/);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const emptyModel = deriveCommunityComputeMapModel(
|
||||
EMPTY_COMMUNITY_COMPUTE_FIXTURE.snapshot,
|
||||
);
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(CommunityComputeTerritoryMap, { model: emptyModel }),
|
||||
);
|
||||
assert.match(html, /data-testid="community-compute-map-empty"/);
|
||||
assert.doesNotMatch(html, /community-compute-deployment-list/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
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.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import type { CommunityComputeKpis } from "../communityComputeMapModel";
|
||||
|
||||
export type CommunityComputeKpiRowProps = {
|
||||
kpis: CommunityComputeKpis;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** Compact, responsive headline figures for the community compute pool. */
|
||||
export function CommunityComputeKpiRow({
|
||||
kpis,
|
||||
className,
|
||||
}: CommunityComputeKpiRowProps) {
|
||||
const items = [
|
||||
{
|
||||
label: "Members sharing",
|
||||
value: formatCount(kpis.contributorMemberCount),
|
||||
detail: `${formatCount(kpis.memberCount)} community members`,
|
||||
},
|
||||
{
|
||||
label: "Contributed VRAM",
|
||||
value: formatCapacity(kpis.sharedCapacityGb),
|
||||
detail: `${formatCount(kpis.deploymentCount)} deployments`,
|
||||
},
|
||||
{
|
||||
label: "VRAM allocated",
|
||||
value:
|
||||
kpis.allocatedPercent === null
|
||||
? "—"
|
||||
: `${Math.round(kpis.allocatedPercent)}%`,
|
||||
detail:
|
||||
kpis.allocatedCapacityGb === null
|
||||
? "Not fully reported"
|
||||
: `${formatCapacity(kpis.allocatedCapacityGb)} hosting models`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<dl
|
||||
className={cn(
|
||||
"grid grid-cols-1 overflow-hidden rounded-xl border border-border/70 bg-muted/15 sm:grid-cols-3",
|
||||
className,
|
||||
)}
|
||||
data-testid="community-compute-kpis"
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 px-3 py-2.5",
|
||||
index > 0 && "border-border/60 border-t sm:border-l sm:border-t-0",
|
||||
)}
|
||||
data-testid={`community-compute-kpi-${kpiTestId(item.label)}`}
|
||||
key={item.label}
|
||||
>
|
||||
<dt className="truncate text-2xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{item.label}
|
||||
</dt>
|
||||
<dd className="mt-1 truncate text-xl font-semibold tabular-nums">
|
||||
{item.value}
|
||||
</dd>
|
||||
<dd className="mt-0.5 truncate text-2xs text-muted-foreground">
|
||||
{item.detail}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
function formatCount(value: number): string {
|
||||
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
function formatCapacity(value: number | null): string {
|
||||
if (value === null) return "—";
|
||||
return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(value)} GB`;
|
||||
}
|
||||
|
||||
function kpiTestId(label: string): string {
|
||||
return label.toLowerCase().replaceAll(" ", "-");
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import {
|
||||
hexTerritoryBoundaryEdges,
|
||||
layoutCommunityComputeHexTerritories,
|
||||
type CommunityComputeHexCell,
|
||||
} 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[];
|
||||
};
|
||||
|
||||
type Point = { x: number; y: number };
|
||||
|
||||
type ViewBox = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
// Deliberately overlap polygons, then trim their outside with the territory
|
||||
// edge. This avoids SVG antialias seams inside one model footprint.
|
||||
const FUSED_TILE_RADIUS = 1.025;
|
||||
const TILE_EDGE_WIDTH = 0.1;
|
||||
const SQRT_THREE = Math.sqrt(3);
|
||||
|
||||
/**
|
||||
* Responsive SVG territory map. Each deployment owns one stable, contiguous
|
||||
* cluster of discrete hexes; health changes never move that cluster.
|
||||
*/
|
||||
export function CommunityComputeTerritoryMap({
|
||||
model,
|
||||
selectedDeploymentId,
|
||||
onSelectedDeploymentChange,
|
||||
className,
|
||||
inferenceDeploymentIds = [],
|
||||
}: CommunityComputeTerritoryMapProps) {
|
||||
const [internalSelection, setInternalSelection] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const deploymentById = React.useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
model.deployments.map((deployment) => [deployment.id, deployment]),
|
||||
),
|
||||
[model.deployments],
|
||||
);
|
||||
const previousLayoutRef = React.useRef<ReturnType<
|
||||
typeof layoutCommunityComputeHexTerritories
|
||||
> | null>(null);
|
||||
const layout = React.useMemo(
|
||||
() =>
|
||||
layoutCommunityComputeHexTerritories(
|
||||
model.deployments.map(({ id, cellCount }) => ({ id, cellCount })),
|
||||
{ previous: previousLayoutRef.current },
|
||||
),
|
||||
[model.deployments],
|
||||
);
|
||||
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 [deploymentSearch, setDeploymentSearch] = React.useState("");
|
||||
const hoveredDeployment = hoveredId
|
||||
? (deploymentById.get(hoveredId) ?? null)
|
||||
: null;
|
||||
|
||||
function selectDeployment(deploymentId: string) {
|
||||
if (selectedDeploymentId === undefined) {
|
||||
setInternalSelection(deploymentId);
|
||||
}
|
||||
onSelectedDeploymentChange?.(deploymentId);
|
||||
}
|
||||
|
||||
if (model.status === "loading") {
|
||||
return (
|
||||
<section
|
||||
aria-busy="true"
|
||||
aria-label="Community compute map"
|
||||
className={cn(
|
||||
"rounded-xl border border-border/70 bg-muted/10 p-4",
|
||||
className,
|
||||
)}
|
||||
data-testid="community-compute-map-loading"
|
||||
>
|
||||
<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…
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (model.status === "empty" || viewBox === null) {
|
||||
return (
|
||||
<section
|
||||
aria-label="Community compute map"
|
||||
className={cn(
|
||||
"flex min-h-48 flex-col items-center justify-center rounded-xl border border-dashed border-border px-5 py-8 text-center",
|
||||
className,
|
||||
)}
|
||||
data-testid="community-compute-map-empty"
|
||||
>
|
||||
<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.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Community compute map"
|
||||
className={cn(
|
||||
"min-w-0 space-y-4 rounded-xl border border-border/70 bg-muted/10 p-3",
|
||||
className,
|
||||
)}
|
||||
data-testid="community-compute-territory-map"
|
||||
>
|
||||
<div className="relative flex min-h-72 min-w-0 items-center justify-center overflow-hidden rounded-lg border border-border/50 bg-background/70 p-3 sm:min-h-96">
|
||||
<svg
|
||||
aria-label={`${model.deployments.length} community compute deployments. Select a territory for details.`}
|
||||
className="h-auto max-h-[28rem] w-full overflow-visible"
|
||||
data-testid="community-compute-map-svg"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
role="img"
|
||||
viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`}
|
||||
>
|
||||
{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.
|
||||
<g
|
||||
aria-label={territoryAccessibleLabel(deployment)}
|
||||
className="cursor-pointer outline-hidden transition-opacity hover:opacity-90 focus-visible:opacity-80"
|
||||
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);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setHoveredId(deployment.id)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<title>{territoryAccessibleLabel(deployment)}</title>
|
||||
<TerritoryShape
|
||||
cells={territory.cells}
|
||||
inferenceActive={inferenceActive}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
{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"
|
||||
data-testid="community-compute-hover-details"
|
||||
>
|
||||
<p
|
||||
className="truncate text-xs font-semibold"
|
||||
title={hoveredDeployment.modelId}
|
||||
>
|
||||
{shortModelName(hoveredDeployment.modelId)}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-2xs text-muted-foreground">
|
||||
{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>
|
||||
</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 TerritoryShape({
|
||||
cells,
|
||||
inferenceActive,
|
||||
}: {
|
||||
cells: readonly CommunityComputeHexCell[];
|
||||
inferenceActive: boolean;
|
||||
}) {
|
||||
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;
|
||||
return (
|
||||
<g>
|
||||
{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",
|
||||
)}
|
||||
data-testid="community-compute-tile"
|
||||
key={cell.id}
|
||||
points={hexPoints(center, FUSED_TILE_RADIUS)}
|
||||
style={territoryAnimationStyle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<path
|
||||
className={cn(
|
||||
"fill-none stroke-background transition-[opacity,stroke-width] duration-200 motion-reduce:transition-none",
|
||||
fillClass,
|
||||
)}
|
||||
d={edgesToPath(boundary)}
|
||||
data-testid="community-compute-territory-boundary"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
// A crisp opaque perimeter cuts an empty gutter between territories.
|
||||
strokeWidth={TILE_EDGE_WIDTH}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function edgesToPath(
|
||||
edges: ReturnType<typeof hexTerritoryBoundaryEdges>,
|
||||
): string {
|
||||
return edges
|
||||
.map(
|
||||
(edge) =>
|
||||
`M ${edge.start.x} ${edge.start.y} L ${edge.end.x} ${edge.end.y}`,
|
||||
)
|
||||
.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 },
|
||||
{ x: 3.58, y: 2.65 },
|
||||
{ x: 2, y: 3.55 },
|
||||
{ x: 0.42, y: 2.65 },
|
||||
];
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-14 w-20" viewBox="-0.7 0.5 5.4 4.3">
|
||||
{centers.map((center) => (
|
||||
<polygon
|
||||
className="fill-muted/40 stroke-muted-foreground/40"
|
||||
key={`${center.x}:${center.y}`}
|
||||
points={hexPoints(center, 0.82)}
|
||||
strokeDasharray="2 2"
|
||||
strokeWidth="0.08"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function axialToPoint(q: number, r: number): Point {
|
||||
return { x: SQRT_THREE * (q + r / 2), y: 1.5 * r };
|
||||
}
|
||||
|
||||
function hexPoints(center: Point, radius: number): string {
|
||||
return Array.from({ length: 6 }, (_, index) => {
|
||||
const angle = ((60 * index - 30) * Math.PI) / 180;
|
||||
return `${center.x + radius * Math.cos(angle)},${center.y + radius * Math.sin(angle)}`;
|
||||
}).join(" ");
|
||||
}
|
||||
|
||||
function computeViewBox(
|
||||
cells: readonly CommunityComputeHexCell[],
|
||||
): ViewBox | null {
|
||||
if (cells.length === 0) return null;
|
||||
const centers = cells.map((cell) => axialToPoint(cell.q, cell.r));
|
||||
const minX = Math.min(...centers.map(({ x }) => x)) - 1.4;
|
||||
const maxX = Math.max(...centers.map(({ x }) => x)) + 1.4;
|
||||
const minY = Math.min(...centers.map(({ y }) => y)) - 1.4;
|
||||
const maxY = Math.max(...centers.map(({ y }) => y)) + 1.4;
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: Math.max(2.8, maxX - minX),
|
||||
height: Math.max(2.8, maxY - minY),
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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"}`;
|
||||
}
|
||||
|
||||
function formatCapacity(value: number | null): string {
|
||||
return value === null
|
||||
? "Capacity not reported"
|
||||
: `${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("%", "-");
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Info, RefreshCw } from "lucide-react";
|
||||
|
||||
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";
|
||||
|
||||
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;
|
||||
|
||||
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 ? (
|
||||
<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}
|
||||
</span>
|
||||
<Button onClick={onRefresh} size="xs" type="button" variant="outline">
|
||||
<RefreshCw /> Try again
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CommunityComputeKpiRow kpis={model.kpis} />
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex items-end justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold tracking-tight">Model mesh</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Each territory is one ready model on one device. Area reflects its
|
||||
reported model-memory tier.
|
||||
</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}
|
||||
</div>
|
||||
<CommunityComputeTerritoryMap
|
||||
inferenceDeploymentIds={inferenceDeploymentIds}
|
||||
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"}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
function formatAge(timestamp: number): string {
|
||||
const timestampMs = timestamp < 10_000_000_000 ? timestamp * 1000 : timestamp;
|
||||
const seconds = Math.max(0, Math.round((Date.now() - timestampMs) / 1000));
|
||||
if (seconds < 10) return "just now";
|
||||
if (seconds < 60) return `${seconds}s ago`;
|
||||
return `${Math.floor(seconds / 60)}m ago`;
|
||||
}
|
||||
103
desktop/src/features/mesh-compute/ui/MeshComputePage.tsx
Normal file
103
desktop/src/features/mesh-compute/ui/MeshComputePage.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
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 { MeshComputeSettingsCard } from "./MeshComputeSettingsCard";
|
||||
import { MeshComputeShareBanner } from "./MeshComputeShareBanner";
|
||||
|
||||
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={`See what ${communityName} can run and contribute this machine’s spare capacity.`}
|
||||
title="Compute"
|
||||
/>
|
||||
|
||||
<div className="space-y-5">
|
||||
{tutorialOpen ? null : (
|
||||
<MeshComputeShareBanner communityName={communityName} mesh={mesh} />
|
||||
)}
|
||||
|
||||
<Tabs onValueChange={setTab} value={tab}>
|
||||
<TabsList className="grid w-full grid-cols-2 sm:w-72">
|
||||
<TabsTrigger data-testid="compute-tab-community" value="community">
|
||||
Community
|
||||
</TabsTrigger>
|
||||
<TabsTrigger data-testid="compute-tab-settings" value="settings">
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent className="mt-5 space-y-5" value="community">
|
||||
{tutorialOpen ? (
|
||||
<CommunityComputeTutorialPanel
|
||||
fixtureId={fixtureId}
|
||||
onExit={() => setTutorialOpen(false)}
|
||||
onFixtureChange={setFixtureId}
|
||||
onStepChange={setTutorialStep}
|
||||
step={tutorialStep}
|
||||
/>
|
||||
) : null}
|
||||
<MeshComputeCommunityView
|
||||
communityName={
|
||||
tutorialOpen ? tutorialFixture.title : communityName
|
||||
}
|
||||
error={mesh.error}
|
||||
onRefresh={mesh.refreshSnapshot}
|
||||
simulated={tutorialOpen}
|
||||
snapshot={tutorialOpen ? tutorialFixture.snapshot : mesh.snapshot}
|
||||
tutorialInference={tutorialOpen}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent className="mt-8" value="settings">
|
||||
<MeshComputeSettingsCard hideHeader />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -68,7 +68,11 @@ const SHARE_COMPUTE_REVEAL_TRANSITION = {
|
||||
* group. User-facing copy describes the shared-compute behavior without
|
||||
* exposing implementation protocols or raw mesh controls.
|
||||
*/
|
||||
export function MeshComputeSettingsCard() {
|
||||
export function MeshComputeSettingsCard({
|
||||
hideHeader = false,
|
||||
}: {
|
||||
hideHeader?: boolean;
|
||||
} = {}) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const { status, error, refresh } = useMeshNodeStatus();
|
||||
const [installedModels, setInstalledModels] = React.useState<
|
||||
@@ -203,10 +207,12 @@ export function MeshComputeSettingsCard() {
|
||||
|
||||
return (
|
||||
<section className="min-w-0" data-testid="settings-mesh-share-compute">
|
||||
<SettingsSectionHeader
|
||||
title="Share compute"
|
||||
description="Provide compute to your Buzz community. More capacity can increase intelligence and availability of models for agents."
|
||||
/>
|
||||
{hideHeader ? null : (
|
||||
<SettingsSectionHeader
|
||||
title="Share compute"
|
||||
description="Provide compute to your Buzz community. More capacity can increase intelligence and availability of models for agents."
|
||||
/>
|
||||
)}
|
||||
|
||||
{error ? (
|
||||
<p className="mb-3 rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
|
||||
113
desktop/src/features/mesh-compute/ui/MeshComputeShareBanner.tsx
Normal file
113
desktop/src/features/mesh-compute/ui/MeshComputeShareBanner.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Cpu, LoaderCircle } from "lucide-react";
|
||||
|
||||
import { Switch } from "@/shared/ui/switch";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import type { useMeshComputeState } from "../hooks/useMeshComputeState";
|
||||
|
||||
export function MeshComputeShareBanner({
|
||||
communityName,
|
||||
mesh,
|
||||
}: {
|
||||
communityName: string;
|
||||
mesh: ReturnType<typeof useMeshComputeState>;
|
||||
}) {
|
||||
const {
|
||||
error,
|
||||
modelToShare,
|
||||
pendingAction,
|
||||
shareSwitchChecked,
|
||||
status,
|
||||
toggle,
|
||||
setSharing,
|
||||
} = mesh;
|
||||
const busy = pendingAction !== null;
|
||||
const statusCopy = shareStatusCopy({
|
||||
communityName,
|
||||
modelToShare,
|
||||
pendingAction,
|
||||
statusModel: status?.modelName ?? status?.modelId ?? null,
|
||||
isConsuming: toggle.isConsuming,
|
||||
isSharing: toggle.isSharing,
|
||||
});
|
||||
|
||||
return (
|
||||
<section
|
||||
className="rounded-2xl border border-border/70 bg-muted/15 px-4 py-3.5"
|
||||
data-testid="compute-share-banner"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"flex size-9 shrink-0 items-center justify-center rounded-xl border border-border/70 bg-background",
|
||||
toggle.isSharing && "border-foreground/30",
|
||||
)}
|
||||
>
|
||||
{busy ? (
|
||||
<LoaderCircle className="size-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Cpu className="size-4" />
|
||||
)}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<label
|
||||
className="text-sm font-semibold"
|
||||
htmlFor="compute-share-banner-toggle"
|
||||
>
|
||||
Share this machine
|
||||
</label>
|
||||
<p
|
||||
className="mt-0.5 truncate text-xs text-muted-foreground"
|
||||
data-testid="compute-share-banner-status"
|
||||
title={statusCopy}
|
||||
>
|
||||
{statusCopy}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={shareSwitchChecked}
|
||||
data-testid="compute-share-banner-toggle"
|
||||
disabled={busy || (!toggle.isSharing && !modelToShare)}
|
||||
id="compute-share-banner-toggle"
|
||||
onCheckedChange={setSharing}
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p className="mt-2 text-xs text-destructive" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function shareStatusCopy({
|
||||
communityName,
|
||||
isConsuming,
|
||||
isSharing,
|
||||
modelToShare,
|
||||
pendingAction,
|
||||
statusModel,
|
||||
}: {
|
||||
communityName: string;
|
||||
isConsuming: boolean;
|
||||
isSharing: boolean;
|
||||
modelToShare: string | null;
|
||||
pendingAction: "start" | "stop" | null;
|
||||
statusModel: string | null;
|
||||
}): string {
|
||||
if (pendingAction === "start") {
|
||||
return `Preparing ${statusModel ?? modelToShare ?? "a model"} for ${communityName}…`;
|
||||
}
|
||||
if (pendingAction === "stop") return "Stopping shared compute…";
|
||||
if (isSharing) {
|
||||
return `${statusModel ?? modelToShare ?? "A model"} is available to ${communityName}`;
|
||||
}
|
||||
if (isConsuming) {
|
||||
return "Using another member’s compute; turn this on to contribute instead";
|
||||
}
|
||||
if (modelToShare) {
|
||||
return `${modelToShare} is ready to contribute to ${communityName}`;
|
||||
}
|
||||
return "Choose a model in Settings before sharing";
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { cn } from "@/shared/lib/cn";
|
||||
|
||||
import { useMeshComputeState } from "../hooks/useMeshComputeState";
|
||||
import type { MeshRowPulse, MeshRowTone } from "../meshRowModel";
|
||||
import { MeshComputePopover } from "./MeshComputePopover";
|
||||
|
||||
/**
|
||||
* The sidebar Shared Compute row.
|
||||
@@ -97,49 +96,42 @@ export function SidebarMeshComputeRow({
|
||||
const mesh = useMeshComputeState();
|
||||
const { row, toggle, pendingAction, shareSwitchChecked, setSharing } = mesh;
|
||||
|
||||
// The row offers joining; the popover owns managing. Once this machine is
|
||||
// sharing there is nothing left to invite, and the popover's labelled switch
|
||||
// is the place to stop — a bare unlabelled switch is a poor control for an
|
||||
// action with consequences.
|
||||
// Clicking the row is navigation: Shared Compute opens the community Compute
|
||||
// view. The trailing switch remains a separate action and does not navigate.
|
||||
const showJoinToggle = !toggle.isSharing;
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<MeshComputePopover
|
||||
mesh={mesh}
|
||||
onOpenComputeSettings={onOpenComputeSettings}
|
||||
<SidebarMenuButton
|
||||
className={cn(
|
||||
"data-[active=true]:font-normal",
|
||||
// Reserve the trailing slot so the label truncates before it runs
|
||||
// under the switch, rather than colliding with it.
|
||||
showJoinToggle && "pr-11 group-data-[collapsible=icon]:!pr-0",
|
||||
)}
|
||||
data-mesh-tone={row.tone}
|
||||
data-testid="sidebar-mesh-compute-row"
|
||||
onClick={onOpenComputeSettings}
|
||||
tooltip={row.tooltip}
|
||||
type="button"
|
||||
>
|
||||
<SidebarMenuButton
|
||||
className={cn(
|
||||
"data-[active=true]:font-normal",
|
||||
// Reserve the trailing slot so the label truncates before it runs
|
||||
// under the switch, rather than colliding with it.
|
||||
showJoinToggle && "pr-11 group-data-[collapsible=icon]:!pr-0",
|
||||
)}
|
||||
data-mesh-tone={row.tone}
|
||||
data-testid="sidebar-mesh-compute-row"
|
||||
tooltip={row.tooltip}
|
||||
type="button"
|
||||
<span
|
||||
aria-hidden
|
||||
className="relative flex h-4 w-4 shrink-0 items-center justify-center"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="relative flex h-4 w-4 shrink-0 items-center justify-center"
|
||||
>
|
||||
<Waypoints className="h-4 w-4 opacity-80" />
|
||||
{/*
|
||||
<Waypoints className="h-4 w-4 opacity-80" />
|
||||
{/*
|
||||
Icon-collapsed only. The label and the dot beside it are both
|
||||
clipped in icon mode, so without this the row would lose its status
|
||||
signal entirely at exactly the width where the tooltip is the only
|
||||
other affordance.
|
||||
*/}
|
||||
<span className="absolute -right-1 -bottom-1 hidden rounded-full bg-sidebar p-px group-data-[collapsible=icon]:block">
|
||||
<MeshPulseDot pulse={row.pulse} size="small" tone={row.tone} />
|
||||
</span>
|
||||
<span className="absolute -right-1 -bottom-1 hidden rounded-full bg-sidebar p-px group-data-[collapsible=icon]:block">
|
||||
<MeshPulseDot pulse={row.pulse} size="small" tone={row.tone} />
|
||||
</span>
|
||||
<SidebarMenuLabel className="opacity-80">
|
||||
{row.label}
|
||||
</SidebarMenuLabel>
|
||||
{/*
|
||||
</span>
|
||||
<SidebarMenuLabel className="opacity-80">{row.label}</SidebarMenuLabel>
|
||||
{/*
|
||||
Right-aligned, the same position and mechanism a channel row uses for
|
||||
its unread dot (`UnreadDotBadge className="ml-auto"`). Beside the name
|
||||
it competed with the label for one optical line; here it reads as this
|
||||
@@ -148,20 +140,18 @@ export function SidebarMeshComputeRow({
|
||||
When the join switch is present the button carries `pr-11`, so the dot
|
||||
lands clear of it rather than underneath.
|
||||
*/}
|
||||
<span className="ml-auto group-data-[collapsible=icon]:hidden">
|
||||
<MeshPulseDot
|
||||
pulse={row.pulse}
|
||||
testId="mesh-row-dot"
|
||||
tone={row.tone}
|
||||
/>
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</MeshComputePopover>
|
||||
<span className="ml-auto group-data-[collapsible=icon]:hidden">
|
||||
<MeshPulseDot
|
||||
pulse={row.pulse}
|
||||
testId="mesh-row-dot"
|
||||
tone={row.tone}
|
||||
/>
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
|
||||
{/*
|
||||
A sibling of the button, never a child: `PopoverTrigger asChild` makes the
|
||||
button itself the trigger, so a switch nested inside would be a button
|
||||
within a button and every toggle would also open the popover.
|
||||
A sibling of the navigation button, never a child: nesting this switch
|
||||
would make toggling sharing navigate away from the current view.
|
||||
*/}
|
||||
{showJoinToggle ? (
|
||||
<div className="absolute top-1/2 right-2 -translate-y-1/2 group-data-[collapsible=icon]:hidden">
|
||||
|
||||
@@ -77,7 +77,7 @@ import { ChannelTemplatesSettingsCard } from "./ChannelTemplatesSettingsCard";
|
||||
import { HarnessesSettingsPanel } from "./HarnessesSettingsPanel";
|
||||
import { ExperimentalFeaturesCard } from "./ExperimentalFeaturesCard";
|
||||
import { KeyboardShortcutsCard } from "./KeyboardShortcutsCard";
|
||||
import { MeshComputeSettingsCard } from "@/features/mesh-compute/ui/MeshComputeSettingsCard";
|
||||
import { MeshComputePage } from "@/features/mesh-compute/ui/MeshComputePage";
|
||||
import { MobilePairingCard } from "./MobilePairingCard";
|
||||
import { ModerationQueueCard } from "./ModerationQueueCard";
|
||||
import { NotificationSettingsCard } from "./NotificationSettingsCard";
|
||||
@@ -883,7 +883,7 @@ export function renderSettingsSection(
|
||||
case "channel-templates":
|
||||
return <ChannelTemplatesSettingsCard />;
|
||||
case "compute":
|
||||
return <MeshComputeSettingsCard />;
|
||||
return <MeshComputePage />;
|
||||
case "appearance":
|
||||
return <ThemeSettingsCard />;
|
||||
case "shortcuts":
|
||||
|
||||
@@ -156,6 +156,10 @@ export type MeshSnapshotDevice = {
|
||||
models: string[];
|
||||
state: MeshDeviceState;
|
||||
isSelf: boolean;
|
||||
/** Additive Community View fields. Older desktop/runtime pairs omit these. */
|
||||
memberPubkey?: string | null;
|
||||
reportedAt?: number | null;
|
||||
modelSizeGb?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -167,11 +171,17 @@ export type MeshSnapshotDevice = {
|
||||
*/
|
||||
export type MeshSnapshot = {
|
||||
sharingDeviceCount: number;
|
||||
/** Unique members with at least one ready serving device, when reported. */
|
||||
contributorMemberCount?: number;
|
||||
/** `null` when no sharing device reported a figure — render the count only. */
|
||||
sharedCapacityGb: number | null;
|
||||
/** Approximate hosted-model footprint when every contributor reports it. */
|
||||
allocatedCapacityGb?: number | null;
|
||||
models: string[];
|
||||
devices: MeshSnapshotDevice[];
|
||||
includesSelf: boolean;
|
||||
observedAt?: number;
|
||||
freshnessSeconds?: number;
|
||||
/**
|
||||
* Members in the community right now (NIP-43 roster size).
|
||||
*
|
||||
|
||||
@@ -1231,6 +1231,8 @@ declare global {
|
||||
*/
|
||||
nodeMode?: "serve" | "client" | null;
|
||||
/** Seed host-side serving usage to exercise the "who's using my compute" indicator. */
|
||||
snapshotDevices?: MockMeshSnapshotDevice[];
|
||||
snapshotMemberCount?: number;
|
||||
servingUsage?: Partial<{
|
||||
inflight: number;
|
||||
peakInflight: number;
|
||||
@@ -3014,6 +3016,18 @@ function resetMockPersonaCatalogEvents(config: E2eConfig | undefined) {
|
||||
// in a browser. They deliberately do NOT model real admission, real inference,
|
||||
// or real mesh routing — those are proven by the Rust layer-2 tests and the
|
||||
// on-hardware layer-1 example. Do not port any of this into production code.
|
||||
type MockMeshSnapshotDevice = {
|
||||
deviceId: string | null;
|
||||
label: string;
|
||||
capacityGb: number | null;
|
||||
models: string[];
|
||||
state: "serving" | "loading" | "standby" | "consuming";
|
||||
isSelf: boolean;
|
||||
memberPubkey?: string | null;
|
||||
reportedAt?: number | null;
|
||||
modelSizeGb?: number | null;
|
||||
};
|
||||
|
||||
type MockServingUsage = {
|
||||
inflight: number;
|
||||
peakInflight: number;
|
||||
@@ -3046,6 +3060,8 @@ const mockMeshState: {
|
||||
nodeState: "off" | "running";
|
||||
nodeMode: "serve" | "client" | null;
|
||||
servingUsage: MockServingUsage;
|
||||
snapshotDevices: MockMeshSnapshotDevice[] | null;
|
||||
snapshotMemberCount: number | null;
|
||||
} = {
|
||||
admitted: true,
|
||||
models: [{ id: "Gemma-4-E4B-it-Q4_K_M", name: "Gemma 4 E4B" }],
|
||||
@@ -3054,6 +3070,8 @@ const mockMeshState: {
|
||||
nodeState: "off",
|
||||
nodeMode: null,
|
||||
servingUsage: { ...ZERO_SERVING_USAGE },
|
||||
snapshotDevices: null,
|
||||
snapshotMemberCount: null,
|
||||
};
|
||||
|
||||
function resetMockMesh() {
|
||||
@@ -3064,6 +3082,8 @@ function resetMockMesh() {
|
||||
mockMeshState.nodeState = "off";
|
||||
mockMeshState.nodeMode = null;
|
||||
mockMeshState.servingUsage = { ...ZERO_SERVING_USAGE };
|
||||
mockMeshState.snapshotDevices = null;
|
||||
mockMeshState.snapshotMemberCount = null;
|
||||
}
|
||||
let mockPersonas: RawPersona[] = [];
|
||||
let mockTeams: RawTeam[] = [];
|
||||
@@ -10253,6 +10273,10 @@ export function maybeInstallE2eTauriMocks() {
|
||||
mockMeshState.denyReason = mesh.denyReason;
|
||||
if (mesh.nodeState !== undefined) mockMeshState.nodeState = mesh.nodeState;
|
||||
if (mesh.nodeMode !== undefined) mockMeshState.nodeMode = mesh.nodeMode;
|
||||
if (mesh.snapshotDevices !== undefined)
|
||||
mockMeshState.snapshotDevices = mesh.snapshotDevices;
|
||||
if (mesh.snapshotMemberCount !== undefined)
|
||||
mockMeshState.snapshotMemberCount = mesh.snapshotMemberCount;
|
||||
if (mesh.servingUsage !== undefined)
|
||||
mockMeshState.servingUsage = {
|
||||
...mockMeshState.servingUsage,
|
||||
@@ -10916,19 +10940,54 @@ export function maybeInstallE2eTauriMocks() {
|
||||
return meshNodeStatus(mockMeshState.nodeState, mockMeshState.nodeMode);
|
||||
case "mesh_serving_usage":
|
||||
return mockMeshState.servingUsage;
|
||||
case "mesh_snapshot":
|
||||
case "mesh_snapshot": {
|
||||
const devices =
|
||||
mockMeshState.snapshotDevices ??
|
||||
(mockMeshState.nodeMode === "serve" && mockMeshState.activeModel
|
||||
? [
|
||||
{
|
||||
deviceId: "mock-self",
|
||||
label: "This machine",
|
||||
capacityGb: 32,
|
||||
models: [mockMeshState.activeModel.id],
|
||||
state: "serving" as const,
|
||||
isSelf: true,
|
||||
memberPubkey: MOCK_IDENTITY_PUBKEY,
|
||||
reportedAt: Math.floor(Date.now() / 1000),
|
||||
modelSizeGb: 3.5,
|
||||
},
|
||||
]
|
||||
: []);
|
||||
const serving = devices.filter((device) => device.state === "serving");
|
||||
return {
|
||||
sharingDeviceCount: mockMeshState.nodeMode === "serve" ? 1 : 0,
|
||||
sharedCapacityGb: mockMeshState.nodeMode === "serve" ? 32 : null,
|
||||
models: mockMeshState.activeModel
|
||||
? [mockMeshState.activeModel.id]
|
||||
: [],
|
||||
devices: [],
|
||||
includesSelf: mockMeshState.nodeMode === "serve",
|
||||
memberCount: 1,
|
||||
reason:
|
||||
mockMeshState.nodeMode === "serve" ? null : "No one is sharing",
|
||||
sharingDeviceCount: serving.length,
|
||||
contributorMemberCount: new Set(
|
||||
serving.map((device) => device.memberPubkey ?? device.deviceId),
|
||||
).size,
|
||||
sharedCapacityGb:
|
||||
serving.length > 0
|
||||
? serving.reduce(
|
||||
(total, device) => total + (device.capacityGb ?? 0),
|
||||
0,
|
||||
)
|
||||
: null,
|
||||
allocatedCapacityGb:
|
||||
serving.length > 0 &&
|
||||
serving.every((device) => device.modelSizeGb != null)
|
||||
? serving.reduce(
|
||||
(total, device) => total + (device.modelSizeGb ?? 0),
|
||||
0,
|
||||
)
|
||||
: null,
|
||||
models: [...new Set(serving.flatMap((device) => device.models))],
|
||||
devices,
|
||||
includesSelf: serving.some((device) => device.isSelf),
|
||||
memberCount: mockMeshState.snapshotMemberCount ?? 1,
|
||||
observedAt: Math.floor(Date.now() / 1000),
|
||||
freshnessSeconds: 120,
|
||||
reason: serving.length > 0 ? null : "No one is sharing",
|
||||
};
|
||||
}
|
||||
case "mesh_live_view":
|
||||
return {
|
||||
connected: mockMeshState.nodeState === "running",
|
||||
|
||||
@@ -53,10 +53,18 @@ 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)) 25%, hsl(var(--background)))",
|
||||
},
|
||||
},
|
||||
},
|
||||
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",
|
||||
|
||||
36
desktop/tests/e2e/mesh-compute-screenshots.spec.ts
Normal file
36
desktop/tests/e2e/mesh-compute-screenshots.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import { openSettings } from "../helpers/settings";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
|
||||
test("captures the Community Compute tutorial", async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
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",
|
||||
});
|
||||
|
||||
await page.getByLabel("Preview scenario").selectOption("large-dense-500");
|
||||
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();
|
||||
await waitForAnimations(page);
|
||||
await page.getByTestId("settings-panel-compute").screenshot({
|
||||
path: "test-results/mesh-compute/community-dense.png",
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,17 @@ type E2eWindow = Window & {
|
||||
__BUZZ_E2E_SET_MESH__?: (mesh: {
|
||||
nodeState?: "off" | "running";
|
||||
nodeMode?: "serve" | "client" | null;
|
||||
snapshotMemberCount?: number;
|
||||
snapshotDevices?: Array<{
|
||||
deviceId: string;
|
||||
label: string;
|
||||
capacityGb: number;
|
||||
models: string[];
|
||||
state: "serving" | "loading" | "standby" | "consuming";
|
||||
isSelf: boolean;
|
||||
memberPubkey?: string;
|
||||
modelSizeGb?: number;
|
||||
}>;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
@@ -20,6 +31,7 @@ test("Share compute chooses a model before sharing", async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
await openSettings(page, "compute");
|
||||
await page.getByTestId("compute-tab-settings").click();
|
||||
|
||||
const card = page.getByTestId("settings-mesh-share-compute");
|
||||
const toggle = page.getByTestId("mesh-share-compute-toggle");
|
||||
@@ -115,6 +127,7 @@ test("a consuming client can switch to sharing its saved local model", async ({
|
||||
});
|
||||
});
|
||||
await openSettings(page, "compute");
|
||||
await page.getByTestId("compute-tab-settings").click();
|
||||
|
||||
const card = page.getByTestId("settings-mesh-share-compute");
|
||||
const toggle = page.getByTestId("mesh-share-compute-toggle");
|
||||
@@ -147,3 +160,134 @@ test("a consuming client can switch to sharing its saved local model", async ({
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Shared Compute sidebar row opens the Compute community view", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("sidebar-mesh-compute-row").click();
|
||||
|
||||
await expect(page.getByTestId("settings-mesh-compute-page")).toBeVisible();
|
||||
await expect(page.getByTestId("compute-tab-community")).toHaveAttribute(
|
||||
"data-state",
|
||||
"active",
|
||||
);
|
||||
await expect(page.getByTestId("community-compute-view")).toBeVisible();
|
||||
await expect(page.getByTestId("mesh-compute-popover")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Community Compute shows live KPIs and the tutorial simulation", 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: 7,
|
||||
snapshotDevices: [
|
||||
{
|
||||
deviceId: "alpha",
|
||||
label: "Alpha Studio",
|
||||
capacityGb: 64,
|
||||
models: ["qwen-32b"],
|
||||
state: "serving",
|
||||
isSelf: false,
|
||||
memberPubkey: "member-alpha",
|
||||
modelSizeGb: 28,
|
||||
},
|
||||
{
|
||||
deviceId: "beta",
|
||||
label: "Beta Workstation",
|
||||
capacityGb: 32,
|
||||
models: ["gemma-12b"],
|
||||
state: "serving",
|
||||
isSelf: false,
|
||||
memberPubkey: "member-beta",
|
||||
modelSizeGb: 12,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
await openSettings(page, "compute");
|
||||
|
||||
await expect(page.getByTestId("compute-share-banner")).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("community-compute-kpi-members-sharing"),
|
||||
).toContainText("2");
|
||||
await expect(
|
||||
page.getByTestId("community-compute-kpi-contributed-vram"),
|
||||
).toContainText("96 GB");
|
||||
await expect(
|
||||
page.getByTestId("community-compute-kpi-vram-allocated"),
|
||||
).toContainText("42%");
|
||||
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();
|
||||
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");
|
||||
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");
|
||||
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");
|
||||
await expect(
|
||||
page.getByTestId("community-compute-simulated-banner"),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId("community-compute-kpi-members-sharing"),
|
||||
).toContainText("2");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user