apirrone
fix syntax highlighting
b05a944
raw
history blame
21.4 kB
import { useState, useMemo } from 'react';
import {
Box,
Container,
Typography,
TextField,
InputAdornment,
Accordion,
AccordionSummary,
AccordionDetails,
Chip,
Stack,
IconButton,
Fade,
Button,
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import CloseIcon from '@mui/icons-material/Close';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css';
import Layout from '../components/Layout';
import PageHero from '../components/PageHero';
// FAQ Data
const faqSections = [
{
id: 'getting-started',
title: 'Getting Started',
items: [
{
question: 'How do I connect to Reachy Mini from Python?',
answer: `To control Reachy Mini, you mainly use the ReachyMini class from the reachy_mini package:
\`\`\`python
from reachy_mini import ReachyMini
with ReachyMini() as mini:
# your code here
...
\`\`\`
This connects to the Reachy Mini daemon and initializes motors and sensors.`,
tags: ['SDK', 'Python'],
},
{
question: 'Do I need to start the daemon manually?',
answer: `Yes. All examples assume you have already started the Reachy Mini daemon:
- Either via command line: \`reachy-mini-daemon\`
- Or via Python: \`reachy_mini.daemon.app.main\``,
tags: ['SDK'],
},
{
question: 'How long does assembly usually take?',
answer: 'Most testers report 1.5–2 hours, with some up to 4 hours depending on experience.',
tags: ['Hardware'],
},
{
question: "The dashboard at http://localhost:8000 doesn't work — what should I check?",
answer: `Typical checks:
1. You are using a proper Python virtual environment (.venv)
2. You installed/updated the Reachy Mini SDK: \`pip install -U reachy-mini\`
3. The daemon is running`,
tags: ['SDK'],
},
],
},
{
id: 'movement',
title: 'Moving the Robot',
items: [
{
question: "How do I move Reachy Mini's head to a specific pose?",
answer: `Use goto_target with a pose created by create_head_pose:
\`\`\`python
from reachy_mini import ReachyMini
from reachy_mini.utils import create_head_pose
with ReachyMini() as mini:
mini.goto_target(head=create_head_pose(y=-10, mm=True))
\`\`\``,
tags: ['Movement', 'SDK'],
},
{
question: "What's the difference between goto_target and set_target?",
answer: `**goto_target:** Interpolates motion over a duration (default 0.5s). Supports different interpolation methods (linear, minjerk, ease, cartoon). Ideal for smooth, timed motions.
**set_target:** Sets the target immediately, without interpolation. Suited for high-frequency control (e.g. sinusoidal trajectories, teleoperation).`,
tags: ['Movement', 'SDK'],
},
{
question: 'How do I move head, body, and antennas at the same time?',
answer: `Use goto_target with multiple named arguments:
\`\`\`python
mini.goto_target(
head=create_head_pose(y=-10, mm=True),
antennas=np.deg2rad([45, 45]),
duration=2.0,
body_yaw=np.deg2rad(30),
)
\`\`\``,
tags: ['Movement'],
},
],
},
{
id: 'apps',
title: 'Writing & Sharing Apps',
items: [
{
question: 'How do I write a Reachy Mini app?',
answer: `Inherit from ReachyMiniApp and implement run:
\`\`\`python
from reachy_mini.apps.app import ReachyMiniApp
from reachy_mini import ReachyMini
class MyApp(ReachyMiniApp):
def run(self, reachy_mini: ReachyMini, stop_event):
# your app logic
...
\`\`\``,
tags: ['SDK', 'Python'],
},
{
question: 'How can I generate a new app template quickly?',
answer: `Use the app template generator:
\`reachy-mini-make-app my_app_name\`
This creates a complete project structure with pyproject.toml, README.md, and the main app file.`,
tags: ['SDK'],
},
],
},
{
id: 'hardware',
title: 'Hardware & Motion Limits',
items: [
{
question: 'What are the safety limits of the head and body?',
answer: `Physical & software limits include:
- Body yaw: [-180°, 180°]
- Head pitch/roll: [-40°, 40°]
- Head yaw: [-180°, 180°]
- Difference (body_yaw - head_yaw): [-65°, 65°]
If you command a pose outside these limits, Reachy Mini will automatically clamp to the nearest safe pose.`,
tags: ['Hardware'],
},
],
},
{
id: 'sensors',
title: 'Sensors & Media',
items: [
{
question: 'How do I grab camera frames from Reachy Mini?',
answer: `Use the media object:
\`\`\`python
with ReachyMini() as mini:
frame = mini.media.get_frame()
# frame is a numpy array
\`\`\``,
tags: ['Vision', 'SDK'],
},
{
question: 'How do I access microphone audio samples?',
answer: `\`\`\`python
with ReachyMini() as mini:
sample = mini.media.get_audio_sample()
\`\`\``,
tags: ['Audio'],
},
],
},
{
id: 'motors',
title: 'Motors & Compliancy',
items: [
{
question: 'How do I enable, disable, or make motors compliant?',
answer: `Three main methods:
1. **enable_motors**: Powers motors ON. Robot holds position.
2. **disable_motors**: Powers motors OFF. Robot is limp.
3. **make_motors_compliant**: Motors ON but soft. Good for teaching-by-demonstration.`,
tags: ['Hardware', 'SDK'],
},
],
},
];
// Tag color - all primary
const tagColor = { bg: 'rgba(255, 149, 0, 0.1)', color: '#FF9500', border: 'rgba(255, 149, 0, 0.25)' };
// Extract all unique tags from FAQ data
const allTags = [...new Set(faqSections.flatMap((s) => s.items.flatMap((i) => i.tags)))].sort();
export default function FAQ() {
const [searchQuery, setSearchQuery] = useState('');
const [selectedTag, setSelectedTag] = useState(null);
// Filter FAQ items based on search and tag
const filteredSections = useMemo(() => {
const query = searchQuery.toLowerCase().trim();
return faqSections
.map((section) => ({
...section,
items: section.items.filter((item) => {
// Tag filter
if (selectedTag && !item.tags.includes(selectedTag)) {
return false;
}
// Search filter
if (query) {
return (
item.question.toLowerCase().includes(query) ||
item.answer.toLowerCase().includes(query) ||
item.tags.some((tag) => tag.toLowerCase().includes(query))
);
}
return true;
}),
}))
.filter((section) => section.items.length > 0);
}, [searchQuery, selectedTag]);
const totalResults = filteredSections.reduce((acc, section) => acc + section.items.length, 0);
const totalItems = faqSections.reduce((acc, section) => acc + section.items.length, 0);
const hasActiveFilter = searchQuery.trim() || selectedTag;
const handleTagClick = (tag) => {
setSelectedTag((prev) => (prev === tag ? null : tag));
};
const clearFilters = () => {
setSearchQuery('');
setSelectedTag(null);
};
return (
<Layout transparentHeader>
<PageHero
eyebrow="Help Center"
title="Frequently Asked Questions"
subtitle="Everything you need to know about Reachy Mini — from setup to advanced programming."
accentColor="#8b5cf6"
stickers={[
{ src: '/assets/reachies/explorer.png', size: 240, top: 20, right: 120, rotation: 8 },
{ src: '/assets/reachies/magician.png', size: 200, bottom: 0, left: 140, rotation: -10 },
]}
primitives={[
{ type: 'ring', color: '#8b5cf6', size: 80, top: 80, left: 200 },
{ type: 'squareOutline', color: '#ec4899', size: 60, bottom: 50, right: 180, rotation: 15 },
]}
/>
{/* Search + HuggingChat CTA */}
<Box
sx={{
position: 'sticky',
top: 64,
zIndex: 100,
backgroundColor: 'background.default',
py: 3,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Container maxWidth="md">
{/* Search + AI Button row */}
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems="stretch">
<TextField
fullWidth
placeholder="Search questions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon color="action" />
</InputAdornment>
),
endAdornment: searchQuery && (
<InputAdornment position="end">
<IconButton size="small" onClick={() => setSearchQuery('')} edge="end">
<CloseIcon fontSize="small" />
</IconButton>
</InputAdornment>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
},
}}
/>
{/* HuggingChat CTA Button */}
<Button
variant="outlined"
color="primary"
href="https://huggingface.co/chat/"
target="_blank"
startIcon={<Box sx={{ fontSize: 18 }}>🤗</Box>}
endIcon={<OpenInNewIcon sx={{ fontSize: 16 }} />}
sx={{
flexShrink: 0,
px: 3,
fontWeight: 600,
fontSize: 14,
whiteSpace: 'nowrap',
borderRadius: 3,
}}
>
Ask AI
</Button>
</Stack>
{/* Tag filters */}
<Stack
direction="row"
spacing={1}
flexWrap="wrap"
useFlexGap
sx={{ mt: 2 }}
>
{allTags.map((tag) => (
<Chip
key={tag}
label={tag}
onClick={() => handleTagClick(tag)}
sx={{
cursor: 'pointer',
fontWeight: 600,
fontSize: 12,
transition: 'all 0.2s ease',
backgroundColor:
selectedTag === tag
? tagColor.color
: tagColor.bg,
color:
selectedTag === tag
? 'white'
: tagColor.color,
border: `1px solid ${
selectedTag === tag
? tagColor.color
: tagColor.border
}`,
'&:hover': {
backgroundColor:
selectedTag === tag
? tagColor.color
: tagColor.bg,
transform: 'translateY(-1px)',
},
}}
/>
))}
</Stack>
{/* Filter status */}
<Fade in={hasActiveFilter}>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ mt: 1.5 }}
>
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2" color="text.secondary">
{totalResults} of {totalItems} question{totalItems !== 1 ? 's' : ''}
</Typography>
{selectedTag && (
<Chip
label={selectedTag}
size="small"
onDelete={() => setSelectedTag(null)}
sx={{
fontSize: 11,
height: 24,
backgroundColor: tagColor.bg,
color: tagColor.color,
fontWeight: 600,
'& .MuiChip-deleteIcon': {
color: tagColor.color,
'&:hover': {
color: tagColor.color,
},
},
}}
/>
)}
</Stack>
<Typography
variant="body2"
onClick={clearFilters}
sx={{
color: 'primary.main',
cursor: 'pointer',
fontWeight: 500,
'&:hover': { textDecoration: 'underline' },
}}
>
Clear all
</Typography>
</Stack>
</Fade>
</Container>
</Box>
{/* FAQ Content */}
<Container maxWidth="md" sx={{ py: 6 }}>
{filteredSections.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 10 }}>
<Typography sx={{ fontSize: 56, mb: 2, opacity: 0.4 }}>🔍</Typography>
<Typography variant="h5" sx={{ mb: 1 }}>
No results found
</Typography>
<Typography color="text.secondary" sx={{ mb: 3 }}>
Try adjusting your search or filters
</Typography>
<Chip
label="Clear filters"
onClick={clearFilters}
sx={{
cursor: 'pointer',
fontWeight: 600,
}}
/>
</Box>
) : (
filteredSections.map((section) => (
<Box key={section.id} sx={{ mb: 6 }}>
{/* Section header */}
<Typography variant="h4" sx={{ mb: 3 }}>{section.title}</Typography>
{/* FAQ items */}
{section.items.map((item, index) => (
<Accordion
key={index}
sx={{
mb: 1.5,
transition: 'all 0.2s ease',
'&:hover': {
borderColor: 'rgba(0,0,0,0.15)',
},
}}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box>
<Typography fontWeight={600} sx={{ mb: 1 }}>
{item.question}
</Typography>
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
{item.tags.map((tag) => (
<Chip
key={tag}
label={tag}
size="small"
onClick={(e) => {
e.stopPropagation();
handleTagClick(tag);
}}
sx={{
fontSize: 11,
height: 22,
cursor: 'pointer',
backgroundColor: tagColor.bg,
color: tagColor.color,
border: `1px solid ${
selectedTag === tag
? tagColor.color
: 'transparent'
}`,
fontWeight: 600,
transition: 'all 0.15s ease',
'&:hover': {
backgroundColor: tagColor.bg,
transform: 'scale(1.05)',
},
}}
/>
))}
</Stack>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box
sx={{
'& pre': {
m: 0,
backgroundColor: 'rgba(0,0,0,0.04)',
border: '1px solid rgba(0,0,0,0.08)',
borderRadius: 2,
p: 2,
overflowX: 'auto',
},
'& code': {
fontFamily: 'monospace',
fontSize: 13,
lineHeight: 1.7,
},
'& code:not(pre code)': {
backgroundColor: 'rgba(0,0,0,0.06)',
px: 1,
py: 0.25,
borderRadius: 1,
display: 'inline-block',
},
'& ul': {
pl: 3,
mb: 1.5,
color: 'text.secondary',
},
'& li': {
mb: 0.5,
lineHeight: 1.7,
},
}}
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight]}
components={{
p: ({ children }) => (
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.7, mb: 1.5 }}>
{children}
</Typography>
),
pre: ({ children }) => <Box component="pre">{children}</Box>,
code: ({ inline, className, children, ...props }) =>
inline ? (
<Box component="code" {...props}>
{children}
</Box>
) : (
<Box component="code" className={className} {...props}>
{children}
</Box>
),
a: ({ children, ...props }) => (
<Box
component="a"
{...props}
sx={{ color: 'primary.main', fontWeight: 600, textDecoration: 'none' }}
target="_blank"
rel="noreferrer"
>
{children}
</Box>
),
ul: ({ children }) => <Box component="ul">{children}</Box>,
li: ({ children }) => <Box component="li">{children}</Box>,
}}
>
{item.answer}
</ReactMarkdown>
</Box>
</AccordionDetails>
</Accordion>
))}
</Box>
))
)}
</Container>
{/* Still have questions? - Discord CTA */}
<Box sx={{ py: 8 }}>
<Container maxWidth="md">
<Box
component="a"
href="https://discord.gg/2bAhWfXme9"
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'flex-start', sm: 'center' },
justifyContent: 'space-between',
gap: 3,
p: { xs: 4, md: 5 },
borderRadius: 4,
background: '#0f0f1a',
border: '1px solid rgba(255,255,255,0.08)',
textDecoration: 'none',
transition: 'all 0.3s ease',
'&:hover': {
borderColor: 'rgba(255,255,255,0.2)',
},
}}
>
<Stack direction="row" spacing={3} alignItems="center">
<Box
component="img"
src="/assets/discord-logo.svg"
alt="Discord"
sx={{ width: 48, height: 48, opacity: 0.9 }}
/>
<Box>
<Typography variant="h5" sx={{ color: 'white', fontWeight: 600, mb: 0.5 }}>
Need more help?
</Typography>
<Typography sx={{ color: 'rgba(255,255,255,0.5)', fontSize: 14 }}>
Join our Discord to chat with 500+ makers and the Pollen team.
</Typography>
</Box>
</Stack>
<Typography
sx={{
color: 'rgba(255,255,255,0.6)',
fontSize: 14,
fontWeight: 500,
whiteSpace: 'nowrap',
}}
>
Join Discord →
</Typography>
</Box>
</Container>
</Box>
</Layout>
);
}