Spaces:
Running
Running
Download space-shell.js from embedl/hfviewer: direct link, hf CLI and curl.
- Browser
- Download file 74.1 kB
-
https://huggingface.co/spaces/embedl/hfviewer/resolve/main/space-shell.js
- Command line
-
hf download hf://spaces/embedl/hfviewer/space-shell.js
-
curl -L -o space-shell.js https://huggingface.co/spaces/embedl/hfviewer/resolve/main/space-shell.js
74.1 kB
| (function () { | |
| "use strict"; | |
| var CARD_PREFIX = "https://hfviewer.com/models/cards/"; | |
| var API_PREFIX = "https://hfviewer.com/api/graph-preview.svg"; | |
| var WEBSITE_ORIGIN = "https://hfviewer.com"; | |
| var HUGGING_FACE_ORIGIN = "https://huggingface.co"; | |
| var HOME_MESSAGE_TYPE = "hfviewer-space-home-requested"; | |
| var HEADER_STATE_MESSAGE_TYPE = "hfviewer-space-header-state"; | |
| var HEADER_ACTION_MESSAGE_TYPE = "hfviewer-space-header-action"; | |
| var HEADER_ACTION_RESULT_MESSAGE_TYPE = "hfviewer-space-header-action-result"; | |
| var COOKIE_CONSENT_MESSAGE_TYPE = "hfviewer-space-cookie-consent"; | |
| var SPACE_BOOKMARK_STATE_URL = WEBSITE_ORIGIN + "/api/hf_space_bookmarks/state"; | |
| var SPACE_BOOKMARK_TOGGLE_URL = WEBSITE_ORIGIN + "/api/hf_space_bookmarks/toggle"; | |
| var SPACE_WATCHLIST_STATE_URL = WEBSITE_ORIGIN + "/api/hf_space_watchlist/state"; | |
| var SPACE_WATCHLIST_TOGGLE_URL = WEBSITE_ORIGIN + "/api/hf_space_watchlist/toggle"; | |
| var COMPACT_EDIT_README_REVEAL_DELAY_MS = 420; | |
| var COOKIE_CONSENT_STORAGE_KEY = "hfviewer_space_cookie_consent_v1"; | |
| var VIEWER_RETRY_DELAY_MS = 12000; | |
| var VIEWER_FAILURE_DELAY_MS = 18000; | |
| var viewerRetryTimer = 0; | |
| var viewerRetryCount = 0; | |
| var viewerLoadSequence = 0; | |
| var viewerRetryMutationExpected = false; | |
| var viewerLifecycleState = "loading"; | |
| var viewerLifecyclePhase = ""; | |
| var viewerLoadStartedAt = 0; | |
| var VIEWER_LIFECYCLE_MESSAGE_TYPE = "hfviewer-space-viewer-lifecycle"; | |
| var viewerHandshakeTimer = 0; | |
| var compactHeaderState = null; | |
| var compactDiffMenu = null; | |
| var compactTooltip = null; | |
| var compactTooltipTarget = null; | |
| var compactEmbedFeedbackTimer = 0; | |
| var compactEditReadmeRevealTimer = 0; | |
| var compactEmbedCopySequence = 0; | |
| var compactEditReadmeRepoId = ""; | |
| var compactBookmarkRepoId = ""; | |
| var compactBookmarkPressed = null; | |
| var compactBookmarkSyncSequence = 0; | |
| var compactBookmarkSyncingRepoId = ""; | |
| var nativeSpaceWatchedOwners = new Set(); | |
| var nativeSpaceWatchStateSequence = 0; | |
| var nativeSpaceWatchStateLoaded = false; | |
| var nativeSpaceWatchOperations = new Map(); | |
| function normalizeNativeSpaceWatchOwner(value) { | |
| var owner = String(value || "").trim().replace(/^\/+|\/+$/g, ""); | |
| return /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/.test(owner) ? owner : ""; | |
| } | |
| function nativeSpaceWatchOwnerForControl(button) { | |
| var explicit = normalizeNativeSpaceWatchOwner(button?.dataset?.watchOrg); | |
| if (explicit) return explicit; | |
| if (button?.dataset?.action === "compare-watch") { | |
| return normalizeNativeSpaceWatchOwner(String(compactHeaderState?.repoId || "").split("/")[0]); | |
| } | |
| var cardOwner = normalizeNativeSpaceWatchOwner( | |
| button?.closest?.(".hf-watchlist-card")?.querySelector?.(".hf-watchlist-card__name")?.textContent | |
| ); | |
| if (cardOwner) return cardOwner; | |
| var panelTitle = button?.closest?.(".hfnx-notify")?.querySelector?.(".hfnx-notify__title")?.textContent || ""; | |
| var panelMatch = String(panelTitle).match(/^Get notified on\s+(.+?)\s+releases$/i); | |
| if (panelMatch) return normalizeNativeSpaceWatchOwner(panelMatch[1]); | |
| var label = String( | |
| button?.querySelector?.(".hfnx-watchcta__label,.pill__label,.sx-action-label")?.textContent || | |
| button?.textContent || "" | |
| ).trim(); | |
| var match = label.match(/^(?:Watch|Watching|Adding|Removing)\s+(.+?)(?:\s+releases)?(?:\s*[\u2713…])?$/i); | |
| return normalizeNativeSpaceWatchOwner(match?.[1]); | |
| } | |
| function setNativeSpaceWatchControl(button, owner, watched, pending) { | |
| if (!button || !owner) return; | |
| button.dataset.spaceWatchOrg = owner; | |
| button.classList.toggle("is-watching", watched && !pending); | |
| button.classList.toggle("is-busy", !!pending); | |
| button.disabled = !!pending; | |
| button.setAttribute("aria-pressed", watched ? "true" : "false"); | |
| var verb = pending ? (watched ? "Adding " : "Removing ") : (watched ? "Watching " : "Watch "); | |
| var suffix = pending ? "…" : watched ? " ✓" : ""; | |
| var label = verb + owner + suffix; | |
| var labelNode = button.querySelector(".hfnx-watchcta__label,.pill__label,.sx-action-label"); | |
| if (button.matches(".hf-watchlist-card__watch")) { | |
| button.textContent = pending | |
| ? (watched ? "Adding…" : "Removing…") | |
| : watched ? "Watching ✓" : "Watch releases"; | |
| } else if (button.matches("#owner-gallery-watch-btn")) { | |
| if (labelNode) labelNode.textContent = pending | |
| ? (watched ? "Adding…" : "Removing…") | |
| : watched ? "Watching " + owner + " ✓" : "Watch " + owner + " releases"; | |
| } else if (button.matches('[data-action="compare-watch"]')) { | |
| if (labelNode) labelNode.textContent = pending | |
| ? (watched ? "Adding…" : "Removing…") | |
| : watched ? "Watching " + owner + " ✓" : "Watch " + owner + " releases"; | |
| } else if (labelNode) { | |
| labelNode.textContent = label; | |
| } | |
| } | |
| function syncNativeSpaceWatchControls(root) { | |
| if (!window.hfviewerSpaceAuth?.isAuthenticated?.()) return; | |
| var controls = []; | |
| if (root?.matches?.(".hfnx-watchcta,.hf-watchlist-card__watch,#owner-gallery-watch-btn,[data-action='compare-watch']")) { | |
| controls.push(root); | |
| } | |
| root?.querySelectorAll?.(".hfnx-watchcta,.hf-watchlist-card__watch,#owner-gallery-watch-btn,[data-action='compare-watch']") | |
| .forEach(function (button) { controls.push(button); }); | |
| controls.forEach(function (button) { | |
| var owner = nativeSpaceWatchOwnerForControl(button); | |
| if (!owner) return; | |
| var pending = nativeSpaceWatchOperations.has(owner.toLowerCase()); | |
| setNativeSpaceWatchControl( | |
| button, | |
| owner, | |
| nativeSpaceWatchedOwners.has(owner.toLowerCase()), | |
| pending | |
| ); | |
| if (!nativeSpaceWatchStateLoaded && !pending) { | |
| button.disabled = true; | |
| button.classList.add("is-busy"); | |
| button.setAttribute("aria-busy", "true"); | |
| } else { | |
| button.removeAttribute("aria-busy"); | |
| } | |
| }); | |
| } | |
| async function loadNativeSpaceWatchlistState() { | |
| if (!window.hfviewerSpaceAuth?.isAuthenticated?.()) return; | |
| var sequence = ++nativeSpaceWatchStateSequence; | |
| nativeSpaceWatchStateLoaded = false; | |
| syncNativeSpaceWatchControls(document); | |
| try { | |
| var result = await window.hfviewerSpaceAuth.postJson(SPACE_WATCHLIST_STATE_URL, {}); | |
| if (sequence !== nativeSpaceWatchStateSequence) return; | |
| nativeSpaceWatchedOwners = new Set( | |
| (Array.isArray(result?.orgs) ? result.orgs : []).map(function (owner) { | |
| return String(owner || "").toLowerCase(); | |
| }) | |
| ); | |
| nativeSpaceWatchStateLoaded = true; | |
| syncNativeSpaceWatchControls(document); | |
| } catch (error) { | |
| console.warn("Failed to read the native Space release watchlist", error); | |
| document.querySelectorAll( | |
| ".hfnx-watchcta,.hf-watchlist-card__watch,#owner-gallery-watch-btn,[data-action='compare-watch']" | |
| ).forEach(function (button) { | |
| button.dataset.sxTooltip = "Could not load watchlist — reload to try again"; | |
| }); | |
| } | |
| } | |
| async function toggleNativeSpaceWatchControl(button, owner, watched) { | |
| var key = owner.toLowerCase(); | |
| if (!nativeSpaceWatchStateLoaded || nativeSpaceWatchOperations.has(key)) return; | |
| // Invalidate any older state read before issuing the mutation. Otherwise | |
| // a delayed state response can overwrite the newer toggle response. | |
| nativeSpaceWatchStateSequence += 1; | |
| nativeSpaceWatchOperations.set(key, true); | |
| setNativeSpaceWatchControl(button, owner, watched, true); | |
| try { | |
| var result = await window.hfviewerSpaceAuth.postJson(SPACE_WATCHLIST_TOGGLE_URL, { | |
| org: owner, | |
| watched: watched, | |
| placement: button.matches(".hfnx-watchcta") ? "landing_search" : | |
| button.matches("#owner-gallery-watch-btn") ? "owner_page" : | |
| button.matches('[data-action="compare-watch"]') ? "compare_header" : "watchlist_page", | |
| }); | |
| nativeSpaceWatchedOwners = new Set( | |
| (Array.isArray(result?.orgs) ? result.orgs : []).map(function (entry) { | |
| return String(entry || "").toLowerCase(); | |
| }) | |
| ); | |
| window.hfviewerAnalyticsBridge?.capture?.( | |
| watched ? "hfnx_watchlist_subscribed" : "hfnx_watchlist_unwatched", | |
| { | |
| product_surface: "hf_space", | |
| source_page: "hf_space", | |
| native_space_auth: true, | |
| org: owner, | |
| placement: button.matches(".hfnx-watchcta") ? "landing_search" : "space_native_watch", | |
| } | |
| ); | |
| } catch (error) { | |
| console.warn("Failed to update the native Space release watchlist", error); | |
| button.dataset.sxTooltip = "Could not update watchlist — try again"; | |
| } finally { | |
| nativeSpaceWatchOperations.delete(key); | |
| syncNativeSpaceWatchControls(document); | |
| } | |
| } | |
| function handleNativeSpaceWatchClick(event) { | |
| var button = event.target.closest?.( | |
| ".hfnx-watchcta,.hf-watchlist-card__watch,#owner-gallery-watch-btn,[data-action='compare-watch']" | |
| ); | |
| if (!button) return; | |
| var owner = nativeSpaceWatchOwnerForControl(button); | |
| if (!owner) return; | |
| if (!window.hfviewerSpaceAuth?.isAuthenticated?.()) { | |
| event.preventDefault(); | |
| event.stopImmediatePropagation(); | |
| window.hfviewerSpaceAuth?.requestLogin?.(button, { | |
| title: "Sign in to watch " + owner, | |
| description: "Continue with Hugging Face to get notified about new " + owner + " releases.", | |
| intent: "watch_" + owner.toLowerCase(), | |
| }); | |
| return; | |
| } | |
| if (!nativeSpaceWatchStateLoaded) { | |
| event.preventDefault(); | |
| event.stopImmediatePropagation(); | |
| return; | |
| } | |
| var watched = nativeSpaceWatchedOwners.has(owner.toLowerCase()); | |
| // The landing search CTA's checked state is an informational confirmation, | |
| // not an unwatch action. Preserve that behavior while intercepting its | |
| // click before the website-cookie handler can open an OAuth form. The | |
| // already-watched click must be intercepted too: letting it bubble would | |
| // re-enter the legacy cookie-authenticated handler even though no toggle | |
| // is wanted. | |
| if (watched && button.matches(".hfnx-watchcta")) { | |
| event.preventDefault(); | |
| event.stopImmediatePropagation(); | |
| setNativeSpaceWatchControl(button, owner, true, false); | |
| return; | |
| } | |
| event.preventDefault(); | |
| event.stopImmediatePropagation(); | |
| void toggleNativeSpaceWatchControl(button, owner, !watched); | |
| } | |
| function normalizeSpaceCookieConsent(value) { | |
| if (!value || typeof value !== "object") return null; | |
| var outcome = String(value.outcome || ""); | |
| if (!/^(accepted|declined|partial)$/.test(outcome)) return null; | |
| return { | |
| outcome: outcome, | |
| analyticsAllowed: value.analyticsAllowed === true, | |
| adsAllowed: value.adsAllowed === true, | |
| }; | |
| } | |
| function readSpaceCookieConsent() { | |
| try { | |
| return normalizeSpaceCookieConsent( | |
| JSON.parse(window.localStorage.getItem(COOKIE_CONSENT_STORAGE_KEY) || "null") | |
| ); | |
| } catch (error) { | |
| return null; | |
| } | |
| } | |
| function writeSpaceCookieConsent(value) { | |
| var consent = normalizeSpaceCookieConsent(value); | |
| if (!consent) return false; | |
| try { | |
| window.localStorage.setItem(COOKIE_CONSENT_STORAGE_KEY, JSON.stringify(consent)); | |
| return true; | |
| } catch (error) { | |
| return false; | |
| } | |
| } | |
| function applySpaceCookieConsentToViewerUrl(value) { | |
| var url = value instanceof URL ? new URL(value.href) : new URL(String(value || ""), WEBSITE_ORIGIN); | |
| var consent = readSpaceCookieConsent(); | |
| if (url.origin !== WEBSITE_ORIGIN) return url; | |
| if (url.searchParams.get("space_interaction") !== "embedded_viewer") return url; | |
| if (!url.searchParams.has("space_load_attempt")) { | |
| url.searchParams.set("space_load_attempt", window.crypto?.randomUUID?.() || | |
| Date.now().toString(36) + "-" + Math.random().toString(36).slice(2)); | |
| } | |
| if (!consent) return url; | |
| url.searchParams.set("space_cookie_consent", consent.outcome); | |
| url.searchParams.set("space_cookie_consent_analytics", consent.analyticsAllowed ? "1" : "0"); | |
| url.searchParams.set("space_cookie_consent_ads", consent.adsAllowed ? "1" : "0"); | |
| return url; | |
| } | |
| window.__hfSpaceApplyCookieConsentToViewerUrl = applySpaceCookieConsentToViewerUrl; | |
| function applyStoredSpaceCookieConsentToFrame(markMutationExpected) { | |
| var frame = document.getElementById("sx-viewer-frame"); | |
| var current = frame?.getAttribute("src") || ""; | |
| if (!frame || !current) return false; | |
| try { | |
| var next = applySpaceCookieConsentToViewerUrl(current).toString(); | |
| if (next === new URL(current, window.location.href).toString()) return false; | |
| if (markMutationExpected) viewerRetryMutationExpected = true; | |
| frame.src = next; | |
| return true; | |
| } catch (error) { | |
| return false; | |
| } | |
| } | |
| function handleSpaceCookieConsent(event) { | |
| var frame = document.getElementById("sx-viewer-frame"); | |
| if ( | |
| event.origin !== WEBSITE_ORIGIN || | |
| !frame || | |
| event.source !== frame.contentWindow || | |
| event.data?.type !== COOKIE_CONSENT_MESSAGE_TYPE || | |
| event.data?.version !== 1 || | |
| event.data?.userInitiated !== true | |
| ) return; | |
| var consent = { | |
| outcome: event.data.outcome, | |
| analyticsAllowed: event.data.analyticsAllowed, | |
| adsAllowed: event.data.adsAllowed, | |
| }; | |
| writeSpaceCookieConsent(consent); | |
| // The Space page's own PostHog client follows the same answer; on later | |
| // visits the shell reads the stored copy at start-up. | |
| try { | |
| window.hfviewerAnalyticsBridge?.applyHfSpaceHostConsent?.(consent); | |
| } catch (error) { /* consent storage above is what must not fail */ } | |
| } | |
| var COMPACT_DISCUSS_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12a8 8 0 0 1-8 8H7l-4 2 1.6-4A9 9 0 1 1 21 12z"/></svg>'; | |
| var COMPACT_INSTALL_ICON = '<span class="sx-compact-logo-icon" aria-hidden="true"></span>'; | |
| var COMPACT_BACK_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5"/><path d="m12 19-7-7 7-7"/></svg>'; | |
| var COMPACT_EXTERNAL_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6"/><path d="M20 4l-9 9"/><path d="M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4"/></svg>'; | |
| var COMPACT_WATCH_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6"/><path d="M10.5 19a1.8 1.8 0 0 0 3 0"/></svg>'; | |
| var COMPACT_EDIT_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13.5 6.5l4 4M4 20l4.5-1 10-10a2.8 2.8 0 0 0-4-4l-10 10z"/><path d="M13 6l4 4"/></svg>'; | |
| function installCompactHeaderStyles() { | |
| if (document.getElementById("sx-compact-header-styles")) return; | |
| var style = document.createElement("style"); | |
| style.id = "sx-compact-header-styles"; | |
| // Phones (<=640px) keep Report and drop Bookmark (1 toggle in 21 days of | |
| // Space traffic); the viewer opens the report panel as a bottom sheet. | |
| style.textContent = | |
| "#sx-header{gap:8px;padding:6px 12px;overflow:visible}" + | |
| "body.sx-viewer-open #sx-header{background:#0d0b14}" + | |
| "#sx-header .sx-brand{flex:0 0 auto;gap:7px}" + | |
| "#sx-header .sx-brand span{display:none!important}" + | |
| "#sx-header .sx-logo{width:22px;height:22px}" + | |
| "#sx-back{flex:0 0 auto;height:32px;box-sizing:border-box;justify-content:center;gap:6px;padding:0 10px;border-radius:8px;font-size:12px;white-space:nowrap}" + | |
| "#sx-back svg,#sx-open svg{width:15px;height:15px;display:block;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}" + | |
| ".sx-model-actions{display:none;align-items:center;gap:5px;margin-left:auto}" + | |
| "body.sx-viewer-open .sx-model-actions{display:flex}" + | |
| ".sx-compact-action{height:32px;min-width:32px;display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:0 9px;border:1px solid #352b47;border-radius:8px;background:#1b1626;color:#ded7eb;font:600 11.5px/1 Inter,system-ui,sans-serif;text-decoration:none;cursor:pointer;white-space:nowrap}" + | |
| ".sx-compact-action[hidden]{display:none!important}" + | |
| ".sx-action-icon[hidden]{display:none!important}" + | |
| ".sx-compact-action:hover,.sx-compact-action:focus-visible{border-color:#645378;background:#241d31;color:#fff;outline:none}" + | |
| ".sx-compact-action svg{width:14px;height:14px;display:block;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}" + | |
| ".sx-compact-logo-icon{position:relative;display:block;width:16px;height:16px;overflow:hidden;border:1px solid rgba(255,255,255,.26);border-radius:5px;background:linear-gradient(135deg,#9cff63,#88d7ff);box-shadow:inset 0 1px 0 rgba(255,255,255,.16)}" + | |
| ".sx-compact-logo-icon::after{content:'';position:absolute;left:2px;top:2px;width:8px;height:8px;border-radius:50%;background:radial-gradient(circle,rgba(255,255,255,.38),rgba(255,255,255,0) 70%)}" + | |
| ".sx-action-icon{display:inline-grid;place-items:center;flex:0 0 auto}" + | |
| ".sx-compact-action.is-bookmarked{color:#9cff63;border-color:rgba(156,255,99,.42);background:rgba(156,255,99,.08)}" + | |
| ".sx-compact-action.is-busy{pointer-events:none;opacity:.72}" + | |
| ".sx-compact-action.is-copy-feedback .sx-action-label{display:inline}" + | |
| ".sx-compact-action--spawned{animation:sx-compact-action-spawn 260ms cubic-bezier(.2,.85,.25,1.2) both}" + | |
| "@keyframes sx-compact-action-spawn{from{opacity:0;transform:translateX(-9px) scale(.88)}to{opacity:1;transform:none}}" + | |
| ".sx-compact-action[data-action='compare-watch']{border-color:rgba(156,255,99,.3);background:rgba(156,255,99,.07);color:#dfffd0}" + | |
| ".sx-compact-action[data-action='compare-install']{border-color:rgba(136,215,255,.3);background:rgba(136,215,255,.08)}" + | |
| ".sx-compact-action--primary{border-color:rgba(156,255,99,.35);background:rgba(156,255,99,.09);color:#dfffd0}" + | |
| ".sx-diff-counts{display:inline-flex;flex-direction:column;align-items:flex-end;font-size:8px;line-height:.82;font-weight:800}" + | |
| ".sx-diff-plus{color:#68dc87}.sx-diff-minus{color:#ff6d77}" + | |
| "#sx-open{flex:0 0 auto;margin-left:0;height:32px;box-sizing:border-box;justify-content:center;gap:6px;padding:0 10px;border-radius:8px;font-size:11.5px}" + | |
| "#sx-open .sx-open-short{display:none}" + | |
| "#sx-auth{position:relative;display:none;align-items:center;flex:0 0 auto}" + | |
| "body.sx-viewer-open #sx-auth:not([hidden]){display:flex}" + | |
| "#sx-auth[hidden]{display:none!important}" + | |
| "#sx-auth .space-native-auth-wrap{display:flex;position:relative;align-items:center}" + | |
| "#sx-auth .hfnx-auth__chip{height:32px;box-sizing:border-box;display:inline-flex!important;align-items:center;gap:7px;padding:0 9px 0 4px;border:1px solid #352b47;border-radius:8px;background:#1b1626;color:#ded7eb;font:600 11.5px/1 Inter,system-ui,sans-serif;cursor:pointer;white-space:nowrap}" + | |
| "#sx-auth .hfnx-auth__chip:hover,#sx-auth .hfnx-auth__chip:focus-visible{border-color:#645378;background:#241d31;color:#fff;outline:none}" + | |
| "#sx-auth .hfnx-auth__avatar{width:24px;height:24px;flex:0 0 24px;border-radius:50%;object-fit:cover}" + | |
| "#sx-auth .hfnx-auth__avatar--letter{display:inline-flex;align-items:center;justify-content:center;font-size:11px}" + | |
| "body #sx-auth .space-native-auth-menu{position:absolute;display:none!important;top:calc(100% + 8px);right:0;z-index:5200;min-width:168px;padding:6px;border:1px solid rgba(255,255,255,.12);border-radius:12px;background:rgba(13,17,29,.98);box-shadow:0 18px 48px rgba(0,0,0,.42)}" + | |
| "body #sx-auth .space-native-auth-menu.is-open{display:grid!important;gap:2px}" + | |
| "#sx-auth .space-native-auth-menu .hfnx-auth__item{display:block;width:100%;box-sizing:border-box;padding:9px 14px;border:0;border-radius:10px;background:transparent;color:#c5d0e3;font:500 13.5px/1.2 Inter,system-ui,sans-serif;text-align:left;text-decoration:none;cursor:pointer}" + | |
| "#sx-auth .space-native-auth-menu .hfnx-auth__item:hover,#sx-auth .space-native-auth-menu .hfnx-auth__item:focus-visible{background:rgba(255,255,255,.06);color:#f5f8ff;outline:none}" + | |
| ".sx-compact-diff-menu{position:fixed;z-index:5000;top:47px;right:58px;min-width:220px;padding:6px;border:1px solid #3b304d;border-radius:10px;background:#171220;box-shadow:0 14px 34px rgba(0,0,0,.42)}" + | |
| ".sx-compact-diff-menu[hidden]{display:none}" + | |
| ".sx-compact-diff-option{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 10px;border:0;border-radius:7px;background:transparent;color:#e9e3f2;font:600 12px/1.25 Inter,system-ui,sans-serif;text-align:left;cursor:pointer}" + | |
| ".sx-compact-diff-option:hover{background:#241d31}" + | |
| ".sx-compact-tooltip{position:fixed;z-index:5100;left:0;top:0;max-width:min(240px,calc(100vw - 20px));padding:7px 9px;border:1px solid rgba(174,163,194,.22);border-radius:8px;background:linear-gradient(180deg,rgba(35,28,48,.98),rgba(20,16,29,.98));color:#f1ebf8;font:600 11px/1.25 Inter,system-ui,sans-serif;letter-spacing:.01em;white-space:nowrap;box-shadow:0 10px 28px rgba(0,0,0,.42),inset 0 1px 0 rgba(255,255,255,.055);pointer-events:none;opacity:0;transform:translateY(-3px);transition:opacity 110ms ease,transform 110ms ease}" + | |
| ".sx-compact-tooltip::before{content:'';position:absolute;top:-4px;left:var(--sx-tooltip-arrow,50%);width:7px;height:7px;border-left:1px solid rgba(174,163,194,.22);border-top:1px solid rgba(174,163,194,.22);background:#231c30;transform:translateX(-50%) rotate(45deg)}" + | |
| ".sx-compact-tooltip.is-visible{opacity:1;transform:translateY(0)}" + | |
| "@media(prefers-reduced-motion:reduce){.sx-compact-tooltip{transition:none}.sx-compact-action--spawned{animation:none}}" + | |
| "@media(max-width:900px){.sx-action-label{display:none}.sx-compact-action{padding:0;min-width:32px}.sx-compact-action--primary .sx-action-label{display:inline}.sx-compact-action--primary{padding:0 7px}}" + | |
| "#hfnx-auth>.space-native-auth-login,.space-native-auth-login{display:inline-flex!important;align-items:center;gap:6px}" + | |
| "#hfnx-auth>.space-native-auth-wrap{display:flex!important;position:relative;align-items:center}" + | |
| ".space-native-auth-login__hf-logo{display:block;width:17px;height:16px;object-fit:contain}" + | |
| "@media(max-width:640px){#sx-header{padding:6px 8px;gap:5px}#sx-header .sx-brand strong{display:none}#sx-back{width:36px;min-width:36px;height:36px;padding:0}#sx-back .sx-back-label{display:none}#sx-back svg{width:18px;height:18px}.sx-compact-action{height:36px;min-width:36px}.sx-compact-action svg{width:16px;height:16px}#sx-auth .hfnx-auth__chip{height:36px}.sx-model-actions{margin-left:auto}.sx-compact-action[data-action='bookmark'],.sx-compact-action[data-action='hugging-face']{display:none!important}#sx-open{min-width:36px;height:36px;padding:0 11px;font-size:12.5px}#sx-open .sx-open-label{display:none}#sx-open .sx-open-short{display:inline}#sx-header.sx-header--tight #sx-open{width:36px;padding:0}#sx-header.sx-header--tight #sx-open .sx-open-short{display:none}#sx-header.sx-header--tight #sx-auth,#sx-header.sx-header--tight #sx-auth .space-native-auth-wrap,#sx-header.sx-header--tight #sx-auth .hfnx-auth__chip{flex-shrink:1;min-width:0}#sx-auth .hfnx-auth__chip{max-width:122px}#sx-auth .hfnx-auth__chip>span:last-child{overflow:hidden;text-overflow:ellipsis}.sx-compact-diff-menu{right:8px;left:8px;min-width:0}}"; | |
| document.head.appendChild(style); | |
| } | |
| // The Space template ships text-glyph arrows ("← All models", "↗") that cannot be | |
| // centred once the labels are hidden on phones. Rebuild both with the same icon | |
| // set as the compact actions; the template script only toggles display/href. | |
| function installCompactNavButtons(back, open) { | |
| back.setAttribute("aria-label", "Back to all models"); | |
| back.innerHTML = '<span class="sx-action-icon">' + COMPACT_BACK_ICON + '</span><span class="sx-back-label">All models</span>'; | |
| open.setAttribute("aria-label", "Open on hfviewer.com"); | |
| open.innerHTML = '<span class="sx-open-label">Open on hfviewer.com</span><span class="sx-open-short">hfviewer.com</span><span class="sx-action-icon">' + COMPACT_EXTERNAL_ICON + '</span>'; | |
| } | |
| // Phones keep a short "hfviewer.com" label on the open link while it fits; | |
| // when the actions or the signed-in chip need the room, it drops to the icon. | |
| function fitCompactHeader(header) { | |
| if (!header) return; | |
| header.classList.remove("sx-header--tight"); | |
| if (header.scrollWidth > header.clientWidth + 1) header.classList.add("sx-header--tight"); | |
| } | |
| function observeCompactHeaderFit(header, elements) { | |
| var fit = function () { fitCompactHeader(header); }; | |
| if (typeof window.ResizeObserver === "function") { | |
| var observer = new window.ResizeObserver(fit); | |
| [header].concat(elements).forEach(function (element) { if (element) observer.observe(element); }); | |
| } else { | |
| window.addEventListener("resize", fit); | |
| } | |
| fit(); | |
| } | |
| function compactAction(action, label, icon, tooltipLabel, options) { | |
| var isLink = options?.link === true; | |
| var button = document.createElement(isLink ? "a" : "button"); | |
| if (!isLink) button.type = "button"; | |
| button.className = "sx-compact-action"; | |
| button.dataset.action = action; | |
| button.dataset.sxTooltip = tooltipLabel || label; | |
| button.setAttribute("aria-label", label); | |
| button.innerHTML = '<span class="sx-action-icon">' + icon + '</span><span class="sx-action-label"></span>'; | |
| button.querySelector(".sx-action-label").textContent = label; | |
| return button; | |
| } | |
| function hideCompactTooltip() { | |
| compactTooltipTarget = null; | |
| compactTooltip?.classList.remove("is-visible"); | |
| } | |
| function positionCompactTooltip(target) { | |
| if (!compactTooltip || compactTooltipTarget !== target || !target?.isConnected) return; | |
| var rect = target.getBoundingClientRect(); | |
| var width = compactTooltip.offsetWidth || 120; | |
| var margin = 10; | |
| var left = Math.max(margin, Math.min(rect.left + rect.width / 2 - width / 2, window.innerWidth - width - margin)); | |
| var top = rect.bottom + 8; | |
| var arrow = Math.max(9, Math.min(width - 9, rect.left + rect.width / 2 - left)); | |
| compactTooltip.style.left = Math.round(left) + "px"; | |
| compactTooltip.style.top = Math.round(top) + "px"; | |
| compactTooltip.style.setProperty("--sx-tooltip-arrow", Math.round(arrow) + "px"); | |
| } | |
| function showCompactTooltip(target) { | |
| var label = String(target?.dataset?.sxTooltip || "").trim(); | |
| if (!label) return; | |
| if (!compactTooltip) { | |
| compactTooltip = document.createElement("div"); | |
| compactTooltip.className = "sx-compact-tooltip"; | |
| compactTooltip.setAttribute("role", "tooltip"); | |
| document.body.appendChild(compactTooltip); | |
| } | |
| compactTooltipTarget = target; | |
| compactTooltip.textContent = label; | |
| positionCompactTooltip(target); | |
| compactTooltip.classList.add("is-visible"); | |
| window.requestAnimationFrame(function () { positionCompactTooltip(target); }); | |
| } | |
| function installCompactHeaderTooltips(header, back, open) { | |
| back.dataset.sxTooltip = "Back to all models"; | |
| open.dataset.sxTooltip = "Open on full website"; | |
| header.addEventListener("pointerover", function (event) { | |
| var target = event.target.closest?.("[data-sx-tooltip]"); | |
| if (!target || !header.contains(target) || target.contains(event.relatedTarget)) return; | |
| showCompactTooltip(target); | |
| }); | |
| header.addEventListener("pointerout", function (event) { | |
| var target = event.target.closest?.("[data-sx-tooltip]"); | |
| if (!target || target.contains(event.relatedTarget)) return; | |
| hideCompactTooltip(); | |
| }); | |
| header.addEventListener("focusin", function (event) { | |
| var target = event.target.closest?.("[data-sx-tooltip]"); | |
| if (target && header.contains(target)) showCompactTooltip(target); | |
| }); | |
| header.addEventListener("focusout", function (event) { | |
| if (!event.target.closest?.("[data-sx-tooltip]")?.contains(event.relatedTarget)) hideCompactTooltip(); | |
| }); | |
| window.addEventListener("resize", hideCompactTooltip); | |
| window.addEventListener("scroll", hideCompactTooltip, true); | |
| } | |
| function setCompactActionLabel(button, label) { | |
| var labelElement = button?.querySelector(".sx-action-label"); | |
| if (labelElement) labelElement.textContent = label; | |
| } | |
| function compactModelBookmarkTarget() { | |
| var repoId = String(compactHeaderState?.repoId || "").trim(); | |
| if (!repoId || repoId.split("/").length !== 2) return null; | |
| return { | |
| target_type: "model", | |
| repo_id: repoId, | |
| title: repoId, | |
| url: "/" + repoId, | |
| }; | |
| } | |
| function renderCompactBookmarkState(repoId, pressed) { | |
| var button = document.querySelector('#sx-model-actions [data-action="bookmark"]'); | |
| if (!button || repoId !== String(compactHeaderState?.repoId || "")) return; | |
| compactBookmarkRepoId = repoId; | |
| compactBookmarkPressed = !!pressed; | |
| button.classList.toggle("is-bookmarked", compactBookmarkPressed); | |
| button.setAttribute("aria-pressed", compactBookmarkPressed ? "true" : "false"); | |
| button.setAttribute("aria-label", compactBookmarkPressed ? "Remove bookmark" : "Bookmark graph"); | |
| button.dataset.sxTooltip = compactBookmarkPressed ? "Remove bookmark" : "Bookmark graph"; | |
| } | |
| async function syncCompactSpaceBookmark() { | |
| var target = compactModelBookmarkTarget(); | |
| if (!target || !window.hfviewerSpaceAuth?.isAuthenticated?.()) return; | |
| if (compactBookmarkRepoId === target.repo_id || compactBookmarkSyncingRepoId === target.repo_id) return; | |
| var sequence = ++compactBookmarkSyncSequence; | |
| compactBookmarkSyncingRepoId = target.repo_id; | |
| try { | |
| var result = await window.hfviewerSpaceAuth.postJson(SPACE_BOOKMARK_STATE_URL, target); | |
| if (sequence !== compactBookmarkSyncSequence) return; | |
| renderCompactBookmarkState(target.repo_id, result?.bookmarked === true); | |
| } catch (error) { | |
| console.warn("Failed to read the native Space bookmark", error); | |
| } finally { | |
| if (compactBookmarkSyncingRepoId === target.repo_id) compactBookmarkSyncingRepoId = ""; | |
| } | |
| } | |
| async function toggleCompactSpaceBookmark(button) { | |
| var target = compactModelBookmarkTarget(); | |
| if (!target || button.classList.contains("is-busy")) return; | |
| var wasPressed = | |
| compactBookmarkRepoId === target.repo_id | |
| ? compactBookmarkPressed === true | |
| : button.getAttribute("aria-pressed") === "true"; | |
| button.classList.add("is-busy"); | |
| try { | |
| var result = await window.hfviewerSpaceAuth.postJson(SPACE_BOOKMARK_TOGGLE_URL, { | |
| ...target, | |
| action: wasPressed ? "remove" : "add", | |
| }); | |
| renderCompactBookmarkState(target.repo_id, result?.bookmarked === true); | |
| window.hfviewerAnalyticsBridge?.capture?.("space_graph_bookmark_toggled", { | |
| product_surface: "hf_space", | |
| source_page: "hf_space", | |
| surface: "space_compact_header", | |
| model_repo_id: target.repo_id, | |
| repo_id: target.repo_id, | |
| bookmarked: result?.bookmarked === true, | |
| }); | |
| } catch (error) { | |
| console.warn("Failed to update the native Space bookmark", error); | |
| if (!window.hfviewerSpaceAuth?.isAuthenticated?.()) { | |
| renderCompactBookmarkState(target.repo_id, false); | |
| window.hfviewerSpaceAuth?.requestLogin?.(button, { | |
| title: "Sign in to bookmark this graph", | |
| description: "Continue with Hugging Face to save it to your hfviewer bookmarks.", | |
| intent: "bookmark_graph", | |
| }); | |
| } | |
| } finally { | |
| button.classList.remove("is-busy"); | |
| } | |
| } | |
| function resetCompactEmbedControls(repoId) { | |
| window.clearTimeout(compactEmbedFeedbackTimer); | |
| window.clearTimeout(compactEditReadmeRevealTimer); | |
| compactEmbedFeedbackTimer = 0; | |
| compactEditReadmeRevealTimer = 0; | |
| var embed = document.querySelector('#sx-model-actions [data-action="embed"]'); | |
| var edit = document.querySelector('#sx-model-actions [data-action="edit-readme"]'); | |
| if (embed) { | |
| embed.classList.remove("is-busy", "is-copy-feedback"); | |
| setCompactActionLabel(embed, "Embed"); | |
| } | |
| if (edit && compactEditReadmeRepoId !== repoId) { | |
| edit.hidden = true; | |
| edit.classList.remove("sx-compact-action--spawned"); | |
| edit.removeAttribute("href"); | |
| compactEditReadmeRepoId = ""; | |
| } | |
| } | |
| function revealCompactEditReadme(repoId) { | |
| var edit = document.querySelector('#sx-model-actions [data-action="edit-readme"]'); | |
| if (!edit || repoId.split("/").length !== 2) return; | |
| var wasHidden = edit.hidden || compactEditReadmeRepoId !== repoId; | |
| edit.href = HUGGING_FACE_ORIGIN + "/" + repoId.split("/").map(encodeURIComponent).join("/") + "/edit/main/README.md"; | |
| edit.target = "_blank"; | |
| edit.rel = "noopener noreferrer"; | |
| edit.hidden = false; | |
| compactEditReadmeRepoId = repoId; | |
| if (wasHidden) { | |
| edit.classList.remove("sx-compact-action--spawned"); | |
| void edit.offsetWidth; | |
| edit.classList.add("sx-compact-action--spawned"); | |
| } | |
| } | |
| function copyCompactEmbedSnippet(value) { | |
| var snippet = String(value || ""); | |
| if (!snippet) return Promise.resolve(false); | |
| // Keep the clipboard write in the Space button's own trusted click. A | |
| // postMessage into the nested hfviewer iframe loses transient user | |
| // activation in Chromium, so both the Clipboard API and execCommand are | |
| // rejected there even though the same button works on hfviewer.com. | |
| var textarea = document.createElement("textarea"); | |
| textarea.value = snippet; | |
| textarea.setAttribute("readonly", ""); | |
| textarea.style.position = "fixed"; | |
| textarea.style.left = "-9999px"; | |
| textarea.style.opacity = "0"; | |
| document.body.appendChild(textarea); | |
| textarea.focus(); | |
| textarea.select(); | |
| textarea.setSelectionRange(0, textarea.value.length); | |
| var copied = false; | |
| try { | |
| copied = document.execCommand("copy"); | |
| } catch (error) { | |
| copied = false; | |
| } | |
| textarea.remove(); | |
| if (copied) return Promise.resolve(true); | |
| // Modern fallback, invoked synchronously before this function yields so | |
| // the browser still associates it with the original click. | |
| if (navigator.clipboard?.writeText) { | |
| try { | |
| return Promise.resolve(navigator.clipboard.writeText(snippet)).then( | |
| function () { return true; }, | |
| function () { return false; } | |
| ); | |
| } catch (error) { | |
| return Promise.resolve(false); | |
| } | |
| } | |
| return Promise.resolve(false); | |
| } | |
| function buildCompactModelCardSnippet(repoId) { | |
| var normalized = String(repoId || "").trim(); | |
| if (normalized.split("/").length !== 2) return ""; | |
| var path = normalized.split("/").map(encodeURIComponent).join("/"); | |
| var slug = normalized | |
| .toLowerCase() | |
| .replace(/[^a-z0-9]+/g, "-") | |
| .replace(/^-+|-+$/g, "") || "model"; | |
| var viewer = new URL("/" + path, WEBSITE_ORIGIN); | |
| viewer.searchParams.set("utm_source", "huggingface"); | |
| viewer.searchParams.set("utm_medium", "embedded_model_card"); | |
| viewer.searchParams.set("utm_campaign", slug + "_card"); | |
| viewer.searchParams.set("utm_content", "embedded_card_open_viewer"); | |
| viewer.searchParams.set("from", "embedded-model-card"); | |
| var card = new URL("/api/card.svg", WEBSITE_ORIGIN); | |
| card.searchParams.set("source", normalized); | |
| card.searchParams.set("granularity", "auto"); | |
| var escapeAttribute = function (value) { | |
| return String(value).replace(/&/g, "&").replace(/"/g, """); | |
| }; | |
| return [ | |
| '<a href="' + escapeAttribute(viewer.href) + '" target="_blank" rel="noopener">', | |
| " <img", | |
| ' src="' + escapeAttribute(card.href) + '"', | |
| ' alt="Architecture graph for ' + escapeAttribute(normalized) + '. Open in hfviewer"', | |
| ' width="100%"', | |
| " />", | |
| "</a>", | |
| ].join("\n"); | |
| } | |
| function buildCompactFrameUrl(href) { | |
| var frame = document.getElementById("sx-viewer-frame"); | |
| if (!frame || !href) return null; | |
| var target; | |
| var current; | |
| try { | |
| target = new URL(href, WEBSITE_ORIGIN); | |
| current = new URL(frame.src || WEBSITE_ORIGIN, WEBSITE_ORIGIN); | |
| } catch (error) { | |
| return null; | |
| } | |
| [ | |
| "space_interaction", | |
| "space_cookie_consent", | |
| "space_cookie_consent_analytics", | |
| "space_cookie_consent_ads", | |
| "tracking_actor", | |
| "tracking_actor_label", | |
| "space_journey_id", | |
| "source_page", | |
| ].forEach(function (key) { | |
| var value = current.searchParams.get(key); | |
| if (value) target.searchParams.set(key, value); | |
| }); | |
| target.searchParams.set("space_interaction", "embedded_viewer"); | |
| return applySpaceCookieConsentToViewerUrl(target); | |
| } | |
| function openCompactFrame(href) { | |
| var frame = document.getElementById("sx-viewer-frame"); | |
| var target = buildCompactFrameUrl(href); | |
| if (!frame || !target) return false; | |
| frame.src = target.href; | |
| return true; | |
| } | |
| function openCompactDiff(href) { | |
| return openCompactFrame(href); | |
| } | |
| function openCompactModel(repoId) { | |
| var normalized = String(repoId || "").replace(/^\/+|\/+$/g, ""); | |
| return normalized ? openCompactFrame("/" + normalized) : false; | |
| } | |
| function finishCompactEmbedCopy(succeeded, repoId, copySequence) { | |
| if ( | |
| copySequence !== compactEmbedCopySequence || | |
| repoId !== String(compactHeaderState?.repoId || "") | |
| ) return; | |
| var embed = document.querySelector('#sx-model-actions [data-action="embed"]'); | |
| if (!embed) return; | |
| embed.classList.remove("is-busy"); | |
| setCompactActionLabel(embed, succeeded ? "Copied" : "Copy failed"); | |
| window.clearTimeout(compactEditReadmeRevealTimer); | |
| compactEditReadmeRevealTimer = 0; | |
| if (succeeded && compactEditReadmeRepoId !== repoId) { | |
| compactEditReadmeRevealTimer = window.setTimeout(function () { | |
| compactEditReadmeRevealTimer = 0; | |
| if ( | |
| copySequence === compactEmbedCopySequence && | |
| repoId === String(compactHeaderState?.repoId || "") | |
| ) revealCompactEditReadme(repoId); | |
| }, COMPACT_EDIT_README_REVEAL_DELAY_MS); | |
| } | |
| window.clearTimeout(compactEmbedFeedbackTimer); | |
| compactEmbedFeedbackTimer = window.setTimeout(function () { | |
| if (copySequence !== compactEmbedCopySequence) return; | |
| compactEmbedFeedbackTimer = 0; | |
| embed.classList.remove("is-copy-feedback"); | |
| setCompactActionLabel(embed, "Embed"); | |
| }, succeeded ? 2600 : 1800); | |
| } | |
| function handleCompactHeaderActionResult(event) { | |
| var frame = document.getElementById("sx-viewer-frame"); | |
| if ( | |
| event.origin !== WEBSITE_ORIGIN || | |
| !frame || | |
| event.source !== frame.contentWindow || | |
| event.data?.type !== HEADER_ACTION_RESULT_MESSAGE_TYPE || | |
| event.data?.version !== 1 || | |
| event.data?.action !== "embed" | |
| ) return; | |
| var repoId = String(compactHeaderState?.repoId || ""); | |
| finishCompactEmbedCopy(event.data?.ok === true, repoId, compactEmbedCopySequence); | |
| } | |
| function installCompactViewerHeader() { | |
| var header = document.getElementById("sx-header"); | |
| var back = document.getElementById("sx-back"); | |
| var open = document.getElementById("sx-open"); | |
| if (!header || !back || !open || document.getElementById("sx-model-actions")) return; | |
| installCompactHeaderStyles(); | |
| header.querySelector(".sx-brand span")?.remove(); | |
| var actions = document.createElement("div"); | |
| actions.id = "sx-model-actions"; | |
| actions.className = "sx-model-actions"; | |
| var embedAction = compactAction("embed", "Embed", '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M4 16H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1"/></svg>', "Embed on model card"); | |
| // The compact controls are installed before the viewer has posted its | |
| // model-specific header state. Do not accept an Embed click during that | |
| // short gap: the compatibility fallback crosses an iframe boundary and | |
| // therefore cannot retain clipboard user activation. | |
| embedAction.disabled = true; | |
| var editReadmeAction = compactAction("edit-readme", "Edit README.md", COMPACT_EDIT_ICON, "Go to edit page", { link: true }); | |
| editReadmeAction.hidden = true; | |
| actions.append( | |
| compactAction("report", "Report", '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/><path d="M12 7v5M12 15.5h.01"/></svg>', "Report graph issue"), | |
| compactAction("hugging-face", "Hugging Face", '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8.5 10h.01M15.5 10h.01M8.5 14c2.1 2 4.9 2 7 0"/></svg>', "Go to HF model", { link: true }), | |
| embedAction, | |
| editReadmeAction, | |
| compactAction("bookmark", "Bookmark", '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3h12v18l-6-4-6 4z"/></svg>', "Bookmark graph"), | |
| compactAction("primary", "Discuss", COMPACT_DISCUSS_ICON, "Discuss model", { link: true }) | |
| ); | |
| var compareBack = compactAction("compare-back", "Back to model", COMPACT_BACK_ICON, "Back to model"); | |
| var compareWatch = compactAction("compare-watch", "Watch releases", COMPACT_WATCH_ICON, "Watch releases"); | |
| var compareInstall = compactAction("compare-install", "Install extension", COMPACT_INSTALL_ICON, "Install Chrome extension", { link: true }); | |
| compareBack.hidden = true; | |
| compareWatch.hidden = true; | |
| compareInstall.hidden = true; | |
| actions.append(compareBack, compareWatch, compareInstall); | |
| header.insertBefore(actions, open); | |
| var compactAuth = document.createElement("div"); | |
| compactAuth.id = "sx-auth"; | |
| compactAuth.hidden = true; | |
| open.insertAdjacentElement("afterend", compactAuth); | |
| installCompactHeaderTooltips(header, back, open); | |
| installCompactNavButtons(back, open); | |
| observeCompactHeaderFit(header, [actions, compactAuth]); | |
| compactDiffMenu = document.createElement("div"); | |
| compactDiffMenu.className = "sx-compact-diff-menu"; | |
| compactDiffMenu.hidden = true; | |
| document.body.appendChild(compactDiffMenu); | |
| actions.addEventListener("click", function (event) { | |
| var button = event.target.closest("[data-action]"); | |
| if (!button) return; | |
| var action = button.dataset.action; | |
| if (action === "edit-readme" && button.hasAttribute("href")) { | |
| try { | |
| window.hfviewerAnalyticsBridge?.capture?.("space_model_card_readme_edit_opened", { | |
| product_surface: "hf_space", | |
| source_page: "hf_space", | |
| surface: "space_compact_header", | |
| placement: "after_embed_copy", | |
| model_repo_id: compactHeaderState?.repoId || "", | |
| repo_id: compactHeaderState?.repoId || "", | |
| href: button.href, | |
| }); | |
| } catch (error) { /* navigation must not depend on analytics */ } | |
| return; | |
| } | |
| if (action === "embed") { | |
| event.preventDefault(); | |
| var repoId = String(compactHeaderState?.repoId || ""); | |
| var copySequence = ++compactEmbedCopySequence; | |
| window.clearTimeout(compactEmbedFeedbackTimer); | |
| window.clearTimeout(compactEditReadmeRevealTimer); | |
| compactEditReadmeRevealTimer = 0; | |
| setCompactActionLabel(button, "Copying…"); | |
| button.classList.add("is-busy", "is-copy-feedback"); | |
| compactEmbedFeedbackTimer = window.setTimeout(function () { | |
| compactEmbedFeedbackTimer = 0; | |
| button.classList.remove("is-busy", "is-copy-feedback"); | |
| setCompactActionLabel(button, "Embed"); | |
| }, 6000); | |
| var snippet = String( | |
| compactHeaderState?.actions?.embed?.snippet || | |
| buildCompactModelCardSnippet(compactHeaderState?.repoId) | |
| ); | |
| if (!snippet) { | |
| finishCompactEmbedCopy(false, repoId, copySequence); | |
| return; | |
| } | |
| void copyCompactEmbedSnippet(snippet).then(function (copied) { | |
| finishCompactEmbedCopy(copied, repoId, copySequence); | |
| try { | |
| window.hfviewerAnalyticsBridge?.capture?.( | |
| copied ? "space_model_card_embed_copied" : "space_model_card_embed_copy_failed", | |
| { | |
| product_surface: "hf_space", | |
| source_page: "hf_space", | |
| surface: "space_compact_header", | |
| model_repo_id: compactHeaderState?.repoId || "", | |
| repo_id: compactHeaderState?.repoId || "", | |
| } | |
| ); | |
| } catch (error) { /* clipboard feedback must not depend on analytics */ } | |
| }); | |
| return; | |
| } | |
| if (action === "bookmark") { | |
| event.preventDefault(); | |
| if (window.hfviewerSpaceAuth?.isAuthenticated?.()) { | |
| void toggleCompactSpaceBookmark(button); | |
| } else { | |
| renderCompactBookmarkState(String(compactHeaderState?.repoId || ""), false); | |
| window.hfviewerSpaceAuth?.requestLogin?.(button, { | |
| title: "Sign in to bookmark this graph", | |
| description: "Continue with Hugging Face to save it to your hfviewer bookmarks.", | |
| intent: "bookmark_graph", | |
| }); | |
| } | |
| return; | |
| } | |
| if (action === "compare-back") { | |
| event.preventDefault(); | |
| openCompactModel(compactHeaderState?.repoId); | |
| return; | |
| } | |
| if (action === "primary" && compactHeaderState?.actions?.primary?.mode === "diff") { | |
| var diffOptions = compactHeaderState.actions.primary.options || []; | |
| event.preventDefault(); | |
| if (diffOptions.length > 1) { | |
| compactDiffMenu.hidden = !compactDiffMenu.hidden; | |
| } else if (diffOptions.length === 1) { | |
| openCompactDiff(diffOptions[0].href); | |
| } | |
| return; | |
| } | |
| if (action === "hugging-face" && button.hasAttribute("href")) { | |
| var hfLinkProperties = { | |
| product_surface: "hf_space", | |
| source_page: "hf_space", | |
| surface: "space_compact_header", | |
| placement: "space_compact_header", | |
| model_repo_id: compactHeaderState?.repoId || "", | |
| repo_id: compactHeaderState?.repoId || "", | |
| href: button.href, | |
| }; | |
| try { | |
| window.hfviewerAnalyticsBridge?.capture?.("open_huggingface_link", hfLinkProperties); | |
| window.hfviewerAnalyticsBridge?.capture?.("external_huggingface_opened", hfLinkProperties); | |
| } catch (error) { /* navigation must not depend on analytics */ } | |
| return; | |
| } | |
| if ( | |
| action === "compare-install" && | |
| button.hasAttribute("href") | |
| ) { | |
| try { | |
| window.hfviewerAnalyticsBridge?.capture?.("chrome_extension_install_clicked", { | |
| product_surface: "hf_space", | |
| source_page: "hf_space", | |
| surface: "space_compact_header", | |
| placement: "space_compact_compare_header", | |
| destination: "chrome_web_store", | |
| extension_id: "mmadlggmpkpiockpjfepaohcllbnakej", | |
| href: button.href, | |
| cta_label: "Install Chrome extension", | |
| }); | |
| } catch (error) { /* navigation must not depend on analytics */ } | |
| return; | |
| } | |
| if ( | |
| action === "primary" && | |
| compactHeaderState?.actions?.primary?.mode === "install" && | |
| button.hasAttribute("href") | |
| ) { | |
| try { | |
| window.hfviewerAnalyticsBridge?.capture?.("chrome_extension_install_clicked", { | |
| product_surface: "hf_space", | |
| source_page: "hf_space", | |
| surface: "space_compact_header", | |
| placement: "space_compact_header", | |
| destination: "chrome_web_store", | |
| extension_id: "mmadlggmpkpiockpjfepaohcllbnakej", | |
| model_repo_id: compactHeaderState?.repoId || "", | |
| href: button.href, | |
| cta_label: "Install Chrome extension", | |
| }); | |
| } catch (error) { /* navigation must not depend on analytics */ } | |
| return; | |
| } | |
| if ( | |
| action === "primary" && | |
| compactHeaderState?.actions?.primary?.mode === "discuss" && | |
| button.hasAttribute("href") | |
| ) { | |
| try { | |
| window.hfviewerAnalyticsBridge?.capture?.("discord_link_clicked", { | |
| product_surface: "hf_space", | |
| source_page: "hf_space", | |
| surface: "space_compact_header", | |
| model_repo_id: compactHeaderState?.repoId || "", | |
| href: button.href, | |
| }); | |
| } catch (error) { /* navigation must not depend on analytics */ } | |
| return; | |
| } | |
| event.preventDefault(); | |
| postHeaderAction(action); | |
| }); | |
| document.addEventListener("click", function (event) { | |
| if (!compactDiffMenu?.hidden && !compactDiffMenu.contains(event.target) && !event.target.closest("[data-action='primary']")) { | |
| compactDiffMenu.hidden = true; | |
| } | |
| }); | |
| } | |
| function postHeaderAction(action, extra) { | |
| var frame = document.getElementById("sx-viewer-frame"); | |
| if (!frameShowsWebsite(frame)) return; | |
| frame.contentWindow.postMessage(Object.assign({ | |
| type: HEADER_ACTION_MESSAGE_TYPE, | |
| version: 1, | |
| action: action, | |
| }, extra || {}), WEBSITE_ORIGIN); | |
| } | |
| function renderCompactHeader(state) { | |
| var previousRepoId = String(compactHeaderState?.repoId || ""); | |
| compactHeaderState = state; | |
| var actions = document.getElementById("sx-model-actions"); | |
| if (!actions) return; | |
| var pageMode = state.viewMode || "model"; | |
| if (previousRepoId !== String(state.repoId || "")) { | |
| compactEmbedCopySequence += 1; | |
| compactBookmarkSyncSequence += 1; | |
| compactBookmarkRepoId = ""; | |
| compactBookmarkPressed = null; | |
| compactBookmarkSyncingRepoId = ""; | |
| resetCompactEmbedControls(String(state.repoId || "")); | |
| } | |
| ["report", "embed", "bookmark", "primary"].forEach(function (name) { | |
| var button = actions.querySelector('[data-action="' + name + '"]'); | |
| if (!button) return; | |
| var actionState = state.actions?.[name] || {}; | |
| button.hidden = pageMode !== "model" || actionState.visible === false; | |
| if (name === "embed") { | |
| button.disabled = !String( | |
| actionState.snippet || buildCompactModelCardSnippet(state.repoId) | |
| ); | |
| } | |
| if (name === "bookmark") { | |
| if ( | |
| window.hfviewerSpaceAuth?.isAuthenticated?.() && | |
| compactBookmarkRepoId === String(state.repoId || "") && | |
| compactBookmarkPressed !== null | |
| ) { | |
| renderCompactBookmarkState(String(state.repoId || ""), compactBookmarkPressed); | |
| } else { | |
| // The embedded website and the Space have separate browser storage. | |
| // Never paint the iframe's cookie-session bookmark state as native | |
| // Space state when there is no native Space profile beside it. | |
| var nativePressed = window.hfviewerSpaceAuth?.isAuthenticated?.() | |
| ? !!actionState.pressed | |
| : false; | |
| button.classList.toggle("is-bookmarked", nativePressed); | |
| button.setAttribute("aria-pressed", nativePressed ? "true" : "false"); | |
| } | |
| } | |
| }); | |
| var editReadme = actions.querySelector('[data-action="edit-readme"]'); | |
| if (editReadme && pageMode !== "model") editReadme.hidden = true; | |
| var huggingFace = actions.querySelector('[data-action="hugging-face"]'); | |
| if (huggingFace) huggingFace.hidden = pageMode !== "model"; | |
| if (huggingFace && pageMode === "model" && state.huggingFaceHref) { | |
| huggingFace.href = new URL(state.huggingFaceHref, HUGGING_FACE_ORIGIN).href; | |
| huggingFace.target = "_blank"; | |
| huggingFace.rel = "noopener noreferrer"; | |
| } | |
| ["compare-back", "compare-watch", "compare-install"].forEach(function (name) { | |
| var button = actions.querySelector('[data-action="' + name + '"]'); | |
| if (!button) return; | |
| var stateKey = name === "compare-back" | |
| ? "compareBack" | |
| : name === "compare-watch" | |
| ? "compareWatch" | |
| : "compareInstall"; | |
| var actionState = state.actions?.[stateKey] || {}; | |
| button.hidden = pageMode === "model" || actionState.visible !== true; | |
| var label = String(actionState.label || button.getAttribute("aria-label") || "").trim(); | |
| if (label) { | |
| button.querySelector(".sx-action-label").textContent = label; | |
| button.setAttribute("aria-label", label); | |
| if (name === "compare-watch") button.dataset.sxTooltip = label; | |
| } | |
| if (name === "compare-install" && actionState.href) { | |
| button.href = new URL(actionState.href, WEBSITE_ORIGIN).href; | |
| button.target = "_blank"; | |
| button.rel = "noopener noreferrer"; | |
| } | |
| }); | |
| var open = document.getElementById("sx-open"); | |
| if (open && state.pagePath) { | |
| open.href = new URL(state.pagePath, WEBSITE_ORIGIN).href; | |
| open.dataset.embeddedPagePath = state.pagePath; | |
| window.__SX_CURRENT_EMBEDDED_PATH = state.pagePath; | |
| } | |
| var primary = actions.querySelector('[data-action="primary"]'); | |
| var primaryState = state.actions?.primary || {}; | |
| if (primary) { | |
| var label = primaryState.label || (primaryState.mode === "diff" ? "Diff" : "Discuss"); | |
| primary.querySelector(".sx-action-label").textContent = label; | |
| var primaryIcon = primary.querySelector(".sx-action-icon"); | |
| primaryIcon.hidden = primaryState.mode === "diff"; | |
| primaryIcon.innerHTML = primaryState.mode === "install" | |
| ? COMPACT_INSTALL_ICON | |
| : primaryState.mode === "discuss" | |
| ? COMPACT_DISCUSS_ICON | |
| : ""; | |
| primary.setAttribute("aria-label", label); | |
| primary.dataset.sxTooltip = primaryState.mode === "install" | |
| ? "Install Chrome extension" | |
| : primaryState.mode === "diff" | |
| ? "View model diff" | |
| : "Discuss model"; | |
| primary.classList.toggle("sx-compact-action--primary", primaryState.mode === "diff"); | |
| if ( | |
| (primaryState.mode === "install" || primaryState.mode === "discuss") && | |
| primaryState.href | |
| ) { | |
| primary.href = new URL(primaryState.href, WEBSITE_ORIGIN).href; | |
| primary.target = "_blank"; | |
| primary.rel = primaryState.mode === "install" ? "noopener noreferrer" : "noopener"; | |
| } else { | |
| primary.removeAttribute("href"); | |
| primary.removeAttribute("target"); | |
| primary.removeAttribute("rel"); | |
| } | |
| primary.querySelector(".sx-diff-counts")?.remove(); | |
| if (primaryState.mode === "diff") { | |
| var totals = (primaryState.options || []).reduce(function (sum, option) { | |
| sum.plus += Number(option.plus || 0); | |
| sum.minus += Number(option.minus || 0); | |
| return sum; | |
| }, { plus: 0, minus: 0 }); | |
| var counts = document.createElement("span"); | |
| counts.className = "sx-diff-counts"; | |
| counts.innerHTML = '<span class="sx-diff-plus"></span><span class="sx-diff-minus"></span>'; | |
| counts.querySelector(".sx-diff-plus").textContent = "+" + totals.plus; | |
| counts.querySelector(".sx-diff-minus").textContent = "−" + totals.minus; | |
| primary.prepend(counts); | |
| } | |
| } | |
| if (pageMode === "model" && window.hfviewerSpaceAuth?.isAuthenticated?.()) { | |
| void syncCompactSpaceBookmark(); | |
| } | |
| compactDiffMenu.replaceChildren(); | |
| (primaryState.options || []).forEach(function (option) { | |
| var button = document.createElement("button"); | |
| button.type = "button"; | |
| button.className = "sx-compact-diff-option"; | |
| button.innerHTML = '<span class="sx-diff-option-label"></span><span class="sx-diff-counts"><span class="sx-diff-plus"></span><span class="sx-diff-minus"></span></span>'; | |
| button.querySelector(".sx-diff-option-label").textContent = option.otherLabel; | |
| button.querySelector(".sx-diff-plus").textContent = "+" + Number(option.plus || 0); | |
| button.querySelector(".sx-diff-minus").textContent = "−" + Number(option.minus || 0); | |
| button.addEventListener("click", function () { | |
| compactDiffMenu.hidden = true; | |
| openCompactDiff(option.href); | |
| }); | |
| compactDiffMenu.appendChild(button); | |
| }); | |
| syncNativeSpaceWatchControls(actions); | |
| } | |
| function handleCompactHeaderState(event) { | |
| var frame = document.getElementById("sx-viewer-frame"); | |
| if ( | |
| event.origin !== WEBSITE_ORIGIN || | |
| !frame || | |
| event.source !== frame.contentWindow || | |
| event.data?.type !== HEADER_STATE_MESSAGE_TYPE || | |
| event.data?.version !== 1 | |
| ) return; | |
| renderCompactHeader(event.data); | |
| } | |
| function replaceSpaceCookieWording(root) { | |
| var walker = document.createTreeWalker(root || document.body, NodeFilter.SHOW_TEXT); | |
| var node; | |
| while ((node = walker.nextNode())) { | |
| var parentTag = node.parentElement?.tagName; | |
| if (parentTag === "SCRIPT" || parentTag === "STYLE") continue; | |
| if (node.nodeValue?.includes("This website stores cookies on your computer")) { | |
| node.nodeValue = node.nodeValue.replaceAll( | |
| "This website stores cookies on your computer", | |
| "This space stores cookies on your computer" | |
| ); | |
| } | |
| } | |
| } | |
| function modelFromLocation() { | |
| return new URLSearchParams(window.location.search).get("model") || ""; | |
| } | |
| var lastParentModel = modelFromLocation(); | |
| function parentQueryString() { | |
| var params = new URLSearchParams(window.location.search); | |
| Array.from(params.keys()).forEach(function (key) { | |
| if (key.startsWith("__")) params.delete(key); | |
| }); | |
| var query = params.toString(); | |
| return query ? "?" + query : ""; | |
| } | |
| function syncModelToHuggingFaceUrl() { | |
| var model = modelFromLocation(); | |
| if (model === lastParentModel) return; | |
| lastParentModel = model; | |
| if (window.parent === window) return; | |
| window.parent.postMessage({ | |
| queryString: parentQueryString(), | |
| hash: window.location.hash || "", | |
| }, HUGGING_FACE_ORIGIN); | |
| } | |
| var MODEL_CLOSE_POPSTATE_TIMEOUT_MS = 400; | |
| function closeModelInPlace(url) { | |
| url.searchParams.delete("model"); | |
| window.history.replaceState(null, "", url.toString()); | |
| window.dispatchEvent(new PopStateEvent("popstate", { state: null })); | |
| } | |
| // The template closes a model with history.back(), which only works when the | |
| // model's own entry is the newest one this page can traverse: | |
| // - inside huggingface.co the Space is a sandboxed cross-origin iframe, so | |
| // back() cannot traverse the joint session history at all, and the parent | |
| // turns each URL message into its own pushState; | |
| // - switching models navigates the nested viewer iframe, whose entries land | |
| // after ours, so back() rewinds that hidden iframe instead of this page. | |
| // Close in place instead: replaceState posts the cleared query to the parent, | |
| // and the replayed popstate is what the template router closes on. | |
| function installModelCloseFallback() { | |
| var originalBack = window.history.back; | |
| if (typeof originalBack !== "function") return; | |
| window.history.back = function () { | |
| var url; | |
| try { | |
| url = new URL(window.location.href); | |
| } catch (error) { | |
| return originalBack.apply(this, arguments); | |
| } | |
| if (!url.searchParams.has("model")) return originalBack.apply(this, arguments); | |
| if (window.parent !== window) { | |
| closeModelInPlace(url); | |
| return; | |
| } | |
| var popped = false; | |
| var onPopState = function () { popped = true; }; | |
| window.addEventListener("popstate", onPopState); | |
| originalBack.apply(this, arguments); | |
| window.setTimeout(function () { | |
| window.removeEventListener("popstate", onPopState); | |
| if (!popped && window.location.href === url.href) closeModelInPlace(url); | |
| }, MODEL_CLOSE_POPSTATE_TIMEOUT_MS); | |
| }; | |
| } | |
| function syncAfterHistoryChange(methodName) { | |
| var original = window.history[methodName]; | |
| if (typeof original !== "function") return; | |
| window.history[methodName] = function () { | |
| var result = original.apply(this, arguments); | |
| syncModelToHuggingFaceUrl(); | |
| return result; | |
| }; | |
| } | |
| function repositoryFromImage(image) { | |
| var alt = String(image.getAttribute("alt") || "").trim(); | |
| var suffix = " architecture graph"; | |
| if (alt.endsWith(suffix)) return alt.slice(0, -suffix.length).trim(); | |
| var card = image.closest("[data-model]"); | |
| if (!card) return ""; | |
| try { | |
| var url = new URL(String(card.dataset.model || ""), window.location.href); | |
| return url.pathname.replace(/^\/+|\/+$/g, ""); | |
| } catch (error) { | |
| return ""; | |
| } | |
| } | |
| function restoreCard(image) { | |
| if (!(image instanceof HTMLImageElement)) return; | |
| if (image.dataset.spaceCardFallback === "1") return; | |
| if (!String(image.currentSrc || image.src || "").startsWith(CARD_PREFIX)) return; | |
| var repository = repositoryFromImage(image); | |
| if (!repository || repository.split("/").length !== 2) return; | |
| image.dataset.spaceCardFallback = "1"; | |
| var query = new URLSearchParams({ | |
| source: repository, | |
| source_page: "hf-space", | |
| }); | |
| image.src = API_PREFIX + "?" + query.toString(); | |
| } | |
| function removeSpaceOnlyProductFooter(root) { | |
| if (!(root instanceof Element || root instanceof Document)) return; | |
| if (root.matches?.("section.products-section[data-page='landing']")) { | |
| root.remove(); | |
| return; | |
| } | |
| root | |
| .querySelectorAll?.("section.products-section[data-page='landing']") | |
| .forEach(function (section) { section.remove(); }); | |
| } | |
| document.addEventListener("error", function (event) { | |
| restoreCard(event.target); | |
| }, true); | |
| function repairAlreadyFailedImages(root) { | |
| var images = (root || document).querySelectorAll("img"); | |
| images.forEach(function (image) { | |
| if (image.complete && image.naturalWidth === 0) restoreCard(image); | |
| }); | |
| } | |
| function returnToSpaceLanding(event) { | |
| var frame = document.getElementById("sx-viewer-frame"); | |
| if ( | |
| event.origin !== WEBSITE_ORIGIN || | |
| !frame || | |
| event.source !== frame.contentWindow || | |
| event.data?.type !== HOME_MESSAGE_TYPE || | |
| event.data?.version !== 1 | |
| ) return; | |
| document.getElementById("sx-back")?.click(); | |
| window.scrollTo({ top: 0 }); | |
| } | |
| function viewerElements() { | |
| return { | |
| overlay: document.getElementById("sx-viewer-overlay"), | |
| frame: document.getElementById("sx-viewer-frame"), | |
| loading: document.getElementById("sx-viewer-loading"), | |
| }; | |
| } | |
| function clearViewerRetryTimer() { | |
| if (viewerRetryTimer) window.clearTimeout(viewerRetryTimer); | |
| viewerRetryTimer = 0; | |
| } | |
| function showViewerLoadingMessage(message, retryable) { | |
| var elements = viewerElements(); | |
| if (!elements.loading) return; | |
| elements.loading.classList.remove("hidden"); | |
| elements.loading.replaceChildren(); | |
| var text = document.createElement("span"); | |
| text.textContent = message; | |
| elements.loading.appendChild(text); | |
| if (!retryable) return; | |
| var button = document.createElement("button"); | |
| button.type = "button"; | |
| button.textContent = "Retry"; | |
| button.style.cssText = | |
| "display:block;margin:14px auto 0;padding:8px 16px;border:1px solid #625276;" + | |
| "border-radius:10px;background:#241b31;color:#fff;font:inherit;cursor:pointer;pointer-events:auto"; | |
| button.addEventListener("click", function () { | |
| retryViewerFrame("manual"); | |
| }); | |
| elements.loading.appendChild(button); | |
| } | |
| function retryViewerFrame(reason) { | |
| var elements = viewerElements(); | |
| if (!elements.frame || !elements.frame.src) return; | |
| clearViewerRetryTimer(); | |
| viewerRetryCount += 1; | |
| viewerLoadSequence += 1; | |
| viewerLifecycleState = "loading"; | |
| viewerLifecyclePhase = ""; | |
| viewerLoadStartedAt = Date.now(); | |
| showViewerLoadingMessage("Loading architecture graph\u2026", false); | |
| try { | |
| var url = new URL(elements.frame.src); | |
| url.searchParams.set("space_retry", String(viewerRetryCount)); | |
| url.searchParams.set("space_load_nonce", String(Date.now())); | |
| url.searchParams.delete("space_load_attempt"); | |
| url = applySpaceCookieConsentToViewerUrl(url); | |
| viewerRetryMutationExpected = true; | |
| elements.frame.src = url.toString(); | |
| } catch (error) { | |
| viewerRetryMutationExpected = true; | |
| elements.frame.src = elements.frame.src; | |
| } | |
| scheduleViewerRecovery(viewerLoadSequence, VIEWER_FAILURE_DELAY_MS); | |
| startViewerHandshake(); | |
| try { | |
| window.hfviewerAnalyticsBridge?.capture?.("space_embedded_viewer_retried", { | |
| product_surface: "hf_space", | |
| retry_reason: reason, | |
| retry_count: viewerRetryCount, | |
| space_load_attempt: new URL(elements.frame.src).searchParams.get("space_load_attempt"), | |
| model_repo_id: new URL(elements.frame.src).pathname.replace(/^\/+|\/+$/g, ""), | |
| }); | |
| } catch (error) { /* recovery must not depend on analytics */ } | |
| } | |
| function scheduleViewerRecovery(sequence, delay) { | |
| clearViewerRetryTimer(); | |
| viewerRetryTimer = window.setTimeout(function () { | |
| viewerRetryTimer = 0; | |
| var elements = viewerElements(); | |
| if ( | |
| sequence !== viewerLoadSequence || | |
| !elements.overlay?.classList.contains("open") || | |
| elements.loading?.classList.contains("hidden") | |
| ) return; | |
| showViewerLoadingMessage( | |
| viewerLifecyclePhase || "The graph is taking longer than expected.", | |
| true | |
| ); | |
| }, delay); | |
| } | |
| function armViewerRecovery() { | |
| var elements = viewerElements(); | |
| if (!elements.frame || !elements.loading) return; | |
| viewerRetryCount = 0; | |
| viewerLoadSequence += 1; | |
| viewerLifecycleState = "loading"; | |
| viewerLifecyclePhase = ""; | |
| viewerLoadStartedAt = Date.now(); | |
| showViewerLoadingMessage("Loading architecture graph\u2026", false); | |
| scheduleViewerRecovery(viewerLoadSequence, VIEWER_RETRY_DELAY_MS); | |
| startViewerHandshake(); | |
| } | |
| function clearViewerHandshake() { | |
| window.clearTimeout(viewerHandshakeTimer); | |
| viewerHandshakeTimer = 0; | |
| } | |
| // Until the viewer document commits, the iframe still holds its initial | |
| // same-origin about:blank; posting to it with the website origin only logs a | |
| // targetOrigin mismatch. A readable location means it has not navigated yet. | |
| function frameShowsWebsite(frame) { | |
| if (!frame?.contentWindow) return false; | |
| try { | |
| void frame.contentWindow.location.href; | |
| return false; | |
| } catch (error) { | |
| return true; | |
| } | |
| } | |
| function spaceAnalyticsDistinctId() { | |
| try { | |
| if (window.hfviewerAnalyticsBridge?.hasAnalyticsConsent?.() !== true) return ""; | |
| var id = window.posthog?.get_distinct_id?.(); | |
| return typeof id === "string" ? id : ""; | |
| } catch (error) { | |
| return ""; | |
| } | |
| } | |
| function sendViewerHandshake() { | |
| var frame = viewerElements().frame; | |
| if (!frame?.getAttribute("src") || !frameShowsWebsite(frame)) return; | |
| try { | |
| var url = new URL(frame.src); | |
| if (url.origin !== WEBSITE_ORIGIN || !url.searchParams.get("space_load_attempt")) return; | |
| var message = { | |
| type: "hfviewer-space-viewer-init", version: 1, | |
| attemptId: url.searchParams.get("space_load_attempt"), | |
| repoId: decodeURIComponent(url.pathname).replace(/^\/+|\/+$/g, ""), | |
| }; | |
| // Join key for the viewer's analytics, sent only under analytics consent | |
| // and only to the website origin (the viewer re-checks its own consent). | |
| var spaceDistinctId = spaceAnalyticsDistinctId(); | |
| if (spaceDistinctId) message.spaceDistinctId = spaceDistinctId; | |
| frame.contentWindow.postMessage(message, WEBSITE_ORIGIN); | |
| } catch (error) { /* the next tick or document load can retry the handshake */ } | |
| } | |
| function startViewerHandshake() { | |
| clearViewerHandshake(); | |
| var sequence = viewerLoadSequence; | |
| function tick() { | |
| viewerHandshakeTimer = 0; | |
| if (sequence !== viewerLoadSequence || !viewerElements().overlay?.classList.contains("open")) return; | |
| sendViewerHandshake(); | |
| // The load listener also covers a document that starts after this bound. | |
| if (Date.now() - viewerLoadStartedAt < 45000) { | |
| viewerHandshakeTimer = window.setTimeout(tick, 500); | |
| } | |
| } | |
| tick(); | |
| } | |
| function installViewerRecovery() { | |
| var elements = viewerElements(); | |
| if (!elements.frame || !elements.loading) return; | |
| window.addEventListener("message", handleViewerLifecycle); | |
| // Document load only establishes the bridge; a checked renderer message | |
| // is still required before dismissing the loading overlay. | |
| elements.frame.addEventListener("load", sendViewerHandshake); | |
| new MutationObserver(function (records) { | |
| if (records.some(function (record) { return record.attributeName === "src"; })) { | |
| if (viewerRetryMutationExpected) { | |
| viewerRetryMutationExpected = false; | |
| return; | |
| } | |
| applyStoredSpaceCookieConsentToFrame(true); | |
| armViewerRecovery(); | |
| } | |
| }).observe(elements.frame, { attributes: true, attributeFilter: ["src"] }); | |
| new MutationObserver(function () { | |
| if (!elements.overlay?.classList.contains("open")) { | |
| clearViewerRetryTimer(); | |
| clearViewerHandshake(); | |
| } | |
| else if (viewerLifecycleState === "ready" || viewerLifecycleState === "content") { | |
| elements.loading.classList.add("hidden"); | |
| } else if (!viewerRetryTimer && elements.frame.getAttribute("src")) { | |
| scheduleViewerRecovery(viewerLoadSequence, VIEWER_RETRY_DELAY_MS); | |
| } | |
| }).observe(elements.overlay, { attributes: true, attributeFilter: ["class"] }); | |
| if (elements.frame.getAttribute("src")) armViewerRecovery(); | |
| } | |
| function handleViewerLifecycle(event) { | |
| var elements = viewerElements(); | |
| var data = event.data; | |
| if (event.origin !== WEBSITE_ORIGIN || event.source !== elements.frame?.contentWindow || | |
| data?.type !== VIEWER_LIFECYCLE_MESSAGE_TYPE || data.version !== 1) return; | |
| var url; | |
| try { url = new URL(elements.frame.src); } catch (error) { return; } | |
| if (!data.attemptId || data.attemptId !== url.searchParams.get("space_load_attempt") || | |
| String(data.repoId || "").toLowerCase() !== decodeURIComponent(url.pathname).replace(/^\/+|\/+$/g, "").toLowerCase()) return; | |
| if (viewerLifecycleState === "ready") return; | |
| var phases = { | |
| shell_ready: "Opening architecture graph\u2026", | |
| resolving: "Finding the model graph\u2026", | |
| loading: "Loading architecture graph\u2026", | |
| rendering: "Drawing architecture graph\u2026", | |
| }; | |
| if (data.state === "ready" || data.state === "content") { | |
| viewerLifecycleState = data.state; | |
| clearViewerRetryTimer(); | |
| elements.loading?.replaceChildren(); | |
| elements.loading?.classList.add("hidden"); | |
| } else if (data.state === "progress" && phases[data.phase]) { | |
| viewerLifecyclePhase = phases[data.phase]; | |
| // Once the inner preparation/error UI is accessible, keep it visible | |
| // through subsequent resolver and renderer progress. | |
| if (viewerLifecycleState !== "content") { | |
| showViewerLoadingMessage(viewerLifecyclePhase, !viewerRetryTimer); | |
| } | |
| } else return; | |
| clearViewerHandshake(); | |
| try { | |
| var properties = { | |
| product_surface: "hf_space", model_repo_id: data.repoId, | |
| space_journey_id: url.searchParams.get("space_journey_id"), | |
| space_load_attempt: data.attemptId, state: data.state, phase: data.phase, | |
| load_ms: Math.max(0, Date.now() - viewerLoadStartedAt), | |
| }; | |
| window.hfviewerAnalyticsBridge?.capture?.("space_viewer_lifecycle", properties); | |
| if (data.state === "ready") { | |
| window.hfviewerAnalyticsBridge?.capture?.("space_embedded_viewer_loaded", { | |
| ...properties, readiness_source: "graph_ready", | |
| }); | |
| } | |
| } catch (error) { /* lifecycle must not depend on analytics */ } | |
| } | |
| removeSpaceOnlyProductFooter(document); | |
| installCompactViewerHeader(); | |
| replaceSpaceCookieWording(document.body); | |
| repairAlreadyFailedImages(document); | |
| syncAfterHistoryChange("replaceState"); | |
| syncAfterHistoryChange("pushState"); | |
| installModelCloseFallback(); | |
| window.addEventListener("popstate", syncModelToHuggingFaceUrl); | |
| window.addEventListener("message", returnToSpaceLanding); | |
| window.addEventListener("message", handleCompactHeaderState); | |
| window.addEventListener("message", handleCompactHeaderActionResult); | |
| window.addEventListener("message", handleSpaceCookieConsent); | |
| document.addEventListener("click", handleNativeSpaceWatchClick, true); | |
| window.addEventListener("hfviewer-space-auth-changed", function (event) { | |
| compactBookmarkSyncSequence += 1; | |
| compactBookmarkRepoId = ""; | |
| compactBookmarkPressed = null; | |
| compactBookmarkSyncingRepoId = ""; | |
| if (event.detail?.authenticated) { | |
| void syncCompactSpaceBookmark(); | |
| void loadNativeSpaceWatchlistState(); | |
| } else { | |
| nativeSpaceWatchStateSequence += 1; | |
| nativeSpaceWatchStateLoaded = false; | |
| nativeSpaceWatchedOwners = new Set(); | |
| nativeSpaceWatchOperations.clear(); | |
| var bookmark = document.querySelector('#sx-model-actions [data-action="bookmark"]'); | |
| if (bookmark) { | |
| bookmark.classList.remove("is-bookmarked"); | |
| bookmark.setAttribute("aria-pressed", "false"); | |
| bookmark.setAttribute("aria-label", "Bookmark graph"); | |
| bookmark.dataset.sxTooltip = "Bookmark graph"; | |
| } | |
| } | |
| }); | |
| applyStoredSpaceCookieConsentToFrame(false); | |
| installViewerRecovery(); | |
| new MutationObserver(function (records) { | |
| records.forEach(function (record) { | |
| record.addedNodes.forEach(function (node) { | |
| if (!(node instanceof Element)) return; | |
| replaceSpaceCookieWording(node); | |
| removeSpaceOnlyProductFooter(node); | |
| if (node instanceof HTMLImageElement) restoreCard(node); | |
| repairAlreadyFailedImages(node); | |
| syncNativeSpaceWatchControls(node); | |
| }); | |
| }); | |
| }).observe(document.body, { childList: true, subtree: true }); | |
| })(); | |