Render widget code offscreen, turn errors into model-readable feedback, and capture screenshots inside the frame.
A widget can fail in ways the model cannot see from its own output: a script throws, a library URL is blocked by the policy, or everything renders at zero height. This page covers the pieces that make those failures visible.
previewWidget
import { previewWidget } from "generative-frame";
const result = await previewWidget(code, { width: 680, appearance: "dark" });
// { ok, kind, width, height, blank, errors, console, screenshot?, screenshotError? }previewWidget renders complete code in a hidden frame, waits for it to settle, inspects it, takes a screenshot, and disposes the frame. ok is true when there are no errors and the render is not blank. It needs a browser.
- widthnumber= 680
Layout width in CSS pixels.
- appearance"light" | "dark"= "light"
Selects the built-in token set when `tokens` is not given.
- tokens?ThemeTokens
Theme tokens to render with.
- screenshotboolean= true
Capture a PNG. When capture fails, `screenshotError` says why.
- settleMsnumber= 600
Time to let scripts, fonts, and animations settle after `end`.
- csp?CspOptions | string
The frame's Content Security Policy, as in `createWidget`.
- product?string
Scopes the frame origin, as in `createWidget`.
- frame?SafeContentFrame
A preconfigured Safe Content Frame.
- css?string
Extra CSS for the frame.
- readyTimeoutMsnumber= 15000
How long to wait for the frame to connect.
The preview frame stays inside the viewport, nearly transparent and behind the page, because Chrome stops animation frames in cross-origin frames outside the viewport, which would freeze script-driven charts mid-animation.
buildRepairFeedback
import { buildRepairFeedback } from "generative-frame/repair";
const feedback = buildRepairFeedback(result);
// feedback.text:
// The widget did not render cleanly. Fix the causes below.
//
// Errors (1):
// - error: Chart is not defined (line 14:5)
//
// Rendered height: 0px.It takes a RenderReport and returns { ok, round, errors, console, blank, height?, screenshot?, text }. text lists errors with their locations, a blank-render note, an excerpt of console warnings and errors (the last 20 by default), and the rendered height. Options: round, ok (override the verdict), consoleLimit, and includeScreenshot (default true).
type RenderReport = {
errors: WidgetError[];
console: ConsoleEntry[];
blank: boolean;
height?: number;
screenshot?: string;
};A PreviewResult and the output of widget.inspect() (with height taken from size) both fit this shape. The assistant-ui toolkit uses the same text in its render reports.
repairLoop
import { previewWidget } from "generative-frame";
import { repairLoop } from "generative-frame/repair";
const { ok, code, report, rounds } = await repairLoop({
generate: async ({ round, previousCode, feedback }) =>
askModel({ previousCode, feedback: feedback?.text }),
render: (code) => previewWidget(code),
maxRounds: 3,
});Each round calls generate, renders the result, and builds feedback for the next round. generate returns new code as a string, or { edits } with exact replacements on the previous round's code. Edits that do not apply count as a failed round with an error that says why.
The loop stops at the first accepted render. If none is accepted, it returns the attempt with the fewest errors (a blank render counts as one).
- generate(context: GenerateContext) => Promise<string | { edits }>
Produces code. `context` has `round`, `previousCode`, and `feedback` (both undefined in the first round).
- render(code: string) => Promise<RenderReport>
Renders code and reports what happened, typically `previewWidget`.
- maxRoundsnumber= 3
Rounds including the first generation.
- accept(report: RenderReport) => boolean= no errors and not blank
Decides whether a render is good enough.
- consoleLimitnumber= 20
Console entries carried into feedback.
- includeScreenshotboolean= true
Forward the screenshot to `generate`, for models that accept images.
Screenshots
const { dataUrl, width, height } = await widget.screenshot({ scale: 2 });A host page cannot capture a cross-origin frame, so the screenshot is taken inside the frame: the runtime clones the widget, inlines computed styles, serializes it into an SVG foreignObject, draws that onto a canvas, and returns a PNG data URL. scale defaults to the frame's device pixel ratio, capped at 2, and background defaults to --color-background. Canvas elements are copied as images.
It is best effort:
- Web fonts loaded by URL are not embedded, because fetching them would need network access the frame does not have by default. Fonts already inlined as
data:URLs are. - Cross-origin images served without CORS headers cannot be inlined.
- Pseudo-elements (
::before,::after) are not reproduced. - Layout can drift by a few pixels from the live frame.
- The canvas side is capped at 4096 pixels; larger captures are scaled down.