carbon-tokenization / backend /demo /lib /embed-studio.ts
tfrere's picture
tfrere HF Staff
feat(demo): full showcase polish - mouse cursor, robust selection, HF affiliations
f469961
Raw
History Blame Contribute Delete
13.3 kB
/**
* Embed Studio scene engine for the showcase demo.
*
* Builds an "agent-picks-a-chart" beat that the viewer sees as:
* - Alice clicks "Create chart" in the empty banner placeholder
* - Studio opens with a real data file already in the sidebar
* - She types a prompt, fake agent streams text + tool bubbles
* (`listDataFiles`, `createEmbed`) through `__demo-embed-chat`
* - A pre-built neural network banner is dropped into the doc via
* `__demo-set-banner`
* - Studio closes
*
* The whole thing is orchestrated through dev-only window events
* (`__demo-embed-data`, `__demo-embed-chat`, `__demo-set-banner`)
* that `App.tsx` subscribes to in dev mode.
*/
import type { Page } from "playwright";
import { humanType, pause } from "../human-typing.js";
import { humanClick } from "./mouse-cursor.js";
import {
ARCHITECTURE_JSON,
ARCHITECTURE_JSON_META,
MODEL_ACCURACY_CSV,
MODEL_ACCURACY_CSV_META,
NEURAL_NETWORK_BANNER_HTML,
} from "../banners.js";
/**
* A single beat of an Embed Studio chat scene. The scene engine builds
* the `messages` array incrementally and re-dispatches the whole tree
* through `__demo-embed-chat` after every mutation so the UI animates
* streaming text, running spinners and fulfilled tool bubbles exactly
* like a real agent round-trip.
*/
type EmbedSceneStep =
| { kind: "user-text"; text: string }
| {
kind: "assistant-text";
text: string;
chunkMs?: [number, number];
chunkSize?: [number, number];
}
| {
kind: "assistant-tool";
toolName: string;
input?: Record<string, unknown>;
output?: string;
thinkMs?: [number, number];
}
| { kind: "pause"; ms: [number, number] };
/**
* Seed a file in the EmbedDataStore through the dev-only
* `__demo-embed-data` window event. Used before opening the Embed
* Studio so the FilesSidebar shows a realistic dataset the agent
* can then "read" via tool calls.
*/
async function seedEmbedDataFile(
page: Page,
file: {
name: string;
content: string;
ext?: string;
columns?: string[];
rowCount?: number;
uploader?: string;
},
) {
await page.evaluate((f) => {
window.dispatchEvent(
new CustomEvent("__demo-embed-data", { detail: { file: f } }),
);
}, file);
}
async function closeEmbedStudio(page: Page) {
const closeBtn = page.getByRole("button", { name: /^Save & Close$/i }).first();
if (await closeBtn.isVisible().catch(() => false)) {
await humanClick(page, closeBtn);
} else {
const xBtn = page.getByRole("button", { name: /Close Embed Studio/i }).first();
if (await xBtn.isVisible().catch(() => false)) {
await humanClick(page, xBtn);
} else {
await page.keyboard.press("Escape");
}
}
}
/**
* Play a series of `EmbedSceneStep` beats against the Embed Studio
* chat. Streams assistant text token-by-token and flips tool parts
* from `input-available` (spinner) to `output-available` (done check)
* so the viewer sees live tool-call bubbles, not just a text blob.
*/
async function runEmbedScene(page: Page, steps: EmbedSceneStep[]) {
const nowTag = Date.now().toString(36);
const userMessage: {
id: string;
role: "user";
parts: Array<{ type: "text"; text: string }>;
} = {
id: `demo-eu-${nowTag}`,
role: "user",
parts: [],
};
const assistantMessage: {
id: string;
role: "assistant";
parts: Array<Record<string, unknown>>;
} = {
id: `demo-ea-${nowTag}`,
role: "assistant",
parts: [],
};
const messages: Array<unknown> = [];
const ensureAssistant = () => {
if (!messages.includes(assistantMessage)) messages.push(assistantMessage);
};
const dispatch = async () => {
const snapshot = JSON.parse(JSON.stringify(messages));
await page.evaluate((msgs) => {
window.dispatchEvent(
new CustomEvent("__demo-embed-chat", { detail: { messages: msgs } }),
);
}, snapshot);
};
for (const step of steps) {
if (step.kind === "user-text") {
userMessage.parts.push({ type: "text", text: step.text });
if (!messages.includes(userMessage)) messages.unshift(userMessage);
await dispatch();
await pause(260, 420);
} else if (step.kind === "assistant-text") {
ensureAssistant();
const textPart: { type: "text"; text: string } = {
type: "text",
text: "",
};
assistantMessage.parts.push(textPart);
const tokens = step.text.split(/(\s+)/).filter((t) => t.length > 0);
const [chunkMin, chunkMax] = step.chunkSize ?? [1, 2];
const [delayMin, delayMax] = step.chunkMs ?? [22, 55];
let i = 0;
while (i < tokens.length) {
const take =
chunkMin +
Math.floor(Math.random() * Math.max(1, chunkMax - chunkMin + 1));
textPart.text += tokens.slice(i, i + take).join("");
i += take;
await dispatch();
await pause(delayMin, delayMax);
}
} else if (step.kind === "assistant-tool") {
ensureAssistant();
const callId = `demo-tc-${nowTag}-${Math.random().toString(36).slice(2, 6)}`;
const toolPart: Record<string, unknown> = {
type: `tool-${step.toolName}`,
toolCallId: callId,
state: "input-available",
input: step.input ?? {},
};
assistantMessage.parts.push(toolPart);
await dispatch();
await pause(step.thinkMs?.[0] ?? 520, step.thinkMs?.[1] ?? 900);
toolPart.state = "output-available";
toolPart.output = step.output ?? "done";
await dispatch();
await pause(160, 280);
} else if (step.kind === "pause") {
await pause(step.ms[0], step.ms[1]);
}
}
}
/**
* Click "Create chart" in the empty banner placeholder, seed a small
* CSV into the EmbedDataStore so the FilesSidebar shows a live file,
* type a prompt in the Embed Studio, then play a tool-aware fake
* agent scene (`listDataFiles` + `createEmbed`) that drops a pre-
* built neural network banner once the tool bubbles animate through.
*
* The banner filename stays `banner.html` (protected by the agent),
* but the tool chips, subtitle and FilesSidebar make the whole beat
* match the current Embed Studio interface instead of feeling like
* a two-line placeholder.
*/
export async function createNeuralNetworkBanner(page: Page, speed: number) {
const createBtn = page.getByRole("button", { name: /Create chart/i }).first();
try {
await createBtn.waitFor({ state: "visible", timeout: 5_000 });
} catch {
console.warn('[alice] "Create chart" button not found, skipping banner');
return;
}
// Seed two data files BEFORE opening the studio so the FilesSidebar
// shows existing datasets the agent can reference. Three purposes:
// 1. visually establishes that the studio now handles data files;
// 2. gives the listDataFiles tool call a real output to surface;
// 3. one of the files (architecture.json) is directly relevant to
// the banner the agent is about to build, so the demo can show
// the agent reading the data, extracting the layer topology,
// and feeding it back into the visualization rather than just
// ignoring the dataset and dropping a hardcoded picture.
await seedEmbedDataFile(page, {
name: ARCHITECTURE_JSON_META.name,
ext: ARCHITECTURE_JSON_META.ext,
content: ARCHITECTURE_JSON,
columns: ARCHITECTURE_JSON_META.columns,
rowCount: ARCHITECTURE_JSON_META.rowCount,
uploader: "alice",
});
await seedEmbedDataFile(page, {
name: MODEL_ACCURACY_CSV_META.name,
ext: MODEL_ACCURACY_CSV_META.ext,
content: MODEL_ACCURACY_CSV,
columns: MODEL_ACCURACY_CSV_META.columns,
rowCount: MODEL_ACCURACY_CSV_META.rowCount,
uploader: "alice",
});
await createBtn.scrollIntoViewIfNeeded();
await pause(120, 220);
await humanClick(page, createBtn);
const promptArea = page.getByPlaceholder("Describe your chart...");
const promptText =
"A neural network banner that captures the spirit of the article. Use the architecture I uploaded to drive the layout - layered graph with soft pulses flowing along the synapses, like attention scores rippling between tokens.";
try {
await promptArea.waitFor({ state: "visible", timeout: 4_000 });
await pause(260, 420);
await humanClick(page, promptArea);
await pause(180, 280);
await humanType(page, promptText, {
speed: speed * 1.6,
typoRate: 0,
thinkRate: 0.02,
});
await pause(180, 320);
await promptArea.fill("");
} catch {
console.warn("[alice] embed studio textarea not found, faking anyway");
}
// The scene mirrors a realistic agent round-trip and narrates
// the artifact we're actually about to inject, so when the
// viewer flips to the preview everything the agent promised is
// visibly there: 5 layers with the exact counts, pulses on a
// random synapse, transparent background, retinted via
// `--primary-color`.
//
// The sequence is:
// readEmbed - look at what's already in the banner slot
// (placeholder HTML from "Create chart" click)
// listDataFiles + readDataFile - confirm there IS a dataset
// but decide it's better saved for inline charts
// createEmbed - write the real banner with a descriptive
// filename and title, and a spec that matches
// the actual NEURAL_NETWORK_BANNER_HTML we ship
const csvPreviewOutput =
`${MODEL_ACCURACY_CSV_META.columns.join(",")}\n` +
MODEL_ACCURACY_CSV.split("\n").slice(1, 4).join("\n") +
"\n... 5 more rows";
await runEmbedScene(page, [
{ kind: "user-text", text: promptText },
{
kind: "assistant-text",
text:
"Good - a hero banner. Let me look at what's already in the slot and what data you've uploaded so I can pick the right shape.",
chunkMs: [22, 48],
},
{
kind: "assistant-tool",
toolName: "readEmbed",
input: { src: "banner.html" },
output:
"banner.html is the placeholder stub (14 lines). Safe to replace.",
thinkMs: [420, 640],
},
{
kind: "assistant-tool",
toolName: "listDataFiles",
input: {},
output:
`- ${ARCHITECTURE_JSON_META.name} (json, ${ARCHITECTURE_JSON.length} bytes) ` +
`keys=[${ARCHITECTURE_JSON_META.columns.join(", ")}]\n` +
`- ${MODEL_ACCURACY_CSV_META.name} (csv, ${MODEL_ACCURACY_CSV.length} bytes) ` +
`rows=${MODEL_ACCURACY_CSV_META.rowCount} ` +
`columns=[${MODEL_ACCURACY_CSV_META.columns.join(", ")}]`,
thinkMs: [440, 680],
},
{
kind: "assistant-text",
text:
` Two files. \`${ARCHITECTURE_JSON_META.name}\` looks directly relevant - that's what should drive the topology. Reading it first.`,
chunkMs: [20, 46],
},
{
kind: "assistant-tool",
toolName: "readDataFile",
input: { name: ARCHITECTURE_JSON_META.name },
output: ARCHITECTURE_JSON,
thinkMs: [520, 780],
},
{
kind: "assistant-text",
text:
" Got it - five layers (tokens 6 → embedding 10 → attention 14 → values 10 → logits 6), fully-connected, softmax activation. That maps perfectly to a layered graph.",
chunkMs: [22, 50],
chunkSize: [1, 3],
},
{
kind: "assistant-tool",
toolName: "readDataFile",
input: { name: MODEL_ACCURACY_CSV_META.name, limit: 4 },
output: csvPreviewOutput,
thinkMs: [380, 560],
},
{
kind: "assistant-text",
text:
" The accuracy CSV is great for an inline chart later in the article, but for the hero I'll stick with the architecture diagram.",
chunkMs: [22, 48],
},
{
kind: "assistant-text",
text:
" Plan: nodes spaced per layer count from the JSON, edges tracing every fully-connected pair, soft dots traveling a random synapse every ~200ms so the network feels active without competing with the title. Pure SVG, transparent background, respects `prefers-reduced-motion`.",
chunkMs: [22, 48],
chunkSize: [1, 3],
},
{
kind: "assistant-tool",
toolName: "createEmbed",
input: {
filename: "banner.html",
title: "Neural network banner",
description:
"Layered graph derived from architecture.json (5 layers: 6/10/14/10/6, fully-connected). Pulses flow along random synapses; pure SVG, transparent background.",
},
output: "wrote banner.html (182 lines, 3.8 KB, 0 external deps)",
thinkMs: [720, 1_080],
},
{
kind: "assistant-text",
text:
" Done - layer counts come from the JSON, the hero container drives the size, and the network rerolls a new burst pattern every cycle.",
chunkMs: [22, 50],
},
]);
// Drop the real banner HTML right after the tool bubbles resolve so
// the iframe flips to the final illustration in sync with the
// agent announcing "Done".
await page.evaluate(
(html) => {
window.dispatchEvent(
new CustomEvent("__demo-set-banner", { detail: { html } }),
);
},
NEURAL_NETWORK_BANNER_HTML,
);
await pause(600, 900);
await closeEmbedStudio(page);
await pause(150, 280);
}