tfrere's picture
tfrere HF Staff
feat(demo): full showcase polish - mouse cursor, robust selection, HF affiliations
f469961
Raw
History Blame Contribute Delete
15.6 kB
/**
* Pre-built banner HTML snippets used by the demo personas.
*
* They follow the same conventions as a real HtmlEmbed in the editor:
* - Single root element with a stable class
* - Scoped <style> using CSS variables for theming
* - IIFE <script> with a `data-mounted` guard
* - No globals, no external dependencies
*
* The Editor exposes a dev-only `__demo-set-banner` window event that writes
* the provided HTML straight into `embedStore` under the `banner.html` key,
* which is exactly what the "Create chart" button + Embed Studio pipeline
* would do once the agent finished generating. We use it from the demo
* script to make the recording deterministic and visually polished.
*/
export const NEURAL_NETWORK_BANNER_HTML = `<div class="d3-banner-nn" aria-label="Neural network banner"></div>
<style>
.d3-banner-nn {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
/* Transparent on purpose: embeds (banner included) inherit the page
background, per the "no background by default" convention. */
background: transparent;
}
.d3-banner-nn svg {
width: 100%;
height: 100%;
display: block;
}
.d3-banner-nn .nn-link {
/* Hardcoded translucent white, deliberately decoupled from the
host theme. The hero surface that sits behind this banner is
always tinted (primary-colored / dark), regardless of whether
the editor body itself is in light or dark mode, so a fixed
light stroke is the most reliable readability target. Binding
the stroke to --text-color flipped between near-black and
near-white per theme and vanished whenever the iframe theme
did not match the host hero (e.g. light editor + dark hero). */
stroke: rgba(255, 255, 255, 0.4);
stroke-width: 1;
vector-effect: non-scaling-stroke;
fill: none;
}
.d3-banner-nn .nn-node {
fill: var(--primary-color, #4f46e5);
opacity: 0.85;
}
.d3-banner-nn .nn-node-ring {
fill: none;
stroke: color-mix(in oklab, var(--primary-color, #4f46e5) 60%, transparent);
stroke-width: 1.2;
vector-effect: non-scaling-stroke;
}
.d3-banner-nn .nn-pulse {
fill: var(--primary-color, #4f46e5);
}
</style>
<script>
(() => {
const bootstrap = () => {
const all = Array.from(document.querySelectorAll('.d3-banner-nn'));
const container = all.find((el) => el.dataset.mounted !== 'true');
if (!container) return;
container.dataset.mounted = 'true';
const NS = 'http://www.w3.org/2000/svg';
const W = 980;
const H = 392;
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
container.appendChild(svg);
// Layer sizes: input -> 3 hidden layers -> output.
const layers = [6, 10, 14, 10, 6];
const padX = 110;
const padY = 70;
const xs = layers.map((_, i) =>
padX + (i * (W - padX * 2)) / (layers.length - 1)
);
const nodes = [];
layers.forEach((count, li) => {
const usable = H - padY * 2;
const step = count > 1 ? usable / (count - 1) : 0;
for (let i = 0; i < count; i++) {
nodes.push({
layer: li,
idx: i,
x: xs[li],
y: padY + (count > 1 ? i * step : usable / 2),
});
}
});
const links = [];
for (let li = 0; li < layers.length - 1; li++) {
const a = nodes.filter((n) => n.layer === li);
const b = nodes.filter((n) => n.layer === li + 1);
a.forEach((na) => b.forEach((nb) => links.push([na, nb])));
}
// Trim each link so its endpoints sit just OUTSIDE the node ring
// (radius 7 in viewBox units, plus a small margin). This guarantees
// the lines never poke through the inside of the ring, which used
// to make them read as if they sat on top of the nodes even though
// gLinks is appended before gNodes in DOM order. With trimming the
// visual layering is unambiguous: the ring contains a clean gap on
// every spoke. We do this analytically rather than relying on a
// background fill, which would fight with the transparent banner.
const NODE_R = 7;
const LINK_GAP = 1.5;
const trim = NODE_R + LINK_GAP;
const gLinks = document.createElementNS(NS, 'g');
links.forEach(([a, b]) => {
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.hypot(dx, dy) || 1;
const nx = dx / dist;
const ny = dy / dist;
const line = document.createElementNS(NS, 'line');
line.setAttribute('class', 'nn-link');
line.setAttribute('x1', String(a.x + nx * trim));
line.setAttribute('y1', String(a.y + ny * trim));
line.setAttribute('x2', String(b.x - nx * trim));
line.setAttribute('y2', String(b.y - ny * trim));
gLinks.appendChild(line);
});
svg.appendChild(gLinks);
const gNodes = document.createElementNS(NS, 'g');
nodes.forEach((n) => {
const ring = document.createElementNS(NS, 'circle');
ring.setAttribute('class', 'nn-node-ring');
ring.setAttribute('cx', String(n.x));
ring.setAttribute('cy', String(n.y));
ring.setAttribute('r', '7');
gNodes.appendChild(ring);
const c = document.createElementNS(NS, 'circle');
c.setAttribute('class', 'nn-node');
c.setAttribute('cx', String(n.x));
c.setAttribute('cy', String(n.y));
c.setAttribute('r', '3.2');
gNodes.appendChild(c);
});
svg.appendChild(gNodes);
const gPulses = document.createElementNS(NS, 'g');
svg.appendChild(gPulses);
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduced) return;
let stopped = false;
container.__demoCleanup = () => { stopped = true; };
const fire = () => {
if (stopped || links.length === 0) return;
const [a, b] = links[Math.floor(Math.random() * links.length)];
const c = document.createElementNS(NS, 'circle');
c.setAttribute('class', 'nn-pulse');
c.setAttribute('r', '2.6');
c.setAttribute('cx', String(a.x));
c.setAttribute('cy', String(a.y));
c.style.opacity = '0.9';
gPulses.appendChild(c);
const start = performance.now();
const dur = 700 + Math.random() * 700;
const tick = (now) => {
if (stopped) { c.remove(); return; }
const t = Math.min(1, (now - start) / dur);
const x = a.x + (b.x - a.x) * t;
const y = a.y + (b.y - a.y) * t;
c.setAttribute('cx', String(x));
c.setAttribute('cy', String(y));
c.style.opacity = String(0.9 * (1 - t));
if (t < 1) requestAnimationFrame(tick);
else c.remove();
};
requestAnimationFrame(tick);
};
const loop = () => {
if (stopped) return;
const burst = 2 + Math.floor(Math.random() * 3);
for (let i = 0; i < burst; i++) fire();
setTimeout(loop, 160 + Math.random() * 180);
};
loop();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
} else {
bootstrap();
}
})();
</script>`;
// ---------------------------------------------------------------------------
// Sample data file + matching chart for the "inline chart from data"
// demo scene. Carol seeds this CSV into the EmbedDataStore, opens the
// Embed Studio and lets the agent turn it into a bar chart. The chart
// purposefully inlines the same rows verbatim so it renders without
// network access (the iframe is sandboxed).
// ---------------------------------------------------------------------------
export const MODEL_ACCURACY_CSV = `epoch,baseline,transformer,mixture-of-experts
1,0.62,0.71,0.74
2,0.68,0.78,0.81
3,0.71,0.83,0.86
4,0.73,0.86,0.89
5,0.74,0.88,0.91
6,0.75,0.89,0.92
7,0.75,0.90,0.93
8,0.76,0.90,0.93`;
export const MODEL_ACCURACY_CSV_META = {
name: "model-accuracy.csv",
ext: "csv",
columns: ["epoch", "baseline", "transformer", "mixture-of-experts"],
rowCount: 8,
};
// ---------------------------------------------------------------------------
// Companion architecture descriptor for the neural-network banner. Seeded
// alongside the CSV so the agent has something concrete to "discover" and
// turn into the visualization. The layer counts here are the SAME ones
// hardcoded in NEURAL_NETWORK_BANNER_HTML above (6 → 10 → 14 → 10 → 6),
// which keeps the demo deterministic: the agent reads the JSON, narrates
// the topology, then drops the matching banner.
// ---------------------------------------------------------------------------
export const ARCHITECTURE_JSON = `{
"name": "transformer-block",
"layers": [
{ "name": "tokens", "kind": "input", "units": 6 },
{ "name": "embedding", "kind": "projection","units": 10 },
{ "name": "attention", "kind": "self-attn", "units": 14 },
{ "name": "values", "kind": "projection","units": 10 },
{ "name": "logits", "kind": "output", "units": 6 }
],
"connectivity": "fully-connected",
"activation": "softmax"
}`;
export const ARCHITECTURE_JSON_META = {
name: "architecture.json",
ext: "json",
columns: ["name", "kind", "units"],
rowCount: 5,
};
/**
* Inline line chart rendered once Carol asks the agent for a
* visualization. Self-contained (no ColorPalettes lookup to keep the
* iframe deterministic in the recording), inlines the CSV rows as a
* parsed array, transparent background, uses CSS variables for theming.
*/
export const MODEL_ACCURACY_CHART_HTML = `<div class="d3-accuracy" aria-label="Model accuracy over epochs"></div>
<style>
.d3-accuracy {
width: 100%;
background: transparent;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.d3-accuracy svg { width: 100%; height: auto; display: block; }
.d3-accuracy .axis path,
.d3-accuracy .axis line {
stroke: var(--axis-color, #94a3b8);
stroke-opacity: 0.55;
}
.d3-accuracy .axis text {
fill: var(--muted-color, #64748b);
font-size: 11px;
}
.d3-accuracy .grid line {
stroke: var(--grid-color, #e2e8f0);
stroke-opacity: 0.4;
}
.d3-accuracy .series { fill: none; stroke-width: 2.2; }
.d3-accuracy .series--baseline { stroke: color-mix(in oklab, var(--muted-color, #64748b) 75%, transparent); stroke-dasharray: 4 3; }
.d3-accuracy .series--transformer { stroke: var(--primary-color, #4f46e5); }
.d3-accuracy .series--moe { stroke: color-mix(in oklab, var(--primary-color, #4f46e5) 60%, #10b981); }
.d3-accuracy .dot { stroke: var(--surface-bg, #fff); stroke-width: 1.5; }
.d3-accuracy .legend {
display: flex; gap: 14px; flex-wrap: wrap;
padding: 6px 4px 10px;
font-size: 12px;
color: var(--text-color, #0f172a);
}
.d3-accuracy .legend .swatch {
display: inline-block; width: 14px; height: 14px;
border-radius: 3px; margin-right: 6px; vertical-align: middle;
}
</style>
<script>
(() => {
const ensureD3 = (cb) => {
if (window.d3 && typeof window.d3.select === 'function') return cb();
let s = document.getElementById('d3-cdn-script');
if (!s) {
s = document.createElement('script');
s.id = 'd3-cdn-script';
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
document.head.appendChild(s);
}
s.addEventListener('load', cb, { once: true });
if (window.d3) cb();
};
const DATA = [
{ epoch: 1, baseline: 0.62, transformer: 0.71, moe: 0.74 },
{ epoch: 2, baseline: 0.68, transformer: 0.78, moe: 0.81 },
{ epoch: 3, baseline: 0.71, transformer: 0.83, moe: 0.86 },
{ epoch: 4, baseline: 0.73, transformer: 0.86, moe: 0.89 },
{ epoch: 5, baseline: 0.74, transformer: 0.88, moe: 0.91 },
{ epoch: 6, baseline: 0.75, transformer: 0.89, moe: 0.92 },
{ epoch: 7, baseline: 0.75, transformer: 0.90, moe: 0.93 },
{ epoch: 8, baseline: 0.76, transformer: 0.90, moe: 0.93 },
];
const SERIES = [
{ key: 'baseline', cls: 'series--baseline', label: 'Baseline' },
{ key: 'transformer', cls: 'series--transformer', label: 'Transformer' },
{ key: 'moe', cls: 'series--moe', label: 'Mixture of Experts' },
];
const bootstrap = () => {
const scriptEl = document.currentScript;
let container = scriptEl ? scriptEl.previousElementSibling : null;
if (!(container && container.classList.contains('d3-accuracy'))) {
const cs = Array.from(document.querySelectorAll('.d3-accuracy'))
.filter(el => el.dataset.mounted !== 'true');
container = cs[cs.length - 1] || null;
}
if (!container) return;
if (container.dataset.mounted === 'true') return;
container.dataset.mounted = 'true';
const d3 = window.d3;
const legend = document.createElement('div');
legend.className = 'legend';
legend.innerHTML = SERIES
.map(s => '<span><span class="swatch" style="background:' +
(s.cls === 'series--transformer' ? 'var(--primary-color,#4f46e5)' :
s.cls === 'series--moe' ? 'color-mix(in oklab, var(--primary-color,#4f46e5) 60%, #10b981)' :
'color-mix(in oklab, var(--muted-color,#64748b) 75%, transparent)')
+ '"></span>' + s.label + '</span>')
.join('');
container.appendChild(legend);
const svg = d3.select(container).append('svg');
const render = () => {
const w = container.clientWidth || 640;
const h = Math.max(240, Math.round(w / 2.6));
const margin = { top: 10, right: 16, bottom: 28, left: 34 };
svg.attr('viewBox', '0 0 ' + w + ' ' + h)
.attr('width', w).attr('height', h);
svg.selectAll('*').remove();
const x = d3.scaleLinear()
.domain(d3.extent(DATA, d => d.epoch))
.range([margin.left, w - margin.right]);
const y = d3.scaleLinear()
.domain([0.55, 1]).nice()
.range([h - margin.bottom, margin.top]);
const g = svg.append('g');
g.append('g')
.attr('class', 'grid')
.attr('transform', 'translate(' + margin.left + ',0)')
.call(d3.axisLeft(y).ticks(5).tickSize(-(w - margin.left - margin.right)).tickFormat(() => ''));
g.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + (h - margin.bottom) + ')')
.call(d3.axisBottom(x).ticks(DATA.length).tickFormat(d3.format('d')));
g.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(' + margin.left + ',0)')
.call(d3.axisLeft(y).ticks(5).tickFormat(d3.format('.0%')));
const line = d3.line().x(d => x(d.epoch)).y(d => y(d.v)).curve(d3.curveMonotoneX);
SERIES.forEach(s => {
const rows = DATA.map(d => ({ epoch: d.epoch, v: d[s.key] }));
g.append('path')
.datum(rows)
.attr('class', 'series ' + s.cls)
.attr('d', line);
g.selectAll('.dot-' + s.key)
.data(rows)
.join('circle')
.attr('class', 'dot')
.attr('r', 3)
.attr('cx', d => x(d.epoch))
.attr('cy', d => y(d.v))
.attr('fill',
s.cls === 'series--transformer' ? 'var(--primary-color,#4f46e5)' :
s.cls === 'series--moe' ? 'color-mix(in oklab, var(--primary-color,#4f46e5) 60%, #10b981)' :
'var(--muted-color,#64748b)');
});
};
ensureD3(() => {
render();
if (window.ResizeObserver) new ResizeObserver(render).observe(container);
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
} else {
bootstrap();
}
})();
</script>`;