lerobot/robot-learning-tutorial
508
1---2import * as ArticleMod from "../content/article.mdx";3 4import Hero from "../components/Hero.astro";5import Footer from "../components/Footer.astro";6import ThemeToggle from "../components/ThemeToggle.astro";7import Seo from "../components/Seo.astro";8import TableOfContents from "../components/TableOfContents.astro";9// Default OG image served from public/10const ogDefaultUrl = "/thumb.auto.jpg";11import "katex/dist/katex.min.css";12import "../styles/global.css";13const articleFM = (ArticleMod as any).frontmatter ?? {};14const Article = (ArticleMod as any).default;15const docTitle = articleFM?.title ?? "Untitled article";16// Allow explicit line breaks in the title via "\n" or YAML newlines17const docTitleHtml = (articleFM?.title ?? "Untitled article")18 .replace(/\\n/g, "<br/>")19 .replace(/\n/g, "<br/>");20const subtitle = articleFM?.subtitle ?? "";21const description = articleFM?.description ?? "";22// Accept authors as string[] or array of objects { name, url, affiliations? }23const rawAuthors = (articleFM as any)?.authors ?? [];24type Affiliation = { id: number; name: string; url?: string };25type Author = { name: string; url?: string; affiliationIndices?: number[] };26 27// Normalize affiliations from frontmatter: supports strings or objects { id?, name, url? }28const rawAffils =29 (articleFM as any)?.affiliations ?? (articleFM as any)?.affiliation ?? [];30const normalizedAffiliations: Affiliation[] = (() => {31 const seen: Map<string, number> = new Map();32 const list: Affiliation[] = [];33 const pushUnique = (name: string, url?: string) => {34 const key = `${String(name).trim()}|${url ? String(url).trim() : ""}`;35 if (seen.has(key)) return seen.get(key)!;36 const id = list.length + 1;37 list.push({38 id,39 name: String(name).trim(),40 url: url ? String(url) : undefined,41 });42 seen.set(key, id);43 return id;44 };45 const input = Array.isArray(rawAffils)46 ? rawAffils47 : rawAffils48 ? [rawAffils]49 : [];50 for (const a of input) {51 if (typeof a === "string") {52 pushUnique(a);53 } else if (a && typeof a === "object") {54 const name = a.name ?? a.label ?? a.text ?? a.affiliation ?? "";55 if (!String(name).trim()) continue;56 const url = a.url || a.link;57 // Respect provided numeric id for display stability if present and sequential; otherwise reassign58 pushUnique(String(name), url ? String(url) : undefined);59 }60 }61 return list;62})();63 64// Helper: ensure an affiliation exists and return its id65const ensureAffiliation = (val: any): number | undefined => {66 if (val == null) return undefined;67 if (typeof val === "number" && Number.isFinite(val) && val > 0) {68 return Math.floor(val);69 }70 const name =71 typeof val === "string"72 ? val73 : (val?.name ?? val?.label ?? val?.text ?? val?.affiliation);74 if (!name || !String(name).trim()) return undefined;75 const existing = normalizedAffiliations.find(76 (a) => a.name === String(name).trim(),77 );78 if (existing) return existing.id;79 const id = normalizedAffiliations.length + 1;80 normalizedAffiliations.push({81 id,82 name: String(name).trim(),83 url: val?.url || val?.link,84 });85 return id;86};87 88// Normalize authors and map affiliations -> indices (Distill-like)89const normalizedAuthors: Author[] = (90 Array.isArray(rawAuthors) ? rawAuthors : []91)92 .map((a: any) => {93 if (typeof a === "string") {94 return { name: a } as Author;95 }96 const name = String(a?.name || "").trim();97 const url = a?.url || a?.link;98 let indices: number[] | undefined = undefined;99 const raw = a?.affiliations ?? a?.affiliation ?? a?.affils;100 if (raw != null) {101 const entries = Array.isArray(raw) ? raw : [raw];102 const ids = entries103 .map(ensureAffiliation)104 .filter((x): x is number => typeof x === "number");105 const unique = Array.from(new Set(ids)).sort((x, y) => x - y);106 if (unique.length) indices = unique;107 }108 return { name, url, affiliationIndices: indices } as Author;109 })110 .filter((a: Author) => a.name && a.name.trim().length > 0);111const authorNames: string[] = normalizedAuthors.map((a) => a.name);112const published = articleFM?.published ?? undefined;113const tags = articleFM?.tags ?? [];114// Prefer seoThumbImage from frontmatter if provided115const fmOg = articleFM?.seoThumbImage as string | undefined;116const imageAbs: string =117 fmOg && fmOg.startsWith("http")118 ? fmOg119 : Astro.site120 ? new URL(fmOg ?? ogDefaultUrl, Astro.site).toString()121 : (fmOg ?? ogDefaultUrl);122 123// ---- Build citation text & BibTeX from frontmatter ----124const stripHtml = (text: string) => String(text || "").replace(/<[^>]*>/g, "");125const rawTitle = articleFM?.title ?? "Untitled article";126const titleFlat = stripHtml(String(rawTitle))127 .replace(/\\n/g, " ")128 .replace(/\n/g, " ")129 .replace(/\s+/g, " ")130 .trim();131const extractYear = (val: string | undefined): number | undefined => {132 if (!val) return undefined;133 const d = new Date(val);134 if (!Number.isNaN(d.getTime())) return d.getFullYear();135 const m = String(val).match(/(19|20)\d{2}/);136 return m ? Number(m[0]) : undefined;137};138 139const year = extractYear(published);140const citationAuthorsText = authorNames.join(", ");141const citationText = `${citationAuthorsText}${year ? ` (${year})` : ""}. "${titleFlat}".`;142 143const authorsBib = authorNames.join(" and ");144const keyAuthor = (authorNames[0] || "article")145 .split(/\s+/)146 .slice(-1)[0]147 .toLowerCase();148const keyTitle = titleFlat149 .toLowerCase()150 .replace(/[^a-z0-9]+/g, "_")151 .replace(/^_|_$/g, "")152 .slice(0, 24);153const bibKey = `${keyAuthor}${year ?? ""}_${keyTitle}`;154const doi = (ArticleMod as any)?.frontmatter?.doi155 ? String((ArticleMod as any).frontmatter.doi)156 : undefined;157const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBib}},\n ${year ? `year={${year}},\n ` : ""}${doi ? `doi={${doi}}` : ""}\n}`;158const envCollapse = false;159const tableOfContentAutoCollapse = Boolean(160 (articleFM as any)?.tableOfContentAutoCollapse ??161 (articleFM as any)?.tableOfContentsAutoCollapse ??162 envCollapse,163);164// Licence note (HTML allowed)165const licence =166 (articleFM as any)?.licence ??167 (articleFM as any)?.license ??168 (articleFM as any)?.licenseNote;169---170 171<html172 lang="en"173 data-theme="light"174 data-toc-auto-collapse={tableOfContentAutoCollapse ? "1" : "0"}175>176 <head>177 <meta charset="utf-8" />178 <meta name="viewport" content="width=device-width, initial-scale=1" />179 <Seo180 title={docTitle}181 description={description}182 authors={authorNames}183 published={published}184 tags={tags}185 image={imageAbs}186 />187 <script is:inline>188 (() => {189 try {190 const saved = localStorage.getItem("theme");191 const prefersDark =192 window.matchMedia &&193 window.matchMedia("(prefers-color-scheme: dark)").matches;194 const theme = saved || (prefersDark ? "dark" : "light");195 document.documentElement.setAttribute("data-theme", theme);196 } catch {}197 })();198 </script>199 <script type="module" src="/scripts/color-palettes.js"></script>200 201 <!-- TO MANAGE PROPERLY -->202 <script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"203 ></script>204 <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>205 <script206 src="https://cdn.jsdelivr.net/npm/medium-zoom@1.1.0/dist/medium-zoom.min.js"207 ></script>208 <script>209 // Debug and global zoom initialization210 211 function initializeZoom() {212 const zoomableImages = document.querySelectorAll(213 'img[data-zoomable="1"]',214 );215 216 if (window.mediumZoom && zoomableImages.length > 0) {217 zoomableImages.forEach((img, index) => {218 // Check if already initialized219 if (!img.classList.contains("medium-zoom-image")) {220 try {221 const instance = window.mediumZoom(img, {222 background: "rgba(0,0,0,.85)",223 margin: 24,224 scrollOffset: 0,225 });226 } catch (error) {227 console.error(228 `Global script: Error initializing zoom for image ${index}:`,229 error,230 );231 }232 } else {233 console.log(`Global script: Image ${index} already has zoom`);234 }235 });236 } else {237 console.log(238 "Global script: mediumZoom not available or no images found",239 );240 }241 }242 243 // Try to initialize immediately244 if (document.readyState === "loading") {245 document.addEventListener("DOMContentLoaded", initializeZoom);246 } else {247 initializeZoom();248 }249 250 // Also try after complete loading251 window.addEventListener("load", () => {252 setTimeout(initializeZoom, 100);253 });254 </script>255 </head>256 <body>257 <ThemeToggle />258 <Hero259 title={docTitleHtml}260 titleRaw={docTitle}261 description={subtitle}262 authors={normalizedAuthors as any}263 affiliations={normalizedAffiliations as any}264 affiliation={articleFM?.affiliation}265 published={articleFM?.published}266 doi={doi}267 pdfProOnly={articleFM?.pdfProOnly}268 />269 270 <section class="content-grid">271 <TableOfContents272 tableOfContentAutoCollapse={tableOfContentAutoCollapse}273 />274 <main>275 <Article />276 </main>277 </section>278 279 <Footer280 citationText={citationText}281 bibtex={bibtex}282 licence={licence}283 doi={doi}284 />285 286 <script>287 // Open external links in a new tab; keep internal anchors in-page288 const setExternalTargets = () => {289 const isExternal = (href) => {290 try {291 const u = new URL(href, location.href);292 return u.origin !== location.origin;293 } catch {294 return false;295 }296 };297 document.querySelectorAll("a[href]").forEach((a) => {298 const href = a.getAttribute("href");299 if (!href) return;300 if (isExternal(href)) {301 a.setAttribute("target", "_blank");302 a.setAttribute("rel", "noopener noreferrer");303 } else {304 a.removeAttribute("target");305 }306 });307 };308 if (document.readyState === "loading") {309 document.addEventListener("DOMContentLoaded", setExternalTargets, {310 once: true,311 });312 } else {313 setExternalTargets();314 }315 </script>316 317 <script>318 // Delegate copy clicks for code blocks injected by rehypeCodeCopy319 document.addEventListener("click", async (e) => {320 const target = e.target instanceof Element ? e.target : null;321 const btn = target ? target.closest(".code-copy") : null;322 if (!btn) return;323 const card = btn.closest(".code-card");324 const pre = card && card.querySelector("pre");325 if (!pre) return;326 const text = pre.textContent || "";327 try {328 await navigator.clipboard.writeText(text.trim());329 const old = btn.innerHTML;330 btn.innerHTML =331 '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>';332 setTimeout(() => (btn.innerHTML = old), 1200);333 } catch {334 btn.textContent = "Error";335 setTimeout(() => (btn.textContent = "Copy"), 1200);336 }337 });338 </script>339 </body>340</html>341 