File size: 18,837 Bytes
4ba001a
87db499
 
4ba001a
 
 
87db499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7dc92f6
 
87db499
7dc92f6
87db499
4ba001a
 
 
 
 
 
 
87db499
 
4ba001a
 
 
 
 
87db499
4ba001a
 
 
 
 
87db499
4ba001a
 
 
 
87db499
4ba001a
87db499
 
 
 
 
 
c701edd
 
 
87db499
 
 
4ba001a
87db499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5aeb2a9
87db499
5aeb2a9
87db499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4ba001a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7dc92f6
 
 
 
 
 
 
 
 
 
4ba001a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7dc92f6
 
 
 
4ba001a
 
87db499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4ba001a
87db499
 
 
 
 
 
df6c411
87db499
 
 
 
 
 
4ba001a
5aeb2a9
 
 
87db499
4ba001a
 
 
 
 
87db499
 
 
 
4ba001a
87db499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4ba001a
87db499
 
4ba001a
87db499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4ba001a
87db499
4ba001a
87db499
 
 
 
 
 
 
 
 
 
 
 
 
4ba001a
87db499
4ba001a
87db499
 
 
4ba001a
87db499
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
// 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 };
}