mirror of
https://github.com/exo-explore/exo.git
synced 2026-02-25 10:48:26 -05:00
Compare commits
1 Commits
remove-pyt
...
alexcheema
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c381ae64ad |
@@ -8,12 +8,16 @@
|
||||
nodeThunderboltBridge,
|
||||
type NodeInfo,
|
||||
} from "$lib/stores/app.svelte";
|
||||
import { getModelDownloadStatus } from "$lib/utils/downloads";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
highlightedNodes?: Set<string>;
|
||||
filteredNodes?: Set<string>;
|
||||
onNodeClick?: (nodeId: string) => void;
|
||||
downloadsData?: Record<string, unknown[]>;
|
||||
activeModelId?: string | null;
|
||||
onDownloadToNode?: (nodeId: string) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -21,6 +25,9 @@
|
||||
highlightedNodes = new Set(),
|
||||
filteredNodes = new Set(),
|
||||
onNodeClick,
|
||||
downloadsData,
|
||||
activeModelId = null,
|
||||
onDownloadToNode,
|
||||
}: Props = $props();
|
||||
|
||||
let svgContainer: SVGSVGElement | undefined = $state();
|
||||
@@ -907,6 +914,95 @@
|
||||
.attr("stroke-width", strokeWidth);
|
||||
}
|
||||
|
||||
// --- Download Status Indicator (top-right of device icon) ---
|
||||
if (activeModelId && downloadsData) {
|
||||
const dlStatus = getModelDownloadStatus(
|
||||
downloadsData,
|
||||
nodeInfo.id,
|
||||
activeModelId,
|
||||
);
|
||||
|
||||
if (dlStatus) {
|
||||
const indicatorSize = isMinimized ? 8 : 12;
|
||||
const indicatorX =
|
||||
nodeInfo.x + iconBaseWidth / 2 - indicatorSize * 0.3;
|
||||
const indicatorY =
|
||||
nodeInfo.y - iconBaseHeight / 2 - indicatorSize * 0.3;
|
||||
|
||||
if (dlStatus === "DownloadCompleted") {
|
||||
// Green circle with white checkmark
|
||||
const dlG = nodeG.append("g").attr("class", "download-indicator");
|
||||
dlG.append("title").text("Downloaded on this node");
|
||||
dlG
|
||||
.append("circle")
|
||||
.attr("cx", indicatorX)
|
||||
.attr("cy", indicatorY)
|
||||
.attr("r", indicatorSize)
|
||||
.attr("fill", "#22c55e")
|
||||
.attr("stroke", "#15803d")
|
||||
.attr("stroke-width", 1);
|
||||
// Checkmark path
|
||||
const checkScale = indicatorSize / 12;
|
||||
dlG
|
||||
.append("path")
|
||||
.attr(
|
||||
"d",
|
||||
`M${indicatorX - 4 * checkScale},${indicatorY} L${indicatorX - 1 * checkScale},${indicatorY + 3.5 * checkScale} L${indicatorX + 5 * checkScale},${indicatorY - 3.5 * checkScale}`,
|
||||
)
|
||||
.attr("stroke", "white")
|
||||
.attr("stroke-width", 2 * checkScale)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke-linecap", "round")
|
||||
.attr("stroke-linejoin", "round");
|
||||
} else if (onDownloadToNode) {
|
||||
// Download arrow icon (not completed — pending/ongoing/failed)
|
||||
const dlG = nodeG
|
||||
.append("g")
|
||||
.attr("class", "download-indicator")
|
||||
.style("cursor", "pointer");
|
||||
dlG.append("title").text("Download to this node");
|
||||
dlG
|
||||
.append("circle")
|
||||
.attr("cx", indicatorX)
|
||||
.attr("cy", indicatorY)
|
||||
.attr("r", indicatorSize)
|
||||
.attr("fill", "rgba(80, 80, 90, 0.9)")
|
||||
.attr("stroke", "rgba(255,215,0,0.5)")
|
||||
.attr("stroke-width", 1);
|
||||
// Arrow-down path
|
||||
const arrowScale = indicatorSize / 12;
|
||||
dlG
|
||||
.append("path")
|
||||
.attr(
|
||||
"d",
|
||||
`M${indicatorX},${indicatorY - 4 * arrowScale} L${indicatorX},${indicatorY + 1.5 * arrowScale} M${indicatorX - 3 * arrowScale},${indicatorY - 1 * arrowScale} L${indicatorX},${indicatorY + 1.5 * arrowScale} L${indicatorX + 3 * arrowScale},${indicatorY - 1 * arrowScale} M${indicatorX - 4 * arrowScale},${indicatorY + 4 * arrowScale} L${indicatorX + 4 * arrowScale},${indicatorY + 4 * arrowScale}`,
|
||||
)
|
||||
.attr("stroke", "rgba(255,215,0,0.8)")
|
||||
.attr("stroke-width", 1.5 * arrowScale)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke-linecap", "round")
|
||||
.attr("stroke-linejoin", "round");
|
||||
dlG.on("click", (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onDownloadToNode(nodeInfo.id);
|
||||
});
|
||||
dlG
|
||||
.on("mouseenter", function () {
|
||||
d3.select(this)
|
||||
.select("circle")
|
||||
.attr("stroke", "rgba(255,215,0,1)")
|
||||
.attr("fill", "rgba(100, 100, 110, 0.9)");
|
||||
})
|
||||
.on("mouseleave", function () {
|
||||
d3.select(this)
|
||||
.select("circle")
|
||||
.attr("stroke", "rgba(255,215,0,0.5)")
|
||||
.attr("fill", "rgba(80, 80, 90, 0.9)");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Vertical GPU Bar (right side of icon) ---
|
||||
// Show in both full mode and minimized mode (scaled appropriately)
|
||||
if (showFullLabels || isMinimized) {
|
||||
@@ -1153,6 +1249,8 @@
|
||||
const _hoveredNodeId = hoveredNodeId;
|
||||
const _filteredNodes = filteredNodes;
|
||||
const _highlightedNodes = highlightedNodes;
|
||||
const _downloadsData = downloadsData;
|
||||
const _activeModelId = activeModelId;
|
||||
if (_data) {
|
||||
renderGraph();
|
||||
}
|
||||
|
||||
152
dashboard/src/lib/utils/downloads.ts
Normal file
152
dashboard/src/lib/utils/downloads.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Shared utilities for parsing and querying download state.
|
||||
*
|
||||
* The download state from `/state` is shaped as:
|
||||
* Record<NodeId, Array<TaggedDownloadEntry>>
|
||||
*
|
||||
* Each entry is a tagged union object like:
|
||||
* { "DownloadCompleted": { shard_metadata: { "PipelineShardMetadata": { model_card: { model_id: "..." }, ... } }, ... } }
|
||||
*/
|
||||
|
||||
/** Unwrap one level of tagged-union envelope, returning [tag, payload]. */
|
||||
function unwrapTagged(
|
||||
obj: Record<string, unknown>,
|
||||
): [string, Record<string, unknown>] | null {
|
||||
const keys = Object.keys(obj);
|
||||
if (keys.length !== 1) return null;
|
||||
const tag = keys[0];
|
||||
const payload = obj[tag];
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
return [tag, payload as Record<string, unknown>];
|
||||
}
|
||||
|
||||
/** Extract the model ID string from a download entry's nested shard_metadata. */
|
||||
export function extractModelIdFromDownload(
|
||||
downloadPayload: Record<string, unknown>,
|
||||
): string | null {
|
||||
const shardMetadata =
|
||||
downloadPayload.shard_metadata ?? downloadPayload.shardMetadata;
|
||||
if (!shardMetadata || typeof shardMetadata !== "object") return null;
|
||||
|
||||
const unwrapped = unwrapTagged(shardMetadata as Record<string, unknown>);
|
||||
if (!unwrapped) return null;
|
||||
const [, shardData] = unwrapped;
|
||||
|
||||
const modelMeta = shardData.model_card ?? shardData.modelCard;
|
||||
if (!modelMeta || typeof modelMeta !== "object") return null;
|
||||
|
||||
const meta = modelMeta as Record<string, unknown>;
|
||||
return (meta.model_id as string) ?? (meta.modelId as string) ?? null;
|
||||
}
|
||||
|
||||
/** Extract the shard_metadata object from a download entry payload. */
|
||||
export function extractShardMetadata(
|
||||
downloadPayload: Record<string, unknown>,
|
||||
): Record<string, unknown> | null {
|
||||
const shardMetadata =
|
||||
downloadPayload.shard_metadata ?? downloadPayload.shardMetadata;
|
||||
if (!shardMetadata || typeof shardMetadata !== "object") return null;
|
||||
return shardMetadata as Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Get the download tag (DownloadCompleted, DownloadOngoing, etc.) from a wrapped entry. */
|
||||
export function getDownloadTag(
|
||||
entry: unknown,
|
||||
): [string, Record<string, unknown>] | null {
|
||||
if (!entry || typeof entry !== "object") return null;
|
||||
return unwrapTagged(entry as Record<string, unknown>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over all download entries for a given node, yielding [tag, payload, modelId].
|
||||
*/
|
||||
function* iterNodeDownloads(
|
||||
nodeDownloads: unknown[],
|
||||
): Generator<[string, Record<string, unknown>, string]> {
|
||||
for (const entry of nodeDownloads) {
|
||||
const tagged = getDownloadTag(entry);
|
||||
if (!tagged) continue;
|
||||
const [tag, payload] = tagged;
|
||||
const modelId = extractModelIdFromDownload(payload);
|
||||
if (!modelId) continue;
|
||||
yield [tag, payload, modelId];
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if a specific model is fully downloaded (DownloadCompleted) on a specific node. */
|
||||
export function isModelDownloadedOnNode(
|
||||
downloadsData: Record<string, unknown[]>,
|
||||
nodeId: string,
|
||||
modelId: string,
|
||||
): boolean {
|
||||
const nodeDownloads = downloadsData[nodeId];
|
||||
if (!Array.isArray(nodeDownloads)) return false;
|
||||
|
||||
for (const [tag, , entryModelId] of iterNodeDownloads(nodeDownloads)) {
|
||||
if (tag === "DownloadCompleted" && entryModelId === modelId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Get all node IDs where a model is fully downloaded (DownloadCompleted). */
|
||||
export function getNodesWithModelDownloaded(
|
||||
downloadsData: Record<string, unknown[]>,
|
||||
modelId: string,
|
||||
): string[] {
|
||||
const result: string[] = [];
|
||||
for (const nodeId of Object.keys(downloadsData)) {
|
||||
if (isModelDownloadedOnNode(downloadsData, nodeId, modelId)) {
|
||||
result.push(nodeId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find shard metadata for a model from any download entry across all nodes.
|
||||
* Returns the first match found (completed entries are preferred).
|
||||
*/
|
||||
export function getShardMetadataForModel(
|
||||
downloadsData: Record<string, unknown[]>,
|
||||
modelId: string,
|
||||
): Record<string, unknown> | null {
|
||||
let fallback: Record<string, unknown> | null = null;
|
||||
|
||||
for (const nodeDownloads of Object.values(downloadsData)) {
|
||||
if (!Array.isArray(nodeDownloads)) continue;
|
||||
|
||||
for (const [tag, payload, entryModelId] of iterNodeDownloads(
|
||||
nodeDownloads,
|
||||
)) {
|
||||
if (entryModelId !== modelId) continue;
|
||||
const shard = extractShardMetadata(payload);
|
||||
if (!shard) continue;
|
||||
|
||||
if (tag === "DownloadCompleted") return shard;
|
||||
if (!fallback) fallback = shard;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the download status tag for a specific model on a specific node.
|
||||
* Returns the "best" status: DownloadCompleted > DownloadOngoing > others.
|
||||
*/
|
||||
export function getModelDownloadStatus(
|
||||
downloadsData: Record<string, unknown[]>,
|
||||
nodeId: string,
|
||||
modelId: string,
|
||||
): string | null {
|
||||
const nodeDownloads = downloadsData[nodeId];
|
||||
if (!Array.isArray(nodeDownloads)) return null;
|
||||
|
||||
let best: string | null = null;
|
||||
for (const [tag, , entryModelId] of iterNodeDownloads(nodeDownloads)) {
|
||||
if (entryModelId !== modelId) continue;
|
||||
if (tag === "DownloadCompleted") return tag;
|
||||
if (tag === "DownloadOngoing") best = tag;
|
||||
else if (!best) best = tag;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
@@ -39,9 +39,11 @@
|
||||
toggleChatSidebarVisible,
|
||||
thunderboltBridgeCycles,
|
||||
nodeThunderboltBridge,
|
||||
startDownload,
|
||||
type DownloadProgress,
|
||||
type PlacementPreview,
|
||||
} from "$lib/stores/app.svelte";
|
||||
import { getShardMetadataForModel } from "$lib/utils/downloads";
|
||||
import HeaderNav from "$lib/components/HeaderNav.svelte";
|
||||
import { fade, fly } from "svelte/transition";
|
||||
import { cubicInOut } from "svelte/easing";
|
||||
@@ -584,6 +586,17 @@
|
||||
isModelPickerOpen = false;
|
||||
}
|
||||
|
||||
async function handleTopologyDownload(nodeId: string) {
|
||||
if (!selectedModelId) return;
|
||||
const shardMeta = getShardMetadataForModel(
|
||||
downloadsData ?? {},
|
||||
selectedModelId,
|
||||
);
|
||||
if (shardMeta) {
|
||||
await startDownload(nodeId, shardMeta);
|
||||
}
|
||||
}
|
||||
|
||||
async function launchInstance(
|
||||
modelId: string,
|
||||
specificPreview?: PlacementPreview | null,
|
||||
@@ -1722,6 +1735,9 @@
|
||||
highlightedNodes={highlightedNodes()}
|
||||
filteredNodes={nodeFilter}
|
||||
onNodeClick={togglePreviewNodeFilter}
|
||||
{downloadsData}
|
||||
activeModelId={selectedModelId}
|
||||
onDownloadToNode={handleTopologyDownload}
|
||||
/>
|
||||
|
||||
<!-- Thunderbolt Bridge Cycle Warning -->
|
||||
@@ -2771,6 +2787,9 @@
|
||||
highlightedNodes={highlightedNodes()}
|
||||
filteredNodes={nodeFilter}
|
||||
onNodeClick={togglePreviewNodeFilter}
|
||||
{downloadsData}
|
||||
activeModelId={selectedModelId}
|
||||
onDownloadToNode={handleTopologyDownload}
|
||||
/>
|
||||
|
||||
<!-- Thunderbolt Bridge Cycle Warning (compact) -->
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
deleteDownload,
|
||||
} from "$lib/stores/app.svelte";
|
||||
import HeaderNav from "$lib/components/HeaderNav.svelte";
|
||||
import {
|
||||
extractModelIdFromDownload,
|
||||
extractShardMetadata,
|
||||
getDownloadTag,
|
||||
} from "$lib/utils/downloads";
|
||||
|
||||
type FileProgress = {
|
||||
name: string;
|
||||
@@ -98,26 +103,7 @@
|
||||
return Math.min(100, Math.max(0, value as number));
|
||||
}
|
||||
|
||||
function extractModelIdFromDownload(
|
||||
downloadPayload: Record<string, unknown>,
|
||||
): string | null {
|
||||
const shardMetadata =
|
||||
downloadPayload.shard_metadata ?? downloadPayload.shardMetadata;
|
||||
if (!shardMetadata || typeof shardMetadata !== "object") return null;
|
||||
|
||||
const shardObj = shardMetadata as Record<string, unknown>;
|
||||
const shardKeys = Object.keys(shardObj);
|
||||
if (shardKeys.length !== 1) return null;
|
||||
|
||||
const shardData = shardObj[shardKeys[0]] as Record<string, unknown>;
|
||||
if (!shardData) return null;
|
||||
|
||||
const modelMeta = shardData.model_card ?? shardData.modelCard;
|
||||
if (!modelMeta || typeof modelMeta !== "object") return null;
|
||||
|
||||
const meta = modelMeta as Record<string, unknown>;
|
||||
return (meta.model_id as string) ?? (meta.modelId as string) ?? null;
|
||||
}
|
||||
// extractModelIdFromDownload imported from $lib/utils/downloads
|
||||
|
||||
function parseDownloadProgress(
|
||||
payload: Record<string, unknown>,
|
||||
@@ -197,14 +183,10 @@
|
||||
for (const downloadWrapped of nodeEntries) {
|
||||
if (!downloadWrapped || typeof downloadWrapped !== "object") continue;
|
||||
|
||||
const keys = Object.keys(downloadWrapped as Record<string, unknown>);
|
||||
if (keys.length !== 1) continue;
|
||||
const tagged = getDownloadTag(downloadWrapped);
|
||||
if (!tagged) continue;
|
||||
|
||||
const downloadKind = keys[0];
|
||||
const downloadPayload = (downloadWrapped as Record<string, unknown>)[
|
||||
downloadKind
|
||||
] as Record<string, unknown>;
|
||||
if (!downloadPayload) continue;
|
||||
const [downloadKind, downloadPayload] = tagged;
|
||||
|
||||
const modelId =
|
||||
extractModelIdFromDownload(downloadPayload) ?? "unknown-model";
|
||||
@@ -273,10 +255,7 @@
|
||||
}
|
||||
|
||||
// Extract shard_metadata for use with download actions
|
||||
const shardMetadata = (downloadPayload.shard_metadata ??
|
||||
downloadPayload.shardMetadata) as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const shardMetadata = extractShardMetadata(downloadPayload);
|
||||
|
||||
const entry: ModelEntry = {
|
||||
modelId,
|
||||
|
||||
Reference in New Issue
Block a user