Spaces:
Running
Running
Download visualizer/hfDeployButton.js from embedl/hfviewer: direct link, hf CLI and curl.
- Browser
- Download file 18.8 kB
-
https://huggingface.co/spaces/embedl/hfviewer/resolve/main/visualizer/hfDeployButton.js
- Command line
-
hf download hf://spaces/embedl/hfviewer/visualizer/hfDeployButton.js
-
curl -L -o hfDeployButton.js https://huggingface.co/spaces/embedl/hfviewer/resolve/main/visualizer/hfDeployButton.js
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 }; | |
| } | |