Download script.js from pollen-robotics/Reachy_Mini_Assembly_Guide: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/spaces/pollen-robotics/Reachy_Mini_Assembly_Guide/resolve/main/script.js
- Command line
-
hf download hf://spaces/pollen-robotics/Reachy_Mini_Assembly_Guide/script.js
-
curl -L -o script.js https://huggingface.co/spaces/pollen-robotics/Reachy_Mini_Assembly_Guide/resolve/main/script.js
16.2 kB
| // Assembly Steps Data | |
| const YOUTUBE_VIDEO_ID = "WeKKdnuXca4"; | |
| const stepsData = [ | |
| { timestamp: "00:14", title: "Set all parts aside" }, | |
| { timestamp: "00:45", title: "Stick Foot Pads" }, | |
| { timestamp: "01:30", title: "Position the protective sleeve on the USB cable" }, | |
| { timestamp: "02:51", title: "Fix the USB Extension Cable" }, | |
| { timestamp: "03:23", title: "Insert the Power Board" }, | |
| { timestamp: "03:39", title: "Fix the Power Board" }, | |
| { timestamp: "04:30", title: "Connect the Battery" }, | |
| { timestamp: "05:23", title: "Connect the cables to the Power Board" }, | |
| { timestamp: "06:27", title: "Position the Bottom Assembly" }, | |
| { timestamp: "07:34", title: "Fix the Foot Assembly to the Bottom Assembly" }, | |
| { timestamp: "08:31", title: "Connect the Foot Motor" }, | |
| { timestamp: "09:15", title: "Screw the link rods onto the motor arms" }, | |
| { timestamp: "12:57", title: "Connect Motor 1 to 2" }, | |
| { timestamp: "13:23", title: "Connect Motor 2 to 3" }, | |
| { timestamp: "13:45", title: "Connect Motor 4 to 5" }, | |
| { timestamp: "14:05", title: "Connect Motor 5 to 6" }, | |
| { timestamp: "14:31", title: "Insert all motors into the Bottom Assembly" }, | |
| { timestamp: "16:13", title: "Clip the motor cables into the Bottom Assembly" }, | |
| { timestamp: "18:56", title: "Position the Tricap" }, | |
| { timestamp: "19:37", title: "Route the cables" }, | |
| { timestamp: "20:23", title: "Connect Motor 3 to 4" }, | |
| { timestamp: "21:06", title: "Route the cables" }, | |
| { timestamp: "23:28", title: "Check rotation" }, | |
| { timestamp: "23:53", title: "Screw the Tricap" }, | |
| { timestamp: "25:44", title: "Screw the Bottom Head onto the link rods" }, | |
| { timestamp: "28:58", title: "Route the cables through the Bottom Head" }, | |
| { timestamp: "30:07", title: "Route the Wi-Fi Antenna and the fan cable through the Head PCB" }, | |
| { timestamp: "30:14", title: "Connect the flexible camera cable (step for reference only)" }, | |
| { timestamp: "30:47", title: "Route the cables through the Head PCB" }, | |
| { timestamp: "31:48", title: "Screw the Head PCB" }, | |
| { timestamp: "33:00", title: "Position the Top Shell" }, | |
| { timestamp: "34:00", title: "Screw the Top Shell" }, | |
| { timestamp: "35:32", title: "Place the lenses in the Glasses Holder" }, | |
| { timestamp: "35:43", title: "Insert the Fisheye Lenses into the caps" }, | |
| { timestamp: "36:07", title: "Snap the Fisheye Lenses" }, | |
| { timestamp: "36:52", title: "Screw the Glasses Assembly onto the Front Head Shell" }, | |
| { timestamp: "37:53", title: "Attach the cases to Antenna's motors" }, | |
| { timestamp: "38:24", title: "Fix Antenna's Assembly to Back Head Shell" }, | |
| { timestamp: "39:41", title: "Connect the Antenna's motors" }, | |
| { timestamp: "40:22", title: "Slide the Back Head Assembly onto the Reachy Mini body" }, | |
| { timestamp: "40:45", title: "Screw the Back Head" }, | |
| { timestamp: "41:45", title: "Fix the Cable Holder" }, | |
| { timestamp: "42:20", title: "Connect Speaker, fan and Motor Cables" }, | |
| { timestamp: "42:53", title: "Connect Power and USB Extension Cables" }, | |
| { timestamp: "43:28", title: "Stick the Wi-Fi Antenna" }, | |
| { timestamp: "44:08", title: "Slide the Top Head Assembly onto the Back Head" }, | |
| { timestamp: "44:33", title: "Connect the Flexible Printed Cable to the Head PCB" }, | |
| { timestamp: "44:52", title: "Connect the Flexible Camera Cable on the Front Head" }, | |
| { timestamp: "45:16", title: "Fix the Front Head" }, | |
| { timestamp: "46:25", title: "Fix the Antennas" }, | |
| { timestamp: "47:08", title: "Turn on Reachy Mini" }, | |
| ]; | |
| // Parse timestamp to seconds (MM:SS format) | |
| function parseTimestamp(ts) { | |
| const parts = ts.split(':').map(Number); | |
| return parts[0] * 60 + parts[1]; | |
| } | |
| // Create assembly steps with parsed timestamps | |
| const assemblySteps = stepsData.map((step, index) => ({ | |
| id: index + 1, | |
| title: step.title, | |
| timestamp: step.timestamp, | |
| timestampSeconds: parseTimestamp(step.timestamp) | |
| })); | |
| const TOTAL_STEPS = assemblySteps.length; | |
| // Available step images | |
| const availableImages = { | |
| 1: 'assets/step1.jpg', | |
| 2: 'assets/step2.jpg', | |
| 3: 'assets/step3.jpg', | |
| 4: 'assets/step4.jpg', | |
| 5: 'assets/step5.jpg', | |
| 6: 'assets/step6.jpg', | |
| 7: 'assets/step7.jpg', | |
| 8: 'assets/step8.jpg', | |
| 9: 'assets/step9.jpg', | |
| 10: 'assets/step10.jpg', | |
| 11: 'assets/step11.jpg', | |
| 12: 'assets/step12.jpg', | |
| 13: 'assets/step13.jpg', | |
| 14: 'assets/step14.jpg', | |
| 15: 'assets/step15.jpg', | |
| 16: 'assets/step16.jpg', | |
| 17: 'assets/step17.jpg', | |
| 18: 'assets/step18.jpg', | |
| 19: 'assets/step19.jpg', | |
| 20: 'assets/step20.jpg', | |
| 21: 'assets/step21.jpg', | |
| 22: 'assets/step22.jpg', | |
| 23: 'assets/step23.jpg', | |
| 24: 'assets/step24.jpg', | |
| 25: 'assets/step25.jpg', | |
| 26: 'assets/step26.jpg', | |
| 27: 'assets/step27.jpg', | |
| 28: 'assets/step28.jpg', | |
| 29: 'assets/step29.jpg', | |
| 30: 'assets/step30.jpg', | |
| 31: 'assets/step31.jpg', | |
| 32: 'assets/step32.jpg', | |
| 33: 'assets/step33.jpg', | |
| 34: 'assets/step34.jpg', | |
| 35: 'assets/step35.jpg', | |
| 36: 'assets/step36.jpg', | |
| 37: 'assets/step37.jpg', | |
| 38: 'assets/step38.jpg', | |
| 39: 'assets/step39.jpg', | |
| 40: 'assets/step40.jpg', | |
| 41: 'assets/step41.jpg', | |
| 42: 'assets/step42.jpg', | |
| 43: 'assets/step43.jpg', | |
| 44: 'assets/step44.jpg', | |
| 45: 'assets/step45.jpg', | |
| 46: 'assets/step46.jpg', | |
| 47: 'assets/step47.jpg', | |
| 48: 'assets/step48.jpg', | |
| 49: 'assets/step49.jpg', | |
| 50: 'assets/step50.jpg', | |
| 51: 'assets/step51.jpg', | |
| }; | |
| function getStepImage(stepId) { | |
| return availableImages[stepId] || null; | |
| } | |
| // App State | |
| let currentStep = 1; | |
| let isFullscreen = false; | |
| let scale = 1; | |
| let position = { x: 0, y: 0 }; | |
| let isDragging = false; | |
| let dragStart = { x: 0, y: 0 }; | |
| // DOM Elements | |
| const stepCounterText = document.getElementById('step-counter-text'); | |
| const stepImage = document.getElementById('step-image'); | |
| const placeholder = document.getElementById('placeholder'); | |
| const placeholderNumber = document.getElementById('placeholder-number'); | |
| const imageWrapper = document.getElementById('image-wrapper'); | |
| const youtubeIframeDesktop = document.getElementById('youtube-iframe-desktop'); | |
| const youtubeIframeMobile = document.getElementById('youtube-iframe-mobile'); | |
| const prevBtn = document.getElementById('prev-btn'); | |
| const nextBtn = document.getElementById('next-btn'); | |
| const stepIndicators = document.getElementById('step-indicators'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const fullscreenBtn = document.getElementById('fullscreen-btn'); | |
| const fullscreenModal = document.getElementById('fullscreen-modal'); | |
| const closeFullscreenBtn = document.getElementById('close-fullscreen-btn'); | |
| const fullscreenStepTitle = document.getElementById('fullscreen-step-title'); | |
| const fullscreenImage = document.getElementById('fullscreen-image'); | |
| const fullscreenPlaceholder = document.getElementById('fullscreen-placeholder'); | |
| const fullscreenPlaceholderNumber = document.getElementById('fullscreen-placeholder-number'); | |
| const fullscreenImageContainer = document.getElementById('fullscreen-image-container'); | |
| const fullscreenYoutubeIframeDesktop = document.getElementById('fullscreen-youtube-iframe-desktop'); | |
| const fullscreenYoutubeIframeMobile = document.getElementById('fullscreen-youtube-iframe-mobile'); | |
| const fullscreenPrevBtn = document.getElementById('fullscreen-prev-btn'); | |
| const fullscreenNextBtn = document.getElementById('fullscreen-next-btn'); | |
| const fullscreenStepIndicators = document.getElementById('fullscreen-step-indicators'); | |
| const fullscreenProgressBar = document.getElementById('fullscreen-progress-bar'); | |
| const zoomInBtn = document.getElementById('zoom-in-btn'); | |
| const zoomOutBtn = document.getElementById('zoom-out-btn'); | |
| const zoomLevel = document.getElementById('zoom-level'); | |
| // Update YouTube embed | |
| function updateYouTubeEmbed(timestampSeconds) { | |
| const embedUrl = `https://www.youtube.com/embed/${YOUTUBE_VIDEO_ID}?start=${timestampSeconds}&rel=0&autoplay=1&mute=1`; | |
| youtubeIframeDesktop.src = embedUrl; | |
| youtubeIframeMobile.src = embedUrl; | |
| fullscreenYoutubeIframeDesktop.src = embedUrl; | |
| fullscreenYoutubeIframeMobile.src = embedUrl; | |
| } | |
| // Render step indicators with pagination | |
| function renderStepIndicators(containerId, currentStep, onClick) { | |
| const container = document.getElementById(containerId); | |
| container.innerHTML = ''; | |
| const groupStart = Math.floor((currentStep - 1) / 10) * 10 + 1; | |
| const groupEnd = Math.min(groupStart + 9, TOTAL_STEPS); | |
| const canGoPrevGroup = groupStart > 1; | |
| const canGoNextGroup = groupEnd < TOTAL_STEPS; | |
| // Previous group button | |
| if (canGoPrevGroup) { | |
| const prevGroupBtn = document.createElement('button'); | |
| prevGroupBtn.className = 'step-indicator-nav'; | |
| prevGroupBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>'; | |
| prevGroupBtn.setAttribute('aria-label', 'Previous group'); | |
| prevGroupBtn.addEventListener('click', () => onClick(groupStart - 1)); | |
| container.appendChild(prevGroupBtn); | |
| } | |
| // Step number buttons | |
| for (let i = 0; i < 10; i++) { | |
| const stepNum = groupStart + i; | |
| if (stepNum > TOTAL_STEPS) break; | |
| const isActive = stepNum === currentStep; | |
| const button = document.createElement('button'); | |
| button.className = `step-indicator-num ${isActive ? 'step-indicator-num-active' : ''}`; | |
| button.textContent = stepNum; | |
| button.setAttribute('aria-label', `Go to step ${stepNum}`); | |
| button.addEventListener('click', () => onClick(stepNum)); | |
| container.appendChild(button); | |
| } | |
| // Next group button | |
| if (canGoNextGroup) { | |
| const nextGroupBtn = document.createElement('button'); | |
| nextGroupBtn.className = 'step-indicator-nav'; | |
| nextGroupBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>'; | |
| nextGroupBtn.setAttribute('aria-label', 'Next group'); | |
| nextGroupBtn.addEventListener('click', () => onClick(groupEnd + 1)); | |
| container.appendChild(nextGroupBtn); | |
| } | |
| } | |
| // Update UI | |
| function updateUI() { | |
| const step = assemblySteps[currentStep - 1]; | |
| const imageSrc = getStepImage(step.id); | |
| // Update step counter | |
| stepCounterText.textContent = `Step ${step.id}/${TOTAL_STEPS}`; | |
| // Update image | |
| if (imageSrc) { | |
| stepImage.src = imageSrc; | |
| stepImage.alt = `Assembly step ${step.id}`; | |
| stepImage.classList.remove('hidden'); | |
| placeholder.classList.add('hidden'); | |
| } else { | |
| stepImage.classList.add('hidden'); | |
| placeholder.classList.remove('hidden'); | |
| placeholderNumber.textContent = step.id; | |
| } | |
| // Update YouTube embed | |
| updateYouTubeEmbed(step.timestampSeconds); | |
| // Update buttons | |
| prevBtn.disabled = currentStep === 1; | |
| nextBtn.disabled = currentStep === TOTAL_STEPS; | |
| // Update step indicators | |
| renderStepIndicators('step-indicators', currentStep, goToStep); | |
| // Update progress bar | |
| const progress = (currentStep / TOTAL_STEPS) * 100; | |
| progressBar.style.width = `${progress}%`; | |
| // Update fullscreen UI | |
| updateFullscreenUI(); | |
| } | |
| // Update fullscreen UI | |
| function updateFullscreenUI() { | |
| const step = assemblySteps[currentStep - 1]; | |
| const imageSrc = getStepImage(step.id); | |
| fullscreenStepTitle.textContent = `Step ${step.id}/${TOTAL_STEPS} - ${step.title}`; | |
| if (imageSrc) { | |
| fullscreenImage.src = imageSrc; | |
| fullscreenImage.alt = `Step ${step.id}`; | |
| fullscreenImage.classList.remove('hidden'); | |
| fullscreenPlaceholder.classList.add('hidden'); | |
| } else { | |
| fullscreenImage.classList.add('hidden'); | |
| fullscreenPlaceholder.classList.remove('hidden'); | |
| fullscreenPlaceholderNumber.textContent = step.id; | |
| } | |
| fullscreenPrevBtn.disabled = currentStep === 1; | |
| fullscreenNextBtn.disabled = currentStep === TOTAL_STEPS; | |
| renderStepIndicators('fullscreen-step-indicators', currentStep, goToStepFullscreen); | |
| const progress = (currentStep / TOTAL_STEPS) * 100; | |
| fullscreenProgressBar.style.width = `${progress}%`; | |
| updateZoomDisplay(); | |
| } | |
| // Navigation functions | |
| function goToPrevious() { | |
| if (currentStep > 1) { | |
| currentStep--; | |
| updateUI(); | |
| } | |
| } | |
| function goToNext() { | |
| if (currentStep < TOTAL_STEPS) { | |
| currentStep++; | |
| updateUI(); | |
| } | |
| } | |
| function goToStep(step) { | |
| if (step >= 1 && step <= TOTAL_STEPS) { | |
| currentStep = step; | |
| updateUI(); | |
| } | |
| } | |
| function goToStepFullscreen(step) { | |
| resetZoom(); | |
| goToStep(step); | |
| } | |
| function goToPreviousFullscreen() { | |
| resetZoom(); | |
| goToPrevious(); | |
| } | |
| function goToNextFullscreen() { | |
| resetZoom(); | |
| goToNext(); | |
| } | |
| // Fullscreen functions | |
| function openFullscreen() { | |
| isFullscreen = true; | |
| fullscreenModal.classList.remove('hidden'); | |
| document.body.style.overflow = 'hidden'; | |
| updateFullscreenUI(); | |
| } | |
| function closeFullscreen() { | |
| isFullscreen = false; | |
| fullscreenModal.classList.add('hidden'); | |
| document.body.style.overflow = ''; | |
| resetZoom(); | |
| } | |
| // Zoom functions | |
| function zoomIn() { | |
| scale = Math.min(scale + 0.5, 4); | |
| updateZoomDisplay(); | |
| } | |
| function zoomOut() { | |
| scale = Math.max(scale - 0.5, 0.5); | |
| updateZoomDisplay(); | |
| } | |
| function resetZoom() { | |
| scale = 1; | |
| position = { x: 0, y: 0 }; | |
| updateZoomDisplay(); | |
| } | |
| function updateZoomDisplay() { | |
| zoomLevel.textContent = `${Math.round(scale * 100)}%`; | |
| fullscreenImage.style.transform = `translate(${position.x}px, ${position.y}px) scale(${scale})`; | |
| } | |
| // Drag functions for fullscreen image | |
| function handleMouseDown(e) { | |
| if (scale > 1) { | |
| isDragging = true; | |
| dragStart = { | |
| x: e.clientX - position.x, | |
| y: e.clientY - position.y | |
| }; | |
| fullscreenImageContainer.style.cursor = 'grabbing'; | |
| } | |
| } | |
| function handleMouseMove(e) { | |
| if (isDragging && scale > 1) { | |
| position = { | |
| x: e.clientX - dragStart.x, | |
| y: e.clientY - dragStart.y | |
| }; | |
| updateZoomDisplay(); | |
| } | |
| } | |
| function handleMouseUp() { | |
| isDragging = false; | |
| fullscreenImageContainer.style.cursor = 'grab'; | |
| } | |
| function handleWheel(e) { | |
| e.preventDefault(); | |
| const delta = e.deltaY > 0 ? -0.2 : 0.2; | |
| scale = Math.max(0.5, Math.min(4, scale + delta)); | |
| updateZoomDisplay(); | |
| } | |
| // Event Listeners | |
| prevBtn.addEventListener('click', goToPrevious); | |
| nextBtn.addEventListener('click', goToNext); | |
| fullscreenBtn.addEventListener('click', openFullscreen); | |
| stepImage.addEventListener('click', openFullscreen); | |
| closeFullscreenBtn.addEventListener('click', closeFullscreen); | |
| fullscreenPrevBtn.addEventListener('click', goToPreviousFullscreen); | |
| fullscreenNextBtn.addEventListener('click', goToNextFullscreen); | |
| zoomInBtn.addEventListener('click', zoomIn); | |
| zoomOutBtn.addEventListener('click', zoomOut); | |
| fullscreenImageContainer.addEventListener('mousedown', handleMouseDown); | |
| fullscreenImageContainer.addEventListener('mousemove', handleMouseMove); | |
| fullscreenImageContainer.addEventListener('mouseup', handleMouseUp); | |
| fullscreenImageContainer.addEventListener('mouseleave', handleMouseUp); | |
| fullscreenImageContainer.addEventListener('wheel', handleWheel, { passive: false }); | |
| // Keyboard navigation | |
| document.addEventListener('keydown', (e) => { | |
| if (isFullscreen) { | |
| if (e.key === 'Escape') closeFullscreen(); | |
| if (e.key === '+' || e.key === '=') zoomIn(); | |
| if (e.key === '-') zoomOut(); | |
| if (e.key === '0') resetZoom(); | |
| if (e.key === 'ArrowLeft' && currentStep > 1) goToPreviousFullscreen(); | |
| if (e.key === 'ArrowRight' && currentStep < TOTAL_STEPS) goToNextFullscreen(); | |
| } else { | |
| if (e.key === 'ArrowRight') goToNext(); | |
| if (e.key === 'ArrowLeft') goToPrevious(); | |
| } | |
| }); | |
| // Initialize | |
| updateUI(); | |