Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions src/components/CommunityIllustration.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
const nodes = [
{ x: 98, y: 108, delay: 0.8 },
{ x: 314, y: 90, delay: 1.2 },
{ x: 350, y: 245, delay: 1.6 },
{ x: 270, y: 355, delay: 2 },
{ x: 88, y: 320, delay: 2.4 },
{ x: 48, y: 208, delay: 2.8 },
];
---

<div class="community-illustration" aria-hidden="true">
<svg viewBox="0 0 420 440" fill="none">
<defs>
<pattern id="community-grid" width="24" height="24" patternUnits="userSpaceOnUse">
<circle cx="12" cy="12" r="0.7" fill="var(--ink-3)" opacity="0.2"></circle>
</pattern>
<radialGradient id="community-fade">
<stop offset="0" stop-color="white"></stop>
<stop offset="1" stop-color="black"></stop>
</radialGradient>
<mask id="community-mask">
<rect width="420" height="440" fill="url(#community-fade)"></rect>
</mask>
</defs>
<rect width="420" height="440" fill="url(#community-grid)" mask="url(#community-mask)"></rect>
<circle cx="210" cy="218" r="150" stroke="var(--line)" stroke-dasharray="2 8"></circle>
<path d="M98 108L314 90L350 245L270 355L88 320L48 208Z" stroke="var(--line)"></path>
{
nodes.map(({ x, y, delay }) => (
<g style={`--delay: ${delay}s`}>
<path
d={`M210 218 Q${(210 + x) / 2 + 20} ${(218 + y) / 2 - 20} ${x} ${y}`}
stroke="var(--line-2)"
/>
<path
class="signal"
pathLength="100"
d={`M210 218 Q${(210 + x) / 2 + 20} ${(218 + y) / 2 - 20} ${x} ${y}`}
stroke="var(--accent)"
stroke-width="2"
stroke-linecap="round"
/>
<circle class="ripple" cx={x} cy={y} r="18" stroke="var(--accent)" />
<circle cx={x} cy={y} r="14" fill="var(--paper)" stroke="var(--line-2)" />
<circle class="node" cx={x} cy={y} r="4" fill="var(--accent)" />
</g>
))
}
<g class="shared-note">
<rect
x="174"
y="173"
width="72"
height="90"
rx="5"
fill="var(--paper)"
stroke="var(--accent)"
stroke-width="1.2"></rect>
<path d="M188 192H222M188 203H232M188 214H224" stroke="var(--ink-3)" stroke-linecap="round"
></path>
<path d="M189 242L199 230L209 237L231 220" stroke="var(--accent)" stroke-width="1.5"></path>
<circle cx="231" cy="220" r="2.5" fill="var(--accent)"></circle>
</g>
</svg>
</div>

<style>
.community-illustration {
width: 100%;
max-width: 420px;
margin: 20px auto 0;
}
svg {
display: block;
width: 100%;
overflow: visible;
}
.signal {
stroke-dasharray: 7 93;
stroke-dashoffset: 8;
opacity: 0;
animation: share 5s linear infinite;
animation-delay: var(--delay);
}
.node {
animation: receive 5s ease-in-out infinite;
animation-delay: var(--delay);
}
.ripple {
transform-box: fill-box;
transform-origin: center;
opacity: 0;
animation: ripple 5s ease-out infinite;
animation-delay: var(--delay);
}
@keyframes share {
0% {
opacity: 0;
stroke-dashoffset: 8;
}
8% {
opacity: 0.8;
}
42% {
opacity: 0.8;
}
50%,
100% {
opacity: 0;
stroke-dashoffset: -100;
}
}
@keyframes receive {
0%,
35%,
75%,
100% {
opacity: 0.35;
}
48%,
58% {
opacity: 1;
}
}
@keyframes ripple {
0%,
40% {
transform: scale(0.5);
opacity: 0;
}
48% {
opacity: 0.45;
}
75%,
100% {
transform: scale(1.7);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.signal,
.node,
.ripple {
animation: none;
}
.node {
opacity: 0.8;
}
}
</style>
4 changes: 2 additions & 2 deletions src/components/HeroFigure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function AttentionFig({ t }: { t: number }) {
// 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));
const activeRow = Math.max(0, Math.min(N - 1, Math.floor(scan)));

return (
<svg
Expand Down Expand Up @@ -587,7 +587,7 @@ function AnimatedScene({
reducedMotion: boolean;
}) {
const t = useAnimationFrame(active);
return <Comp t={reducedMotion ? 5 : t} />;
return <Comp t={reducedMotion || (!active && t === 0) ? 5 : t} />;
}

export default function HeroFigure() {
Expand Down
2 changes: 2 additions & 0 deletions src/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState } from 'react';
import ThroughputCalc from '@/content/tools/throughput-calc/ThroughputCalc';
import GpuMemoryCalc from '@/content/tools/gpu-mem-calc/GpuMemoryCalc';
import AttentionViz from '@/content/tools/attention-viz/AttentionViz';
import TrainingComputeCalc from '@/content/tools/training-compute-calc/TrainingComputeCalc';

export type PlaygroundTool = {
id: string;
Expand All @@ -17,6 +18,7 @@ const TOOL_COMPONENTS: Record<string, React.ComponentType> = {
'throughput-calc': ThroughputCalc,
'gpu-mem-calc': GpuMemoryCalc,
'attention-viz': AttentionViz,
'training-compute-calc': TrainingComputeCalc,
};

export default function Playground({ tools }: { tools: PlaygroundTool[] }) {
Expand Down
16 changes: 16 additions & 0 deletions src/components/ToolGlyph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ export default function ToolGlyph({ id }: { id: string }) {
<rect x="20" y="46" width="16" height="3" fill="var(--ink-3)" opacity="0.5" rx="1" />
</svg>
);
case 'training-compute-calc':
return (
<svg width={size} height={size} viewBox="0 0 56 56">
<path
d="M6 46 L18 34 L28 40 L50 10"
fill="none"
stroke="var(--accent)"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<line x1="6" y1="50" x2="50" y2="50" stroke="var(--ink-3)" strokeWidth="1.5" />
<line x1="6" y1="6" x2="6" y2="50" stroke="var(--ink-3)" strokeWidth="1.5" />
<circle cx="50" cy="10" r="3" fill="var(--accent)" />
</svg>
);
case 'model-card':
return (
<svg width={size} height={size} viewBox="0 0 56 56">
Expand Down
140 changes: 35 additions & 105 deletions src/content/tools/attention-viz/AttentionViz.tsx
Original file line number Diff line number Diff line change
@@ -1,128 +1,58 @@
'use client';

import { useEffect, useState } from 'react';
import { Field } from '@/components/playground/primitives';
import { AttentionFig } from '@/components/HeroFigure';

function useAnimationFrame() {
const [t, setT] = useState(0);
export default function AttentionViz() {
const [t, setT] = useState(5);
const [paused, setPaused] = useState(false);
const [reducedMotion, setReducedMotion] = useState(true);
useEffect(() => {
let raf: number;
const start = performance.now();
const tick = (now: number) => {
setT((now - start) / 1000);
raf = requestAnimationFrame(tick);
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
let frame = 0;
const update = () => {
cancelAnimationFrame(frame);
setReducedMotion(query.matches);
if (query.matches || paused) return;
const start = performance.now();
const tick = (now: number) => {
setT((now - start) / 1000);
frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
return t;
}

export default function AttentionViz({ compact = false }: { compact?: boolean }) {
const [model, setModel] = useState('llama-7b');
const [layer, setLayer] = useState(14);
const [head, setHead] = useState(3);
const [prompt, setPrompt] = useState('The cat sat on the mat and the dog ran past him.');
const t = useAnimationFrame();
update();
query.addEventListener('change', update);
return () => {
cancelAnimationFrame(frame);
query.removeEventListener('change', update);
};
}, [paused]);

return (
<div>
{!compact && (
<>
<div
style={{
display: 'flex',
alignItems: 'baseline',
gap: 12,
marginBottom: 8,
flexWrap: 'wrap',
}}
>
<h2
style={{
fontFamily: 'var(--font-display)',
fontSize: 28,
margin: 0,
fontWeight: 400,
}}
>
Attention Visualizer
</h2>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--accent)' }}>
· LIVE
</span>
</div>
<p style={{ fontSize: 13, color: 'var(--ink-3)', margin: '0 0 24px' }}>
Inspect attention patterns for any model. Drag prompts in, scrub layers and heads.
</p>
</>
<p style={{ color: 'var(--ink-2)', fontSize: 15, lineHeight: 1.65 }}>
An illustrative attention map for a fixed sentence. These patterns are synthetic; no model
is loaded and no prompt is sent to a server.
</p>
{!reducedMotion && (
<button className="filter-chip" onClick={() => setPaused(!paused)} aria-pressed={paused}>
{paused ? 'Play animation' : 'Pause animation'}
</button>
)}
<Field label="Model">
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{['llama-7b', 'llama-70b', 'mistral-7b', 'qwen-72b'].map((m) => (
<button
key={m}
className={`filter-chip ${model === m ? 'active' : ''}`}
onClick={() => setModel(m)}
>
{m}
</button>
))}
</div>
</Field>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 16 }}>
<Field label="Layer" value={`${layer} / 32`}>
<input
type="range"
min={0}
max={31}
value={layer}
onChange={(e) => setLayer(+e.target.value)}
style={{ width: '100%' }}
/>
</Field>
<Field label="Head" value={`${head} / 32`}>
<input
type="range"
min={0}
max={31}
value={head}
onChange={(e) => setHead(+e.target.value)}
style={{ width: '100%' }}
/>
</Field>
</div>
<div style={{ marginTop: 16 }}>
<Field label="Prompt">
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
style={{
width: '100%',
padding: '10px 12px',
background: 'var(--paper)',
border: '1px solid var(--line-2)',
borderRadius: 6,
fontFamily: 'var(--font-mono)',
fontSize: 13,
color: 'var(--ink)',
}}
/>
</Field>
</div>
<div
role="img"
aria-label="Synthetic causal attention map, with earlier tokens visible to later tokens"
style={{
marginTop: 24,
border: '1px solid var(--line-2)',
borderRadius: 8,
padding: 20,
background: 'var(--paper)',
height: 320,
height: 360,
}}
>
<AttentionFig t={t + layer * 0.5 + head * 0.3} />
<AttentionFig t={t} />
</div>
</div>
);
Expand Down
Loading
Loading