diff --git a/src/components/Terminal.css b/src/components/Terminal.css index 29c3d54..d5df426 100644 --- a/src/components/Terminal.css +++ b/src/components/Terminal.css @@ -33,6 +33,45 @@ color: var(--muted); flex: 1; } +.term-tabs { + display: flex; + align-items: center; + gap: 6px; + flex: 1; +} +.term-tab { + position: relative; + min-width: 96px; + font-family: var(--mono); + font-size: 13.5px; + letter-spacing: 0.04em; + color: var(--muted); + background: transparent; + border: 1px solid var(--hair-2); + border-radius: 8px; + padding: 9px 20px; + cursor: pointer; + transition: color 0.2s, border-color 0.2s, background 0.2s; +} +.term-tab:hover { + color: var(--text-2); +} +.term-tab.active { + color: var(--text); + border-color: var(--accent-line); + background: rgba(255, 255, 255, 0.05); +} +.term-tab-curl { + min-width: 144px; + text-align: left; +} +.term-tab .soon { + position: absolute; + top: 4px; + right: 8px; + font-size: 7px; + padding: 1px 5px; +} .term-copy { font-family: var(--mono); font-size: 12px; @@ -52,18 +91,48 @@ padding: 22px 20px 26px; font-family: var(--mono); font-size: clamp(12px, 1.4vw, 14.5px); - line-height: 1.9; + line-height: 1.7; + height: 460px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--hair-2) transparent; +} +.term-body::-webkit-scrollbar { + width: 8px; +} +.term-body::-webkit-scrollbar-track { + background: transparent; +} +.term-body::-webkit-scrollbar-thumb { + background-color: var(--hair-2); + background-clip: padding-box; + border: 2px solid transparent; + border-radius: 8px; +} +.term-body::-webkit-scrollbar-thumb:hover { + background-color: var(--text-2); +} +.term-body > * + * { + margin-top: 3px; } .term-cmd { display: flex; gap: 10px; align-items: flex-start; + margin-top: 10px; +} +.term-cmd:first-child { + margin-top: 0; } .term-prompt { color: var(--accent); user-select: none; } .term-cmd code { + /* same UA-stylesheet override as .term-pre: `code { font-family: monospace }` + beats the inherited --mono unless restated here. */ + font-family: inherit; + font-size: inherit; background: none; color: var(--text); padding: 0; @@ -83,10 +152,6 @@ opacity: 0; } } -.term-out { - margin-top: 6px; - min-height: 116px; -} .term-line { color: var(--text-2); } @@ -94,6 +159,49 @@ color: var(--green); } +.term-pre { + margin: 10px 0; + color: var(--text-2); + /* the UA stylesheet sets `pre { font-family: monospace }`, which beats + inheriting --mono from .term-body — restate it explicitly here. */ + font-family: var(--mono); + font-size: inherit; + white-space: pre; + overflow-x: auto; + scrollbar-width: thin; + scrollbar-color: var(--hair-2) transparent; +} +.term-pre::-webkit-scrollbar { + height: 8px; +} +.term-pre::-webkit-scrollbar-track { + background: transparent; +} +.term-pre::-webkit-scrollbar-thumb { + background-color: var(--hair-2); + background-clip: padding-box; + border: 2px solid transparent; + border-radius: 8px; +} +.term-pre::-webkit-scrollbar-thumb:hover { + background-color: var(--text-2); +} +.term-banner, +.term-doctor { + /* Tighter than the 1.7 used for prose-like output, so box-drawing borders sit flush. */ + line-height: 1.35; +} +.term-banner { + /* The banner is 127 real terminal columns — at the shared 14.5px body size it's + wider than the terminal box. 10.2px is the largest size that still fits the + 900px-wide box without a horizontal scrollbar (measured empirically); the + inherited overflow-x: auto from .term-pre stays as a fallback on narrow viewports. */ + font-size: 10.2px; +} +.term-ok { + color: var(--green); +} + .term-note { margin-top: 20px; font-family: var(--mono); diff --git a/src/components/Terminal.jsx b/src/components/Terminal.jsx index 318ec5c..99435e5 100644 --- a/src/components/Terminal.jsx +++ b/src/components/Terminal.jsx @@ -3,59 +3,242 @@ import { motion, useInView, useReducedMotion } from "framer-motion"; import { useReveal } from "../hooks/useReveal.js"; import "./Terminal.css"; -const CMD = "curl -fsSL https://raw.githubusercontent.com/OpenRAL/openral/master/scripts/install.sh | sh"; -const OUTPUT = [ - { t: "▸ fetching openral install script…", k: "run" }, - { t: "▸ resolving ROS 2 + Python deps…", k: "run" }, - { t: "▸ installing harness · L0–L7…", k: "run" }, - { t: "✓ openral ready — run `openral doctor` to verify", k: "ok" }, +// Exact `console.export_text()` output of openral_cli.main.render_banner("0.1.0", width=127) +// and of the `openral doctor` Rich table — captured from the real CLI so the site shows the +// literal terminal output, not a re-styled approximation. +const BANNER_TEXT = `╭─ OPENRAL v0.1.0 ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ █ █ ██████╗ ██████╗ ███████╗███╗ ██╗██████╗ █████╗ ██╗ │ Discord discord.gg/3paXT2bVyB │ +│ ██▄ ▄██ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔══██╗██╔══██╗██║ │ GitHub github.com/OpenRAL/openral │ +│ ████▄▄ ▄▄████ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║██████╔╝███████║██║ │ Hugging Face huggingface.co/OpenRAL │ +│ ▀██████ ██████▀ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██╔══██╗██╔══██║██║ │ Website openral.com │ +│ ▀███████▀ ╚██████╔╝██║ ███████╗██║ ╚████║██║ ██║██║ ██║███████╗ │ ──────────────────────────────────────── │ +│ ▀ ▀███▀ ▀ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ │ doctor diagnose your host setup │ +│ │ rskill search find installable skills │ +│ OpenRAL — Open Robot Agentic Layer (harness) for embodied AI │ help list every command │ +│ fast policies · slow reasoning · rewards · perception · control │ exit leave the repl · Ctrl-D │ +│ │ +╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯`; + +const DOCTOR_TEXT = ` openral doctor +┏━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ check ┃ status ┃ details ┃ +┡━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ +│ Python │ ok │ 3.12.4 │ +│ Platform │ ok │ Ubuntu 24.04 (noble) x86_64 │ +│ ROS 2 distro │ ok │ jazzy │ +│ colcon │ ok │ 0.20.0 │ +│ GPU │ ok │ NVIDIA RTX 4090 · CUDA 12.6 │ +│ USB devices │ ok │ so101, realsense │ +└──────────────┴────────┴─────────────────────────────┘`; + +// Real openral ROS 2 package names (~/openral), one per product pillar — observability, +// safety, perception, reasoning, skill execution — interleaved like colcon's parallel build. +const CLI_CLONE_CMD = "git clone git@github.com:OpenRAL/openral.git"; +const CLI_SCRIPT = [ + { cmd: CLI_CLONE_CMD }, + { cmd: "just quickstart" }, + { + out: [ + { t: "Starting >>> opentelemetry_cpp_vendor" }, + { t: "Starting >>> openral_safety_kernel" }, + { t: "Finished <<< opentelemetry_cpp_vendor [17.9s]", k: "ok" }, + { t: "Starting >>> openral_perception_ros" }, + { t: "Finished <<< openral_safety_kernel [9.2s]", k: "ok" }, + { t: "Starting >>> openral_reasoner_ros" }, + { t: "Finished <<< openral_perception_ros [14.1s]", k: "ok" }, + { t: "Starting >>> openral_rskill_ros" }, + { t: "Finished <<< openral_reasoner_ros [11.6s]", k: "ok" }, + { t: "Finished <<< openral_rskill_ros [20.9s]", k: "ok" }, + ], + }, + { banner: true }, + { cmd: "doctor" }, + { table: true }, ]; +const CLI_COPY = `${CLI_CLONE_CMD} && cd openral && just quickstart`; -export default function Terminal() { - const reveal = useReveal(); - const reduce = useReducedMotion(); - const ref = useRef(null); - const inView = useInView(ref, { once: true, amount: 0.4 }); +// Tier-0 (ADR-0021): installs uv + CPython 3.12 + openral-cli only — no ROS 2, +// no colcon build, no REPL launch — so no Starting/Finished lines here, and the +// ending below is the real "next steps" text from scripts/install.sh, condensed. +const CURL_CMD = "curl -fsSL https://raw.githubusercontent.com/OpenRAL/openral/master/scripts/install.sh | sh"; +const CURL_SCRIPT = [ + { cmd: CURL_CMD }, + { + out: [ + { t: "▸ installing uv + CPython 3.12…" }, + { t: "▸ installing openral-cli (uv tool install)…" }, + { t: "✓ openral installed: ~/.local/bin/openral", k: "ok" }, + { t: "==> Tier-0 install complete.", k: "ok" }, + { t: "openral doctor — diagnose the host (Python, OS, GPU, USB)" }, + { t: "openral install ros — ROS 2 + libusb + udev (sudo, opt-in)" }, + ], + }, +]; - const [typed, setTyped] = useState(reduce ? CMD : ""); - const [lines, setLines] = useState(reduce ? OUTPUT.length : 0); - const [copied, setCopied] = useState(false); +function flattenReduced(script) { + return script.flatMap((b) => { + if (b.cmd) return [{ kind: "cmd", text: b.cmd }]; + if (b.out) return b.out.map((l) => ({ kind: "out", ...l })); + if (b.banner) return [{ kind: "banner" }]; + return [{ kind: "table" }]; + }); +} + +function useScriptRunner(script, active, reduce) { + const [rows, setRows] = useState([]); + const [typing, setTyping] = useState(null); useEffect(() => { - if (!inView || reduce) return; + if (reduce) { + setRows(flattenReduced(script)); + return; + } + if (!active) return; let cancelled = false; const timers = []; + const after = (fn, ms) => timers.push(setTimeout(() => !cancelled && fn(), ms)); - const run = () => { - setTyped(""); - setLines(0); - let i = 0; - const type = () => { - if (cancelled) return; - i += 1; - setTyped(CMD.slice(0, i)); - if (i < CMD.length) { - timers.push(setTimeout(type, 26)); - } else { - OUTPUT.forEach((_, idx) => - timers.push(setTimeout(() => !cancelled && setLines(idx + 1), 420 * (idx + 1))) - ); - timers.push(setTimeout(run, 420 * OUTPUT.length + 4200)); // loop - } - }; - timers.push(setTimeout(type, 500)); + const step = (i) => { + if (cancelled) return; + if (i >= script.length) { + after(() => { + setRows([]); + step(0); + }, 3600); + return; + } + const block = script[i]; + if (block.cmd) { + let c = 0; + const type = () => { + if (cancelled) return; + c += 1; + setTyping(block.cmd.slice(0, c)); + if (c < block.cmd.length) { + after(type, 22); + } else { + after(() => { + setRows((r) => [...r, { kind: "cmd", text: block.cmd }]); + setTyping(null); + step(i + 1); + }, 300); + } + }; + after(type, 300); + } else if (block.out) { + let li = 0; + const reveal = () => { + if (cancelled) return; + const item = block.out[li]; + setRows((r) => [...r, { kind: "out", ...item }]); + li += 1; + if (li < block.out.length) after(reveal, 260); + else after(() => step(i + 1), 500); + }; + after(reveal, 220); + } else { + after(() => { + setRows((r) => [...r, { kind: block.banner ? "banner" : "table" }]); + step(i + 1); + }, 400); + } }; - run(); + step(0); return () => { cancelled = true; timers.forEach(clearTimeout); }; - }, [inView, reduce]); + }, [script, active, reduce]); + + return { rows, typing }; +} + +// Colors the literal "ok" status cells without disturbing column alignment — +// a adds no character width, so the monospace grid stays intact. +function colorizeOk(line, i) { + return ( +
+ {line.split(/(ok)/g).map((chunk, j) => + chunk === "ok" ? ( + + ok + + ) : ( + chunk + ) + )} +
+ ); +} + +function Banner() { + return
{BANNER_TEXT}
; +} + +function DoctorTable() { + return
{DOCTOR_TEXT.split("\n").map(colorizeOk)}
; +} + +function ScriptPane({ script, active, reduce }) { + const { rows, typing } = useScriptRunner(script, active, reduce); + const bodyRef = useRef(null); + + useEffect(() => { + const el = bodyRef.current; + if (el) el.scrollTop = el.scrollHeight; + }, [rows, typing]); + + return ( +
+ {rows.map((r, i) => { + if (r.kind === "cmd") { + return ( +
+ $ + {r.text} +
+ ); + } + if (r.kind === "banner") return ; + if (r.kind === "table") return ; + return ( + + {r.t} + + ); + })} + {typing !== null && ( +
+ $ + + {typing} + {!reduce && } + +
+ )} +
+ ); +} + +export default function Terminal() { + const reveal = useReveal(); + const reduce = useReducedMotion(); + const ref = useRef(null); + const inView = useInView(ref, { once: true, amount: 0.4 }); + const [tab, setTab] = useState("cli"); + const [copied, setCopied] = useState(false); const copy = async () => { try { - await navigator.clipboard.writeText(CMD); + await navigator.clipboard.writeText(tab === "cli" ? CLI_COPY : CURL_CMD); setCopied(true); setTimeout(() => setCopied(false), 1800); } catch { @@ -83,33 +266,32 @@ export default function Terminal() {
- openral · install +
+ + +
-
-
- $ - - {typed} - {!reduce && typed.length < CMD.length && } - -
-
- {OUTPUT.slice(0, lines).map((l, i) => ( - - {l.t} - - ))} -
-
+ {tab === "cli" ? ( + + ) : ( + + )}

Prerequisites: ROS 2 Jazzy · Python 3.12+ · a CUDA GPU for VLA inference. Full guide in the{" "}