Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion dashboard/src/lib/components/FamilyLogos.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@
let { family, class: className = "" }: FamilyLogoProps = $props();
</script>

{#if family === "favorites"}
{#if family === "recommended"}
<svg class="w-6 h-6 {className}" viewBox="0 0 24 24" fill="currentColor">
<path
d="M11 3l1.8 4.7L17.5 9.5l-4.7 1.8L11 16l-1.8-4.7L4.5 9.5l4.7-1.8L11 3z"
/>
<path d="M18 13l.9 2.4 2.4.9-2.4.9-.9 2.4-.9-2.4-2.4-.9 2.4-.9.9-2.4z" />
</svg>
{:else if family === "favorites"}
<svg class="w-6 h-6 {className}" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
Expand Down
28 changes: 28 additions & 0 deletions dashboard/src/lib/components/FamilySidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
type FamilySidebarProps = {
families: string[];
selectedFamily: string | null;
hasRecommended: boolean;
hasFavorites: boolean;
hasRecents: boolean;
onSelect: (family: string | null) => void;
Expand All @@ -12,13 +13,15 @@
let {
families,
selectedFamily,
hasRecommended,
hasFavorites,
hasRecents,
onSelect,
}: FamilySidebarProps = $props();

// Family display names
const familyNames: Record<string, string> = {
recommended: "Recommended",
favorites: "Favorites",
recents: "Recent",
huggingface: "Hub",
Expand All @@ -45,6 +48,31 @@
<div
class="flex flex-col gap-1 py-2 px-1 border-r border-exo-yellow/10 bg-exo-medium-gray/30 min-w-[80px] sm:min-w-[72px] overflow-y-auto scrollbar-hide"
>
<!-- Recommended (curated set) -->
{#if hasRecommended}
<button
type="button"
onclick={() => onSelect("recommended")}
class="group flex flex-col items-center justify-center p-2 rounded transition-all duration-200 cursor-pointer {selectedFamily ===
'recommended'
? 'bg-exo-yellow/20 border-l-2 border-exo-yellow'
: 'hover:bg-white/5 border-l-2 border-transparent'}"
title="Recommended models"
>
<FamilyLogos
family="recommended"
class={selectedFamily === "recommended"
? "text-exo-yellow"
: "text-white/50 group-hover:text-exo-yellow/70"}
/>
<span
class="text-[11px] font-mono mt-0.5 {selectedFamily === 'recommended'
? 'text-exo-yellow'
: 'text-white/40 group-hover:text-white/60'}">Rec'd</span
>
</button>
{/if}

<!-- All models (no filter) -->
<button
type="button"
Expand Down
45 changes: 33 additions & 12 deletions dashboard/src/lib/components/ModelPickerModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
is_custom?: boolean;
tasks?: string[];
hugging_face_id?: string;
recommended?: boolean;
}

interface ModelGroup {
Expand Down Expand Up @@ -110,7 +111,7 @@

// Local state
let searchQuery = $state("");
let selectedFamily = $state<string | null>(null);
let selectedFamily = $state<string | null>("recommended");
let expandedGroups = $state<Set<string>>(new Set());
let showFilters = $state(false);
let filters = $state<FilterState>({
Expand Down Expand Up @@ -210,6 +211,9 @@
manualModelId = "";
addModelError = null;
justAddedModelId = null;
if (selectedFamily === "recommended" && !hasRecommended) {
selectedFamily = null;
}
if (justAddedTimer) {
clearTimeout(justAddedTimer);
justAddedTimer = null;
Expand Down Expand Up @@ -242,6 +246,7 @@
selectedFamily === "huggingface" ||
selectedFamily === "recents" ||
selectedFamily === "favorites" ||
selectedFamily === "recommended" ||
query.length < 2 ||
!noLocalResults
) {
Expand Down Expand Up @@ -477,7 +482,20 @@
let result: ModelGroup[] = [...groupedModels];

// Filter by family
if (selectedFamily === "favorites") {
if (selectedFamily === "recommended") {
result = result
.map((g) => {
const variants = g.variants.filter((v) => v.recommended);
if (variants.length === 0) return null;
return {
...g,
variants,
smallestVariant: variants[0],
hasMultipleVariants: variants.length > 1,
};
})
.filter((g): g is ModelGroup => g !== null);
} else if (selectedFamily === "favorites") {
result = result.filter((g) => favorites.has(g.id));
} else if (
selectedFamily &&
Expand Down Expand Up @@ -567,6 +585,10 @@
// Check if any favorites exist
const hasFavorites = $derived(favorites.size > 0);

const hasRecommended = $derived(
groupedModels.some((g) => g.variants.some((v) => v.recommended)),
);

// Timestamp lookup for recent models
const recentTimestamps = $derived(
new Map(getRecentEntries().map((e) => [e.modelId, e.launchedAt])),
Expand Down Expand Up @@ -609,8 +631,9 @@
);
});

// Split filtered groups into recommended (fits_now) and others for visual separation
const recommendedGroups = $derived(
// Split filtered groups into ones that fit now and others, for visual
// separation (distinct from the curated "Recommended" tab).
const fitsGroups = $derived(
filteredGroups.filter((g) =>
g.variants.some((v) => getModelFitStatus(v.id) === "fits_now"),
),
Expand Down Expand Up @@ -783,6 +806,7 @@
<FamilySidebar
families={uniqueFamilies}
{selectedFamily}
{hasRecommended}
{hasFavorites}
hasRecents={hasRecentsTab}
onSelect={(family) => (selectedFamily = family)}
Expand Down Expand Up @@ -958,8 +982,8 @@
{/if}
</div>
{:else}
<!-- Recommended for your cluster -->
{#if recommendedGroups.length > 0 && otherGroups.length > 0 && !searchQuery.trim()}
<!-- Fits in available memory -->
{#if fitsGroups.length > 0 && otherGroups.length > 0 && !searchQuery.trim()}
<div
class="sticky top-0 z-10 flex items-center gap-2 px-3 py-2 bg-green-950/60 border-b border-green-500/20 backdrop-blur-sm"
>
Expand All @@ -978,14 +1002,11 @@
</svg>
<span
class="text-xs font-mono text-green-400 tracking-wider uppercase"
>Recommended for your cluster</span
>
<span class="text-xs font-mono text-green-400/50"
>— fits in available memory</span
>Fits in available memory</span
>
</div>
{/if}
{#each recommendedGroups as group}
{#each fitsGroups as group}
<ModelPickerGroup
{group}
isExpanded={expandedGroups.has(group.id)}
Expand All @@ -1004,7 +1025,7 @@
/>
{/each}
<!-- Other models -->
{#if otherGroups.length > 0 && recommendedGroups.length > 0 && !searchQuery.trim()}
{#if otherGroups.length > 0 && fitsGroups.length > 0 && !searchQuery.trim()}
<div
class="sticky top-0 z-10 flex items-center gap-2 px-3 py-2 bg-exo-dark-gray/80 border-y border-exo-medium-gray/20 backdrop-blur-sm"
>
Expand Down
10 changes: 3 additions & 7 deletions dashboard/src/lib/components/PrefillDecodeDisaggregation.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { onMount } from "svelte";
import FamilyLogos from "$lib/components/FamilyLogos.svelte";
import {
instances,
Expand All @@ -19,14 +19,10 @@
VllmInstance?: Instance;
};

let interval: ReturnType<typeof setInterval> | null = null;

onMount(() => {
// One immediate refresh on mount; live updates ride the global /state poll
// in app.svelte.ts (no separate interval here — it doubled /state traffic).
refreshState();
interval = setInterval(refreshState, 3000);
});
onDestroy(() => {
if (interval) clearInterval(interval);
});

type InstanceRow = {
Expand Down
55 changes: 52 additions & 3 deletions dashboard/src/lib/stores/app.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,9 +606,12 @@ class AppStore {
/** Number of consecutive fetch failures. */
private consecutiveFailures = 0;
private static readonly CONNECTION_LOST_THRESHOLD = 3;
private static readonly STATE_POLL_INTERVAL_MS = 2000;

private fetchInterval: ReturnType<typeof setInterval> | null = null;
private previewsInterval: ReturnType<typeof setInterval> | null = null;
private stateEtag: string | null = null;
private visibilityListener: (() => void) | null = null;
private lastConversationPersistTs = 0;
private previousNodeIds: Set<string> = new Set();

Expand Down Expand Up @@ -1284,14 +1287,45 @@ class AppStore {
startPolling() {
this.fetchState();
this.fetchFeatureFlags();
this.fetchInterval = setInterval(() => this.fetchState(), 1000);
this.startStateInterval();

// Pause polling while the tab is hidden — a backgrounded dashboard has no
// reason to keep pulling /state. Resume with an immediate fetch when the
// tab becomes visible again.
if (typeof document !== "undefined" && !this.visibilityListener) {
this.visibilityListener = () => {
if (document.hidden) {
this.stopStateInterval();
} else if (!this.fetchInterval) {
this.fetchState();
this.startStateInterval();
}
};
document.addEventListener("visibilitychange", this.visibilityListener);
}
}

stopPolling() {
private startStateInterval() {
if (this.fetchInterval) return;
this.fetchInterval = setInterval(
() => this.fetchState(),
AppStore.STATE_POLL_INTERVAL_MS,
);
}

private stopStateInterval() {
if (this.fetchInterval) {
clearInterval(this.fetchInterval);
this.fetchInterval = null;
}
}

stopPolling() {
this.stopStateInterval();
if (this.visibilityListener && typeof document !== "undefined") {
document.removeEventListener("visibilitychange", this.visibilityListener);
this.visibilityListener = null;
}
this.stopPreviewsPolling();
}

Expand All @@ -1307,10 +1341,25 @@ class AppStore {

async fetchState() {
try {
const response = await fetch("/state");
const headers: Record<string, string> = {};
if (this.stateEtag) {
headers["If-None-Match"] = this.stateEtag;
}
const response = await fetch("/state", { headers });

// 304: state unchanged since our last poll — no body shipped. Keep the
// data we already have and treat it as a successful poll.
if (response.status === 304) {
this.lastUpdate = Date.now();
if (!this.isConnected) this.isConnected = true;
this.consecutiveFailures = 0;
return;
}

if (!response.ok) {
throw new Error(`Failed to fetch state: ${response.status}`);
}
this.stateEtag = response.headers.get("ETag");
const data: RawStateResponse = await response.json();

if (data.topology) {
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@
quantization?: string;
base_model?: string;
capabilities?: string[];
recommended?: boolean;
}>
>([]);
type ModelMemoryFitStatus =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ reasoning_dialect = "tool_conditional"

context_length = 1048576
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 155095760030

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ reasoning_dialect = "tool_conditional"

context_length = 1048576
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 849681803879

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ capabilities = ["text", "thinking"]
reasoning_dialect = "post_last_user"
context_length = 202752
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 465173655552

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ capabilities = ["text", "thinking"]
reasoning_dialect = "post_last_user"
context_length = 202752
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 405480321024

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ capabilities = ["text", "thinking"]
reasoning_dialect = "post_last_user"
context_length = 202752
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 1487822475264

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ capabilities = ["text", "thinking", "thinking_toggle", "vision"]
reasoning_dialect = "suffix"
context_length = 262144
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 470628683776

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ capabilities = ["text"]

context_length = 131072
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 729808896

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ capabilities = ["text", "thinking"]
reasoning_dialect = "post_last_user"
context_length = 196608
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 121537496794

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ capabilities = ["text", "thinking"]
reasoning_dialect = "post_last_user"
context_length = 196608
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 128682598717

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ capabilities = ["text", "thinking"]
reasoning_dialect = "post_last_user"
context_length = 196608
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
recommended = true
[storage_size]
in_bytes = 243002680786

Expand Down
Loading
Loading