hfviewer / visualizer /hfDeployButton.js
hfviewer automation
Sync private Space from hfviewer-6a4a0ac28978
c701edd
Raw History Blame Contribute Delete
18.8 kB
// Hosted and Space shell launch controls. Renderer/card and Models embeds stay excluded.
export const EMBEDL_MODELS_ORIGIN = Object.freeze({ preview: 'https://embedl.com', live: 'https://embedl.com' });
export const INDEX_URL = '/api/embedl_models_index';
export const DEPLOY_CHIP_ICON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 2v4m6-4v4M9 18v4m6-4v4M2 9h4m-4 6h4m12-6h4m-4 6h4"/></svg>';
const configRequests = new WeakMap();
const initialPreviews = new WeakMap();
const PREVIEW_KEY = 'hfviewer:embedl-deploy';
const CACHE_KEY = 'hfviewer:embedl-deploy-index:v1';
const TOOLTIP = 'Optimized builds of this model, ready to run on real devices: Embedl Models';
// Names, masks and legacy id from embedl/model-web main, hardware.ts / HardwareLabel.svelte.
// Source revision and asset sizes: docs/hfviewer_deploy_button.md.
export const HARDWARE = Object.freeze({
thor: { name: 'AGX Thor', icon: '/assets/hardware/thor.png' },
'agx-orin': { name: 'AGX Orin', icon: '/assets/hardware/agx-orin.png' },
'orin-nx-16gb': { name: 'Orin NX 16GB', icon: '/assets/hardware/orin-nx-16gb.png' },
'orin-nano-super': { name: 'Orin Nano Super', icon: '/assets/hardware/orin-nano-super.png' },
l4: { name: 'L4', icon: '/assets/hardware/l4.png' },
h200: { name: 'H200', icon: '/assets/hardware/h200.png' },
'ryzen-ai-max-395': { name: 'Ryzen AI Max+ PRO 395', icon: '/assets/hardware/ryzen-ai-max-395.png' },
'rtx-2080-ti': { name: 'RTX 2080 Ti', icon: '/assets/hardware/rtx-2080-ti.png' },
});
export function hardwareDetails(id) {
const key = id === 'strix-halo' ? 'ryzen-ai-max-395' : id;
return Object.hasOwn(HARDWARE, key) ? HARDWARE[key] : { name: id };
}
export function readPreview(search, storage) {
const value = new URLSearchParams(search).get('embedl-deploy');
try {
if (value === '0') storage?.removeItem(PREVIEW_KEY);
if (value === '1') storage?.setItem(PREVIEW_KEY, '1');
return value === '1' || (value !== '0' && storage?.getItem(PREVIEW_KEY) === '1');
} catch { return value === '1'; }
}
// Launched 2026-10-21: live unless the server explicitly kills it (false). A
// missing or failed config read must not hide the launch.
export function deployMode(enabled, preview) {
return enabled !== false ? 'live' : preview ? 'preview' : null;
}
export function isSpaceContext(win) {
const params = new URLSearchParams(win.location.search);
return /(^|\.)hf\.space$/i.test(win.location.hostname) ||
win.__HF_SPACE_EMBEDDED__ === true ||
win.HFVIEWER_PRODUCT_CONTEXT?.product_surface === 'hf_space' ||
['1', 'true', 'yes', 'on'].includes((params.get('hfSpace') || '').toLowerCase());
}
export function allowedContext(win) {
const params = new URLSearchParams(win.location.search);
const space = isSpaceContext(win);
// A Space app may itself be framed by the Hub or its compact Space shell.
// Generic renderer/card embeds and Models surfaces remain excluded.
return (win.top === win || space) && !params.has('embed') &&
(!params.has('space_interaction') || space) &&
params.get('skin')?.toLowerCase() !== 'models' &&
!/(^|\.)(embedl\.com|embedl\.dev)$/i.test(win.location.hostname) &&
(!/(^|\.)huggingface\.co$/i.test(win.location.hostname) || space);
}
export function configUrlFor(win, fallback = '/api/ui_config') {
return isSpaceContext(win) ? 'https://hfviewer.com/api/ui_config' : fallback;
}
export function indexUrl(search, mode, win) {
const url = win && isSpaceContext(win) ? 'https://hfviewer.com/api/embedl_models_index' : INDEX_URL;
const override = mode === 'preview' && new URLSearchParams(search).get('embedl-deploy-index');
return override ? (win && isSpaceContext(win) ? new URL(override, 'https://hfviewer.com').href : override) : url;
}
export function deployLink(page, mode, utmContent) {
// The public index may supply paths, never origins or executable URLs.
if (typeof page !== 'string' || !/^\/(?!\/)/.test(page) || /[\\\s]/.test(page)) return null;
const origin = EMBEDL_MODELS_ORIGIN[mode];
if (!origin) return null;
const url = new URL(page, origin);
if (url.origin !== origin) return null;
// Land on the model's Run tab (model and hardware pages both accept it;
// embedl.com canonical URLs drop the query).
url.searchParams.set('tab', 'run');
url.searchParams.set('utm_source', 'hfviewer');
url.searchParams.set('utm_medium', 'deploy_button');
url.searchParams.set('utm_campaign', 'models_launch');
if (utmContent) url.searchParams.set('utm_content', utmContent);
return url.href;
}
export function lookupModel(index, repoId) {
if (index?.v !== 1 || !index.models || typeof repoId !== 'string') return null;
const entry = index.models[repoId.toLowerCase()];
if (!entry || !deployLink(entry.page, 'live') || !Array.isArray(entry.builds) || !entry.builds.length) return null;
if (!entry.builds.every(build => typeof build?.hardware === 'string' && build.hardware && deployLink(build.page, 'live'))) return null;
return entry;
}
export function speedup(headline) {
const text = headline?.text ?? headline?.label; // Spec spelling and published index spelling.
if (typeof text !== 'string' || text.trim().toLowerCase() !== 'faster') return null;
const match = /^\s*(\d+(?:\.\d+)?)\s*[×x]\s*$/i.exec(headline.value);
const value = match ? Number(match[1]) : 0;
return Number.isFinite(value) && value > 0 ? value : null;
}
export function headlineChip(headline) {
const value = speedup(headline);
return value ? `${Number(value.toFixed(1))}× faster` : '';
}
export function modelChip(builds) {
const best = builds.reduce((a, b) => (speedup(b.headline) || 0) > (speedup(a?.headline) || 0) ? b : a, null);
if (best) return headlineChip(best.headline);
// No speed-up: a device count only helps when there is a choice to make.
const count = new Set(builds.map(build => build.hardware)).size;
return count > 1 ? `${count} devices` : '';
}
export async function fetchJson(url, fetcher, timeoutMs = 2500) {
const controller = new AbortController();
let timer;
try {
return await Promise.race([
(async () => {
const response = await fetcher(url, { signal: controller.signal, credentials: 'omit' });
return response.ok ? await response.json() : null;
})(),
new Promise(resolve => { timer = setTimeout(() => { controller.abort(); resolve(null); }, timeoutMs); }),
]);
} catch { return null; }
finally { clearTimeout(timer); }
}
// Some shell routes normalize their URL before this module finishes importing.
// Preserve the initial flag, including when sessionStorage is unavailable.
export function initializeDeployPreview(win, search = win.location.search) {
let storage;
try { storage = win.sessionStorage; } catch { /* Storage is optional. */ }
initialPreviews.set(win, readPreview(search, storage));
}
function currentDeployPreview(win) {
let storage;
try { storage = win.sessionStorage; } catch { /* Storage is optional. */ }
const preview = readPreview(win.location.search, storage);
if (new URLSearchParams(win.location.search).has('embedl-deploy')) initialPreviews.set(win, preview);
return preview || initialPreviews.get(win) === true;
}
// Shared by the navbar and graph action: one config read per document/URL.
// The pill may request the switch before a graph; the index still waits for graph-ready.
export async function loadDeployMode({ window: win, configUrl }) {
currentDeployPreview(win); // Clear an explicit zero immediately.
if (!allowedContext(win)) return null;
configUrl = configUrlFor(win, configUrl);
let requests = configRequests.get(win);
if (!requests) { requests = new Map(); configRequests.set(win, requests); }
if (!requests.has(configUrl)) requests.set(configUrl, fetchJson(configUrl, win.fetch.bind(win)));
const config = await requests.get(configUrl);
return deployMode(config?.EMBEDL_DEPLOY_BUTTON, currentDeployPreview(win));
}
export function launchPillContent({ navbar_route, surface } = {}) {
return navbar_route || surface?.replace(/_header$/, '') || 'other';
}
export function launchPillLink(mode, utmContent = 'other') {
const origin = EMBEDL_MODELS_ORIGIN[mode];
if (!origin) return null;
const url = new URL('/', origin);
url.searchParams.set('utm_source', 'hfviewer');
url.searchParams.set('utm_medium', 'launch_pill');
url.searchParams.set('utm_campaign', 'models_launch');
url.searchParams.set('utm_content', utmContent);
return url.href;
}
export async function configureLaunchPill({ window: win, like, launch, configUrl, track, props = () => ({}) }) {
const space = isSpaceContext(win);
if (space) like.hidden = true; // Space never promotes liking itself, even while off.
// The launch is the default: show it before any network wait so the first
// paint never swaps pills. The config read can only kill it (explicit false).
let mode = allowedContext(win) ? 'live' : null;
function show(visible) {
launch.hidden = !visible;
if (!space) like.hidden = visible;
}
if (!mode) { show(false); return; }
launch.querySelector('.hfnx-launch-pill__icon').innerHTML = DEPLOY_CHIP_ICON;
function refreshLink() {
const context = props();
const utmContent = `${isSpaceContext(win) ? 'space_' : ''}${launchPillContent(context)}`;
launch.href = launchPillLink(mode, utmContent);
return { ...context, utm_content: utmContent };
}
refreshLink();
// The navbar survives SPA navigation: update before native open/copy actions.
for (const event of ['pointerenter', 'pointerdown', 'focus', 'contextmenu']) {
launch.addEventListener(event, refreshLink);
}
const clicked = event => {
if (event.type !== 'click' && event.button !== 1) return;
const context = refreshLink();
try { track('embedl_launch_pill_clicked', { ...context, destination: 'embedl_models',
href: launch.href, preview: mode === 'preview' }); } catch { /* Never block navigation. */ }
};
launch.addEventListener('click', clicked);
launch.addEventListener('auxclick', clicked);
// Both states occupy the same reserved grid cell; only visibility changes.
show(true);
const resolved = await loadDeployMode({ window: win, configUrl });
if (!resolved) { show(false); return; }
if (resolved !== mode) { mode = resolved; refreshLink(); }
}
export function createIndexLoader({ fetcher, storage }) {
const pending = new Map();
return (url, mode) => {
if (!mode) return Promise.resolve(null);
if (!pending.has(url)) {
pending.set(url, (async () => {
try {
const cache = JSON.parse(storage?.getItem(CACHE_KEY) || 'null');
if (cache?.url === url && cache.index?.v === 1) return cache.index;
} catch { /* Storage is optional, including private browsing. */ }
const index = await fetchJson(url, fetcher);
if (index?.v !== 1 || !index.models || typeof index.models !== 'object') return null;
try { storage?.setItem(CACHE_KEY, JSON.stringify({ url, index })); } catch { /* Quota / storage disabled. */ }
return index;
})());
}
return pending.get(url);
};
}
// Native links retain new-tab, copy-link and modifier-click behaviour.
export function createDeployAction({ document: doc, entry, repoId, mode, space = false, track = () => {} }) {
const root = doc.createElement('div');
root.className = 'embedl-deploy';
const multiple = entry.builds.length > 1;
const trigger = doc.createElement(multiple ? 'button' : 'a');
trigger.className = 'pill embedl-deploy__trigger';
trigger.title = TOOLTIP;
trigger.setAttribute('aria-label', 'Run on device');
const props = { model: repoId, hf_id: repoId, repo_id: repoId, build_count: entry.builds.length, preview: mode === 'preview' };
function span(className, text) {
const el = doc.createElement('span'); el.className = className; el.textContent = text; return el;
}
const icon = span('embedl-deploy__icon', '');
icon.setAttribute('aria-hidden', 'true');
icon.innerHTML = DEPLOY_CHIP_ICON;
const chip = modelChip(entry.builds);
trigger.append(icon, span('embedl-deploy__label', 'Run on device'));
if (chip) trigger.append(span('embedl-deploy__chip', chip));
root.append(trigger);
function link(el, page, hardware, placement) {
// Pass the raw value to URLSearchParams: encode the HF slash exactly once.
const utmContent = `${repoId.toLowerCase()}__${placement}${space ? '__space' : ''}`;
el.href = deployLink(page, mode, utmContent); el.target = '_blank'; el.rel = 'noopener';
const clicked = event => { if (event.type === 'click' || event.button === 1) track('embedl_deploy_clicked', { ...props, hardware, utm_content: utmContent }); };
el.addEventListener('click', clicked);
el.addEventListener('auxclick', clicked);
}
let cleanup = () => {};
if (!multiple) link(trigger, entry.builds[0].page, entry.builds[0].hardware, 'direct');
else {
trigger.type = 'button';
trigger.setAttribute('aria-haspopup', 'menu');
trigger.setAttribute('aria-expanded', 'false');
const menu = doc.createElement('div');
menu.className = 'embedl-deploy__menu'; menu.hidden = true;
menu.id = 'embedl-deploy-menu'; menu.setAttribute('role', 'menu');
menu.setAttribute('aria-label', 'Optimized builds on Embedl Models');
trigger.setAttribute('aria-controls', menu.id);
const items = entry.builds.map(build => {
const item = doc.createElement('a'); item.className = 'embedl-deploy__item';
const hardware = hardwareDetails(build.hardware);
const label = span('embedl-deploy__hardware', '');
if (hardware.icon) {
const icon = span('embedl-deploy__hardware-icon', '');
icon.setAttribute('aria-hidden', 'true');
icon.style.setProperty('--hardware-icon', `url("${hardware.icon}")`);
label.append(icon);
}
label.append(span('embedl-deploy__hardware-name', hardware.name));
item.append(label);
item.append(span('embedl-deploy__precision', build.precision || ''));
const chip = headlineChip(build.headline);
if (chip) item.append(span('embedl-deploy__chip', chip));
const arrow = span('embedl-deploy__arrow', '›'); arrow.setAttribute('aria-hidden', 'true'); item.append(arrow);
link(item, build.page, build.hardware, 'menu'); return item;
});
const all = doc.createElement('a'); all.className = 'embedl-deploy__all';
all.textContent = 'All builds on Embedl Models →'; link(all, entry.page, 'all', 'all'); items.push(all);
items.forEach(item => { item.setAttribute('role', 'menuitem'); item.tabIndex = -1; menu.append(item); });
root.append(menu);
function close(returnFocus = false) {
menu.hidden = true; trigger.setAttribute('aria-expanded', 'false');
if (returnFocus) trigger.focus();
}
function positionMenu() {
if (menu.hidden || !doc.documentElement) return;
menu.style.transform = '';
const rect = menu.getBoundingClientRect();
const width = doc.documentElement.clientWidth;
const offset = Math.max(8, Math.min(rect.left, width - rect.width - 8)) - rect.left;
menu.style.transform = `translateX(${offset}px)`;
}
function open(index = 0) {
menu.hidden = false; trigger.setAttribute('aria-expanded', 'true');
positionMenu(); items[index].focus();
}
trigger.addEventListener('click', () => menu.hidden ? open() : close(true));
trigger.addEventListener('keydown', event => {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault(); event.stopPropagation(); open(event.key === 'ArrowUp' ? items.length - 1 : 0);
}
});
root.addEventListener('keydown', event => {
if (menu.hidden) return;
const index = items.indexOf(doc.activeElement);
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close(true); }
else if (index >= 0 && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
event.preventDefault();
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 :
(index + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
items[next].focus();
}
else if (event.key === 'Tab') close(true); // Natural Tab/Shift-Tab leaves the trigger.
});
items.forEach(item => item.addEventListener('click', () => close(true)));
const outside = event => { if (!root.contains(event.target)) close(); };
const blur = event => { if (!root.contains(event.relatedTarget)) close(); };
doc.addEventListener('pointerdown', outside); root.addEventListener('focusout', blur);
doc.defaultView?.addEventListener('resize', positionMenu);
cleanup = () => {
doc.removeEventListener('pointerdown', outside);
doc.defaultView?.removeEventListener('resize', positionMenu);
};
}
return { root, shown: () => track('embedl_deploy_shown', props), destroy: () => { cleanup(); root.remove(); } };
}
export function createDeployController({ window: win, container, configUrl, track, onLayout = () => {} }) {
let storage;
try { storage = win.sessionStorage; } catch { /* Storage disabled. */ }
// Read flags immediately so ?embedl-deploy=0 clears even on a model with no graph.
if (!initialPreviews.has(win)) initializeDeployPreview(win);
const load = createIndexLoader({ fetcher: win.fetch.bind(win), storage });
let generation = 0, currentRepo = '', readyRepo = '', action;
const eligible = () => allowedContext(win);
function reset(repoId = '') {
const next = repoId.toLowerCase();
if (next === currentRepo) return;
generation++; currentRepo = next; readyRepo = '';
action?.destroy(); action = null; onLayout();
}
function graphReady(repoId) {
if (!eligible() || !repoId || repoId.toLowerCase() !== currentRepo || readyRepo === currentRepo) return;
readyRepo = currentRepo;
const expected = generation;
const work = async () => {
if (expected !== generation) return;
const mode = await loadDeployMode({ window: win, configUrl });
if (!mode || expected !== generation) return;
const index = await load(indexUrl(win.location.search, mode, win), mode);
if (expected !== generation) return;
const entry = lookupModel(index, repoId);
if (!entry) return;
action = createDeployAction({ document: win.document, entry, repoId, mode, space: isSpaceContext(win), track });
container.append(action.root); onLayout(); action.shown();
};
if (win.requestIdleCallback) win.requestIdleCallback(() => { void work(); }, { timeout: 1500 });
else win.setTimeout(() => { void work(); }, 0);
}
return { reset, graphReady };
}