// 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 = ''; 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 }; }