CoolFace
Apppublic

lerobot/robot-learning-tutorial

sourceHugging Faceupdated 1y agoView on Hugging Face
508likes
Hero.astro612 linesDownload Raw Back to components
1---2import HtmlEmbed from "./HtmlEmbed.astro";3import Image from "./Image.astro";4import lerobotLogo from "../content/assets/image/figures/ch1/ch1-lerobot-figure1.png";5 6interface Props {7  title: string; // may contain HTML (e.g., <br/>)8  titleRaw?: string; // plain title for slug/PDF (optional)9  description?: string;10  authors?: Array<11    string | { name: string; url?: string; affiliationIndices?: number[] }12  >;13  affiliations?: Array<{ id: number; name: string; url?: string }>;14  affiliation?: string; // legacy single affiliation15  published?: string;16  doi?: string;17  pdfProOnly?: boolean; // Gate PDF download to Pro users only18}19 20const {21  title,22  titleRaw,23  description,24  authors = [],25  affiliations = [],26  affiliation,27  published,28  doi,29  pdfProOnly = false,30} = Astro.props as Props;31 32type Author = { name: string; url?: string; affiliationIndices?: number[] };33 34function normalizeAuthors(35  input: Array<36    | string37    | {38        name?: string;39        url?: string;40        link?: string;41        affiliationIndices?: number[];42        affiliations?: number[];43      }44  >,45): Author[] {46  return (Array.isArray(input) ? input : [])47    .map((a) => {48      if (typeof a === "string") {49        return { name: a } as Author;50      }51      const name = (a?.name ?? "").toString();52      const url = (a?.url ?? a?.link) as string | undefined;53      // Support both 'affiliationIndices' and 'affiliations' as property names54      const affiliationIndices = Array.isArray((a as any)?.affiliationIndices)55        ? (a as any).affiliationIndices56        : Array.isArray((a as any)?.affiliations)57        ? (a as any).affiliations58        : undefined;59      return { name, url, affiliationIndices } as Author;60    })61    .filter((a) => a.name && a.name.trim().length > 0);62}63 64const normalizedAuthors: Author[] = normalizeAuthors(authors as any);65 66// Determine if affiliation superscripts should be shown (only when there are multiple distinct affiliations referenced by authors)67const authorAffiliationIndexSet = new Set<number>();68for (const author of normalizedAuthors) {69  const indices = Array.isArray(author.affiliationIndices)70    ? author.affiliationIndices71    : [];72  for (const idx of indices) {73    if (typeof idx === "number") {74      authorAffiliationIndexSet.add(idx);75    }76  }77}78const hasMultipleAffiliations =79  Array.isArray(affiliations) && affiliations.length > 1;80const shouldShowAffiliationSupers = hasMultipleAffiliations && authorAffiliationIndexSet.size > 0;81 82function stripHtml(text: string): string {83  return String(text || "").replace(/<[^>]*>/g, "");84}85 86function slugify(text: string): string {87  return (88    String(text || "")89      .normalize("NFKD")90      .replace(/\p{Diacritic}+/gu, "")91      .toLowerCase()92      .replace(/[^a-z0-9]+/g, "-")93      .replace(/^-+|-+$/g, "")94      .slice(0, 120) || "article"95  );96}97 98const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title);99const pdfFilename = `${slugify(pdfBase)}.pdf`;100---101 102<section class="hero">103  <h1 class="hero-title" set:html={title} />104  <!-- <div class="hero-banner">105    <HtmlEmbed src="banner.html" frameless />106    <Image 107      src={lerobotLogo} 108      alt="LeRobot Logo" 109    />110    {description && <p class="hero-desc">{description}</p>}111  </div> -->112</section>113 114<header class="meta" aria-label="Article meta information">115  <div class="meta-container">116    {117      normalizedAuthors.length > 0 && (118        <div class="meta-container-cell">119          <h3>Author{normalizedAuthors.length > 1 ? "s" : ""}</h3>120          <ul class="authors">121            {normalizedAuthors.map((a, i) => {122              const supers =123                shouldShowAffiliationSupers &&124                Array.isArray(a.affiliationIndices) &&125                a.affiliationIndices.length ? (126                  <sup>{a.affiliationIndices.join(", ")}</sup>127                ) : null;128              return (129                <li>130                  {a.url ? <a href={a.url}>{a.name}</a> : a.name}{supers}{i < normalizedAuthors.length - 1 && <span set:html=",&nbsp;" />}131                </li>132              );133            })}134          </ul>135        </div>136      )137    }138    {139      Array.isArray(affiliations) && affiliations.length > 0 && (140        <div class="meta-container-cell meta-container-cell--affiliations">141          <h3>Affiliation{affiliations.length > 1 ? "s" : ""}</h3>142          {hasMultipleAffiliations ? (143            <ol class="affiliations">144              {affiliations.map((af) => (145                <li value={af.id}>146                  {af.url ? (147                    <a href={af.url} target="_blank" rel="noopener noreferrer">148                      {af.name}149                    </a>150                  ) : (151                    af.name152                  )}153                </li>154              ))}155            </ol>156          ) : (157            <p>158              {affiliations[0]?.url ? (159                <a160                  href={affiliations[0].url}161                  target="_blank"162                  rel="noopener noreferrer"163                >164                  {affiliations[0].name}165                </a>166              ) : (167                affiliations[0]?.name168              )}169            </p>170          )}171        </div>172      )173    }174    {175      (!affiliations || affiliations.length === 0) && affiliation && (176        <div class="meta-container-cell meta-container-cell--affiliations">177          <h3>Affiliation</h3>178          <p>{affiliation}</p>179        </div>180      )181    }182    {183      published && (184        <div class="meta-container-cell meta-container-cell--published">185          <h3>Published</h3>186          <p>{published}</p>187        </div>188      )189    }190    <!-- {doi && (191      <div class="meta-container-cell">192        <h3>DOI</h3>193        <p><a href={`https://doi.org/${doi}`} target="_blank" rel="noopener noreferrer">{doi}</a></p>194      </div>195    )} -->196    <div class="meta-container-cell meta-container-cell--pdf">197      <div class="pdf-header-wrapper">198        <h3>PDF</h3>199        <span class="pro-badge-wrapper" style="display: none;">200          <span class="pro-badge-prefix">- you are</span>201          <span class="pro-badge">PRO</span>202        </span>203        <span class="pro-only-label" style="display: none;">204          <span class="pro-only-dash">-</span>205          <span class="pro-only-text">pro only</span>206          <svg class="pro-only-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">207            <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>208            <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>209          </svg>210        </span>211      </div>212      <div id="pdf-download-container" data-pdf-pro-only={pdfProOnly.toString()}>213        <p class="pdf-loading">Checking access...</p>214        <p class="pdf-pro-only" style="display: none;">215          <a216            class="button"217            href={`http://arxiv.org/pdf/2510.12403`}218            target="_blank"219            rel="noopener noreferrer"220          >221            Download PDF222          </a>223        </p>224        <div class="pdf-locked" style="display: none;">225          <a 226            class="button button-locked" 227            href="https://huggingface.co/subscribe/pro"228            target="_blank"229            rel="noopener noreferrer"230          >231            <svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 12 12" fill="none">232              <path d="M6.48 1.26c0 1.55.67 2.58 1.5 3.24.86.68 1.9 1 2.58 1.07v.86A5.3 5.3 0 0 0 7.99 7.5a3.95 3.95 0 0 0-1.51 3.24h-.96c0-1.55-.67-2.58-1.5-3.24a5.3 5.3 0 0 0-2.58-1.07v-.86A5.3 5.3 0 0 0 4.01 4.5a3.95 3.95 0 0 0 1.51-3.24h.96Z" fill="currentColor"></path>233            </svg>234            <span class="locked-title">Subscribe to Pro</span>235          </a>236        </div>237      </div>238    </div>239  </div>240</header>241 242<script>243  // PDF access control for Pro users only244  245  // ⚙️ Configuration for local development246  const LOCAL_IS_PRO = false; // Set to true to test Pro access locally247  248  const FALLBACK_TIMEOUT_MS = 3000;249  let userPlanChecked = false;250 251  // Check if PDF Pro gating is enabled252  const pdfContainer = document.querySelector("#pdf-download-container") as HTMLElement;253  const pdfProOnly = pdfContainer?.getAttribute("data-pdf-pro-only") === "true";254 255  /**256   * Check if user has Pro access257   * Isolated logic for Pro user verification258   * Expected plan structure: { user: "pro", org: "enterprise" }259   */260  function isProUser(plan: any): boolean {261    if (!plan) return false;262    263    // Check if user property is "pro"264    return plan.user === "pro";265  }266 267  /**268   * Update UI based on user's Pro status269   */270  function updatePdfAccess(isPro: boolean) {271    const loadingEl = document.querySelector(".pdf-loading") as HTMLElement;272    const proOnlyEl = document.querySelector(".pdf-pro-only") as HTMLElement;273    const lockedEl = document.querySelector(".pdf-locked") as HTMLElement;274    const proOnlyLabel = document.querySelector(".pro-only-label") as HTMLElement;275    const proBadgeWrapper = document.querySelector(".pro-badge-wrapper") as HTMLElement;276    277    // Hide loading state278    if (loadingEl) loadingEl.style.display = "none";279    280    // If PDF Pro gating is disabled, just show the download button281    if (!pdfProOnly) {282      if (proOnlyEl) proOnlyEl.style.display = "block";283      if (proOnlyLabel) proOnlyLabel.style.display = "none";284      if (lockedEl) lockedEl.style.display = "none";285      if (proBadgeWrapper) proBadgeWrapper.style.display = "none";286      return;287    }288    289    // Show appropriate state based on Pro status290    if (isPro) {291      if (proOnlyEl) proOnlyEl.style.display = "block";292      if (proOnlyLabel) proOnlyLabel.style.display = "none";293      if (lockedEl) lockedEl.style.display = "none";294      if (proBadgeWrapper) proBadgeWrapper.style.display = "inline-flex";295    } else {296      if (proOnlyEl) proOnlyEl.style.display = "none";297      if (proOnlyLabel) proOnlyLabel.style.display = "inline-flex";298      if (lockedEl) lockedEl.style.display = "block";299      if (proBadgeWrapper) proBadgeWrapper.style.display = "none";300    }301  }302 303  /**304   * Handle user plan response305   */306  function handleUserPlan(plan: any) {307    userPlanChecked = true;308    const isPro = isProUser(plan);309    updatePdfAccess(isPro);310    311    // Optional: log for debugging312    console.log("[PDF Access]", { plan, isPro });313  }314 315  /**316   * Fallback behavior when no parent window responds317   * Uses LOCAL_IS_PRO configuration for local development318   */319  function handleFallback() {320    if (LOCAL_IS_PRO) {321      handleUserPlan({ user: "pro" });322    } else {323      handleUserPlan({ user: "free" });324    }325  }326 327  // If PDF Pro gating is disabled, show the download button immediately328  if (!pdfProOnly) {329    updatePdfAccess(true);330  } else {331    // Listen for messages from parent window (Hugging Face Spaces)332    window.addEventListener("message", (event) => {333      if (event.data.type === "USER_PLAN") {334        handleUserPlan(event.data.plan);335      }336    });337 338    // Request user plan on page load339    if (window.parent && window.parent !== window) {340      // We're in an iframe, request user plan341      window.parent.postMessage({ type: "USER_PLAN_REQUEST" }, "*");342      343      // Fallback if no response after timeout344      setTimeout(() => {345        if (!userPlanChecked) {346          handleFallback();347        }348      }, FALLBACK_TIMEOUT_MS);349    } else {350      // Not in iframe (local development), use fallback immediately351      handleFallback();352    }353  }354</script>355 356<style>357  /* Hero (full-width) */358  .hero {359    width: 100%;360    padding: 48px 16px 16px;361    text-align: center;362  }363  .hero-title {364    font-size: clamp(28px, 4vw, 48px);365    font-weight: 800;366    line-height: 1.1;367    margin: 0 0 8px;368    max-width: 100%;369    margin: auto;370  }371  .hero-banner {372    max-width: 980px;373    margin: 0 auto;374    border: 1px solid var(--border-color);375    border-radius: 8px;376    margin-top: 8px;377    margin-bottom: 8px;378    overflow: hidden;379  }380  .hero-banner .ri-root {381    border-radius: 8px;382  }383  .hero-desc {384    color: var(--muted-color);385    font-style: italic;386    margin: 0 0 16px 0;387  }388 389  /* Meta (byline-like header) */390  .meta {391    border-top: 1px solid var(--border-color);392    border-bottom: 1px solid var(--border-color);393    padding: 1rem 0;394    font-size: 0.9rem;395  }396  .meta-container {397    max-width: 760px;398    display: flex;399    flex-direction: row;400    justify-content: space-between;401    margin: 0 auto;402    padding: 0 var(--content-padding-x);403    gap: 8px;404  }405  /* Subtle underline for links in meta; keep buttons without underline */406  .meta-container a:not(.button) {407    color: var(--primary-color);408    text-decoration: underline;409    text-underline-offset: 2px;410    text-decoration-thickness: 0.06em;411    text-decoration-color: var(--link-underline);412    transition: text-decoration-color 0.15s ease-in-out;413  }414  .meta-container a:hover {415    text-decoration-color: var(--link-underline-hover);416  }417  .meta-container a.button,418  .meta-container .button {419    text-decoration: none;420  }421  .meta-container-cell {422    display: flex;423    flex-direction: column;424    gap: 8px;425    flex: 1;426    min-width: 0;427  }428  .meta-container-cell h3 {429    margin: 0;430    font-size: 12px;431    font-weight: 400;432    color: var(--muted-color);433    text-transform: uppercase;434    letter-spacing: 0.02em;435  }436  .meta-container-cell p {437    margin: 0;438  }439  .authors {440    margin: 0;441    list-style-type: none;442    padding-left: 0;443    display: flex;444    flex-wrap: wrap;445  }446  .authors li {447    white-space: nowrap;448    padding:0;449  }450  .affiliations {451    margin: 0;452    padding-left: 1.25em;453  }454  .affiliations li {455    margin: 0;456  }457 458  header.meta .meta-container {459    flex-wrap: wrap;460    row-gap: 12px;461  }462 463  @media (max-width: 768px) {464    .meta-container-cell--affiliations,465    .meta-container-cell--pdf {466      text-align: right;467    }468  }469 470  @media print {471    .meta-container-cell--pdf {472      display: none !important;473    }474  }475 476  /* PDF access control styles */477  .pdf-header-wrapper {478    display: flex;479    align-items: center;480    gap: 6px;481    line-height: 1;482  }483 484  .pdf-header-wrapper h3 {485    line-height: 1;486  }487 488  .pdf-loading {489    color: var(--muted-color);490    font-size: 0.9em;491  }492 493  .pdf-pro-only {494    margin: 0;495  }496 497  .pro-badge-wrapper {498    display: inline-flex;499    align-items: center;500    gap: 5px;501    font-style: normal;502  }503 504  .pro-badge-prefix {505    font-size: 0.85em;506    opacity: 0.5;507    font-weight: 400;508    font-style: normal;509  }510 511  .pro-badge {512    display: inline-block;513    border: 1px solid rgba(0, 0, 0, 0.025);514    background: linear-gradient(to bottom right, #f9a8d4, #86efac, #fde047);515    color: black;516    padding: 1px 5px;517    border-radius: 3px;518    font-size: 0.5rem;519    font-weight: 700;520    font-style: normal;521    letter-spacing: 0.025em;522    text-transform: uppercase;523  }524 525  /* Dark mode pro badge */526  :global(.dark) .pro-badge,527  :global([data-theme="dark"]) .pro-badge {528    background: linear-gradient(to bottom right, #ec4899, #22c55e, #eab308);529    border-color: rgba(255, 255, 255, 0.15);530  }531 532  .pro-only-label {533    display: inline-flex;534    flex-direction: row;535    align-items: center;536    gap: 5px;537    font-size: 0.85em;538    opacity: 0.5;539    font-weight: 400;540    line-height: 1;541  }542 543  .pro-only-dash {544    display: inline-flex;545    align-items: center;546    line-height: 1;547  }548 549  .pro-only-icon {550    width: 11px;551    height: 11px;552    flex-shrink: 0;553    display: inline-flex;554    align-items: center;555  }556 557  .pro-only-text {558    display: inline-flex;559    align-items: center;560    line-height: 1;561  }562 563  .pdf-locked {564    display: block;565  }566 567  .button-locked {568    display: inline-flex;569    align-items: center;570    gap: 6px;571    background: linear-gradient(135deg, 572                  var(--primary-color) 0%, 573                  oklch(from var(--primary-color) calc(l - 0.1) calc(c + 0.05) calc(h - 60)) 100%);574    border-radius: var(--button-radius);575    padding: var(--button-padding-y) var(--button-padding-x);576    font-size: var(--button-font-size);577    line-height: 1;578    color: var(--on-primary);579    position: relative;580    overflow: hidden;581    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);582    font-weight: normal;583    border-color: rgba(0, 0, 0, 0.15);584  }585 586  .button-locked:active {587    transform: translateY(0);588  }589 590  .lock-icon {591    font-size: 1em;592    flex-shrink: 0;593    position: relative;594    z-index: 1;595  }596 597  .locked-title {598    position: relative;599    z-index: 1;600  }601 602  /* Dark mode locked button - inherits from light mode variables */603 604  @media (max-width: 768px) {605    .meta-container-cell--pdf {606      display: flex;607      flex-direction: column;608      align-items: flex-end;609    }610  }611</style>612