diff --git a/src/components/FounderBee.astro b/src/components/FounderBee.astro new file mode 100644 index 0000000..dbca59d --- /dev/null +++ b/src/components/FounderBee.astro @@ -0,0 +1,101 @@ + + + diff --git a/src/components/HeroFigure.tsx b/src/components/HeroFigure.tsx index 65560b7..21ae5fc 100644 --- a/src/components/HeroFigure.tsx +++ b/src/components/HeroFigure.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState, useEffect, useMemo, useRef } from 'react'; +import SystemsFlow from './SystemsFlow'; +import { useState, useEffect, useMemo, useRef, type ComponentType } from 'react'; function useAnimationFrame(active: boolean) { const [t, setT] = useState(0); @@ -58,6 +59,10 @@ export function AttentionFig({ t }: { t: number }) { ]; const cell = 22; const size = N * cell; + // Build the causal triangle in under two seconds, then scan the completed rows. + const reveal = t / 0.14; + const scan = t < 1.96 ? reveal : ((t - 1.96) % 2.8) / 0.2; + const activeRow = Math.min(N - 1, Math.floor(scan)); return ( {tok} @@ -95,8 +101,9 @@ export function AttentionFig({ t }: { t: number }) { ))} {pattern.map((row, i) => row.map((v, j) => { - const phase = Math.sin(t * 1.2 + i * 0.3 + j * 0.15) * 0.5 + 0.5; - const opacity = v * (0.55 + 0.45 * phase); + const entrance = Math.max(0, Math.min(1, (reveal - i) * 2)); + const highlight = Math.max(0, 1 - Math.abs(i - scan)); + const opacity = entrance * Math.min(1, v * 0.65 + (v > 0 ? highlight * 0.5 : 0)); return ( margin.l + (i / 79) * innerW; const sy = (y: number) => margin.t + ((maxY - y) / (maxY - minY)) * innerH; - const progress = Math.min(1, (t % 12) / 8); + const progress = Math.min(1, t / 5); const visibleN = Math.floor(80 * progress); const linePath = (arr: number[]) => arr @@ -292,15 +310,15 @@ function LossFig({ t }: { t: number }) { /> - ours (FP4) + FP4 example - FP8 baseline + FP8 example - BF16 ref. + BF16 example ; + active: boolean; + reducedMotion: boolean; +}) { + const t = useAnimationFrame(active); + return ; +} - const [idx, setIdx] = useState(0); +export default function HeroFigure() { + const [{ idx, previous }, setScene] = useState<{ idx: number; previous: number | null }>({ + idx: 0, + previous: null, + }); const rootRef = useRef(null); - const [reducedMotion] = useState( - () => - typeof window !== 'undefined' && - window.matchMedia('(prefers-reduced-motion: reduce)').matches, - ); + const [reducedMotion, setReducedMotion] = useState(true); + const [paused, setPaused] = useState(false); + const [hintOpen, setHintOpen] = useState(false); + const [touchControls, setTouchControls] = useState(false); const [pageVisible, setPageVisible] = useState(true); const [onScreen, setOnScreen] = useState(true); useEffect(() => { - const onVisibility = () => setPageVisible(!document.hidden); - onVisibility(); - document.addEventListener('visibilitychange', onVisibility); - return () => document.removeEventListener('visibilitychange', onVisibility); + const query = window.matchMedia('(prefers-reduced-motion: reduce)'); + const update = () => setReducedMotion(query.matches); + update(); + query.addEventListener('change', update); + return () => query.removeEventListener('change', update); + }, []); + useEffect(() => { + const update = () => setPageVisible(!document.hidden); + update(); + document.addEventListener('visibilitychange', update); + return () => document.removeEventListener('visibilitychange', update); }, []); - useEffect(() => { - const el = rootRef.current; - if (!el) return; + if (!rootRef.current) return; const observer = new IntersectionObserver(([entry]) => setOnScreen(entry.isIntersecting)); - observer.observe(el); + observer.observe(rootRef.current); return () => observer.disconnect(); }, []); - - const active = !reducedMotion && pageVisible && onScreen; - + const active = !reducedMotion && !paused && !hintOpen && pageVisible && onScreen; useEffect(() => { if (!active) return; - const cycleMs = 9000; - const interval = setInterval(() => setIdx((i) => (i + 1) % FIGS.length), cycleMs); + const interval = setInterval( + () => setScene(({ idx }) => ({ idx: (idx + 1) % FIGS.length, previous: idx })), + 6000, + ); return () => clearInterval(interval); - }, [active, FIGS.length]); - - const t = useAnimationFrame(active); - const current = FIGS[idx]; - const Comp = current.Comp; - + }, [active, idx]); + useEffect(() => { + if (previous === null) return; + const timeout = setTimeout(() => setScene((scene) => ({ ...scene, previous: null })), 750); + return () => clearTimeout(timeout); + }, [previous]); + useEffect(() => { + if (!touchControls) return; + const timer = setTimeout(() => setTouchControls(false), 5000); + return () => clearTimeout(timer); + }, [touchControls]); + const moveScene = (direction: number) => { + setHintOpen(false); + setScene(({ idx }) => ({ idx: (idx + direction + FIGS.length) % FIGS.length, previous: idx })); + }; return ( -
-
-
- {FIGS.map((f, i) => ( - - ))} -
-
-
-
- -
-
-
- - FIG. {idx + 1}.{idx + 1} - - {current.caption} -
-
- {FIGS.map((_, i) => ( +
{ + if (event.pointerType === 'touch') setTouchControls(true); + }} + ref={rootRef} + role="group" + aria-label="Machine learning systems illustrations" + > +
+ {FIGS.map(({ label, Comp }, i) => (
+ key={label} + className={`hero-scene${i === idx ? ' is-current' : ''}`} + aria-hidden={i !== idx} + > + {(i === idx || i === previous) && ( +
+ +
+ )} +
))}
+
+ +
{ + if (event.pointerType === 'mouse') setHintOpen(true); + }} + onPointerLeave={(event) => { + if (event.pointerType === 'mouse') setHintOpen(false); + }} + onBlur={(event) => { + if (!event.currentTarget.contains(event.relatedTarget)) setHintOpen(false); + }} + onKeyDown={(event) => { + if (event.key === 'Escape') { + event.preventDefault(); + setHintOpen(false); + } + }} + > + + +
+ {!reducedMotion && ( + + )} + +
); } diff --git a/src/components/Nav.astro b/src/components/Nav.astro index 5e4d628..b54814c 100644 --- a/src/components/Nav.astro +++ b/src/components/Nav.astro @@ -1,5 +1,5 @@ --- -// Top navigation — static markup; mobile menu + theme toggle use ~30 lines of inline JS. +import { TOPICS } from '@/lib/topics'; interface Props { currentPath: string; @@ -7,7 +7,7 @@ interface Props { const { currentPath } = Astro.props; const LINKS = [ - { id: 'blog', label: 'Blog', path: '/blog' }, + { id: 'blog', label: 'Articles', path: '/blog' }, { id: 'topics', label: 'Topics', path: '/topics' }, { id: 'playground', label: 'Playground', path: '/playground' }, { id: 'forum', label: 'Forum', path: '/forum' }, @@ -16,7 +16,7 @@ const LINKS = [ const isActive = (path: string) => currentPath === path || currentPath.startsWith(path + '/'); --- -