@ai-gui/core 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1404,7 +1404,7 @@ function collectNodeRenderers(plugins = [], debugOptions = {}) {
1404
1404
  map[k] = render;
1405
1405
  continue;
1406
1406
  }
1407
- map[k] = (node) => {
1407
+ map[k] = (node, context) => {
1408
1408
  const emit = (type, data) => target?.emitDebug(type, data) ?? debug?.emit(type, data);
1409
1409
  emit("plugin-render-started", {
1410
1410
  plugin: p.name,
@@ -1412,7 +1412,7 @@ function collectNodeRenderers(plugins = [], debugOptions = {}) {
1412
1412
  nodeKey: node.key
1413
1413
  });
1414
1414
  try {
1415
- const output = render(node);
1415
+ const output = render(node, context);
1416
1416
  if (isPromise(output)) return output.then((resolved) => {
1417
1417
  emit("async-output-resolved", {
1418
1418
  plugin: p.name,
@@ -1672,6 +1672,85 @@ function normalizeLineEndings(src) {
1672
1672
  return src.replace(/\r\n|\r/g, "\n");
1673
1673
  }
1674
1674
 
1675
+ //#endregion
1676
+ //#region src/export-image.ts
1677
+ /** The intrinsic size of an SVG element, falling back to its attributes and then to a default. */
1678
+ function measure(svg, options) {
1679
+ const box = typeof svg.getBoundingClientRect === "function" ? svg.getBoundingClientRect() : void 0;
1680
+ const attribute = (name) => Number.parseFloat(svg.getAttribute(name) ?? "");
1681
+ const fromViewBox = (index) => {
1682
+ const parts = (svg.getAttribute("viewBox") ?? "").split(/[\s,]+/).map(Number);
1683
+ return parts.length === 4 && Number.isFinite(parts[index]) ? parts[index] : Number.NaN;
1684
+ };
1685
+ const pick = (...candidates) => candidates.find((value) => Number.isFinite(value) && value > 0);
1686
+ return {
1687
+ width: Math.round(pick(options.width ?? Number.NaN, box?.width ?? Number.NaN, attribute("width"), fromViewBox(2), 720)),
1688
+ height: Math.round(pick(options.height ?? Number.NaN, box?.height ?? Number.NaN, attribute("height"), fromViewBox(3), 400))
1689
+ };
1690
+ }
1691
+ /**
1692
+ * Render an SVG element to a raster data URL.
1693
+ *
1694
+ * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
1695
+ * it in — is what gets exported.
1696
+ */
1697
+ async function exportSVGToImage(svg, options = {}) {
1698
+ const { width, height } = measure(svg, options);
1699
+ const scale = options.scale && options.scale > 0 ? options.scale : globalThis.devicePixelRatio || 1;
1700
+ const source = new XMLSerializer().serializeToString(svg);
1701
+ const namespaced = /\sxmlns=/.test(source) ? source : source.replace(/^<svg/, "<svg xmlns=\"http://www.w3.org/2000/svg\"");
1702
+ const url = URL.createObjectURL(new Blob([namespaced], { type: "image/svg+xml;charset=utf-8" }));
1703
+ try {
1704
+ const image = await loadImage(url);
1705
+ const canvas = document.createElement("canvas");
1706
+ canvas.width = Math.max(1, Math.round(width * scale));
1707
+ canvas.height = Math.max(1, Math.round(height * scale));
1708
+ const context = canvas.getContext("2d");
1709
+ if (!context) throw new Error("This browser cannot rasterize images");
1710
+ const background = options.background ?? "#ffffff";
1711
+ if (background !== "transparent") {
1712
+ context.fillStyle = background;
1713
+ context.fillRect(0, 0, canvas.width, canvas.height);
1714
+ }
1715
+ context.scale(scale, scale);
1716
+ context.drawImage(image, 0, 0, width, height);
1717
+ return {
1718
+ dataUrl: canvas.toDataURL(options.type ?? "image/png", options.quality),
1719
+ width: canvas.width,
1720
+ height: canvas.height
1721
+ };
1722
+ } finally {
1723
+ URL.revokeObjectURL(url);
1724
+ }
1725
+ }
1726
+ /**
1727
+ * Export every drawing inside a rendered answer.
1728
+ *
1729
+ * A host holds the element it handed to the renderer, not the individual charts, and the plugins
1730
+ * decide what lands in it.
1731
+ */
1732
+ async function exportRenderedImages(root, options = {}) {
1733
+ const drawings = Array.from(root.querySelectorAll("svg"));
1734
+ const exported = [];
1735
+ for (const drawing of drawings) exported.push(await exportSVGToImage(drawing, options));
1736
+ return exported;
1737
+ }
1738
+ /** Save an exported image under the given file name. */
1739
+ function downloadImage(image, filename) {
1740
+ const link = document.createElement("a");
1741
+ link.download = filename;
1742
+ link.href = image.dataUrl;
1743
+ link.click();
1744
+ }
1745
+ function loadImage(url) {
1746
+ return new Promise((resolve, reject) => {
1747
+ const image = new Image();
1748
+ image.onload = () => resolve(image);
1749
+ image.onerror = () => reject(new Error("The drawing could not be decoded"));
1750
+ image.src = url;
1751
+ });
1752
+ }
1753
+
1675
1754
  //#endregion
1676
1755
  //#region src/diff.ts
1677
1756
  /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
@@ -2550,6 +2629,9 @@ exports.createActionRuntime = createActionRuntime
2550
2629
  exports.createParser = createParser
2551
2630
  exports.createParserWithMetadata = createParserWithMetadata
2552
2631
  exports.diffAst = diffAst
2632
+ exports.downloadImage = downloadImage
2633
+ exports.exportRenderedImages = exportRenderedImages
2634
+ exports.exportSVGToImage = exportSVGToImage
2553
2635
  exports.getActionKey = getActionKey
2554
2636
  exports.getIdleActionState = getIdleActionState
2555
2637
  exports.isCardPatchResult = isCardPatchResult
package/dist/index.d.cts CHANGED
@@ -166,7 +166,18 @@ interface MountedCardSlot {
166
166
  interface RenderMountContext {
167
167
  mountCard?: (host: HTMLElement, request: MountCardSlotRequest) => MountedCardSlot | undefined;
168
168
  }
169
- type NodeRenderer = (node: ASTNode) => RenderOutput | Promise<RenderOutput>;
169
+ /** What the host can tell a plugin about the surroundings it is rendering into. */
170
+ interface NodeRenderContext {
171
+ /**
172
+ * The host's colour scheme, "light" or "dark" by convention.
173
+ *
174
+ * A diagram or a chart picks its own palette, and a plugin has no way to read the palette of
175
+ * the page it is embedded in, so without this an answer rendered on a dark page comes back
176
+ * with white plot areas.
177
+ */
178
+ readonly theme?: string;
179
+ }
180
+ type NodeRenderer = (node: ASTNode, context?: NodeRenderContext) => RenderOutput | Promise<RenderOutput>;
170
181
  interface PluginCommitContext {
171
182
  readonly generation: number;
172
183
  emitDebug(type: string, data?: Record<string, unknown>): void;
@@ -550,6 +561,57 @@ declare function collectNodeRenderers(plugins?: AIGuiPlugin[], debugOptions?: Co
550
561
  /** The set of node types claimed by the given plugins. */
551
562
  declare function pluginNodeTypes(plugins?: AIGuiPlugin[]): Set<string>;
552
563
 
564
+ //#endregion
565
+ //#region src/export-image.d.ts
566
+ /**
567
+ * Turn what a plugin drew into a PNG the reader can keep.
568
+ *
569
+ * Charts and diagrams are the point of rendering a model's answer, and the first thing anyone
570
+ * wants to do with one is save it. There is no browser API for "download this SVG as an image":
571
+ * it has to be serialized, loaded through an `Image`, and painted onto a canvas. Every host that
572
+ * needs it rewrites those twenty lines, and gets the same details wrong — a transparent
573
+ * background that turns black in a viewer, a blurry export on a retina screen, or a light
574
+ * background hardcoded into a page that has since gone dark.
575
+ */
576
+ interface ExportImageOptions {
577
+ /**
578
+ * Device pixels per CSS pixel. Defaults to the screen's own ratio, so an export looks as sharp
579
+ * as what it was copied from rather than half the resolution on a retina display.
580
+ */
581
+ scale?: number;
582
+ /**
583
+ * What to paint behind the drawing. An SVG is usually transparent, which reads as black in most
584
+ * image viewers. Pass the page's own background — or "transparent" to keep the alpha channel.
585
+ */
586
+ background?: string;
587
+ /** Overrides for the drawing's own size, in CSS pixels. */
588
+ width?: number;
589
+ height?: number;
590
+ type?: "image/png" | "image/jpeg" | "image/webp";
591
+ quality?: number;
592
+ }
593
+ interface ExportedImage {
594
+ dataUrl: string;
595
+ width: number;
596
+ height: number;
597
+ }
598
+ /**
599
+ * Render an SVG element to a raster data URL.
600
+ *
601
+ * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
602
+ * it in — is what gets exported.
603
+ */
604
+ declare function exportSVGToImage(svg: SVGElement, options?: ExportImageOptions): Promise<ExportedImage>;
605
+ /**
606
+ * Export every drawing inside a rendered answer.
607
+ *
608
+ * A host holds the element it handed to the renderer, not the individual charts, and the plugins
609
+ * decide what lands in it.
610
+ */
611
+ declare function exportRenderedImages(root: ParentNode, options?: ExportImageOptions): Promise<ExportedImage[]>;
612
+ /** Save an exported image under the given file name. */
613
+ declare function downloadImage(image: ExportedImage, filename: string): void;
614
+
553
615
  //#endregion
554
616
  //#region src/diff.d.ts
555
617
  /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
@@ -694,4 +756,4 @@ declare function mockModelStream(events: Iterable<ModelStreamEvent> | AsyncItera
694
756
  declare function readableBytes(chunks: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>): ReadableStream<Uint8Array>;
695
757
 
696
758
  //#endregion
697
- export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderer, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SourceBlock, StreamParseOptions, StreamRouter, Usage, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
759
+ export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SourceBlock, StreamParseOptions, StreamRouter, Usage, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
package/dist/index.d.ts CHANGED
@@ -166,7 +166,18 @@ interface MountedCardSlot {
166
166
  interface RenderMountContext {
167
167
  mountCard?: (host: HTMLElement, request: MountCardSlotRequest) => MountedCardSlot | undefined;
168
168
  }
169
- type NodeRenderer = (node: ASTNode) => RenderOutput | Promise<RenderOutput>;
169
+ /** What the host can tell a plugin about the surroundings it is rendering into. */
170
+ interface NodeRenderContext {
171
+ /**
172
+ * The host's colour scheme, "light" or "dark" by convention.
173
+ *
174
+ * A diagram or a chart picks its own palette, and a plugin has no way to read the palette of
175
+ * the page it is embedded in, so without this an answer rendered on a dark page comes back
176
+ * with white plot areas.
177
+ */
178
+ readonly theme?: string;
179
+ }
180
+ type NodeRenderer = (node: ASTNode, context?: NodeRenderContext) => RenderOutput | Promise<RenderOutput>;
170
181
  interface PluginCommitContext {
171
182
  readonly generation: number;
172
183
  emitDebug(type: string, data?: Record<string, unknown>): void;
@@ -550,6 +561,57 @@ declare function collectNodeRenderers(plugins?: AIGuiPlugin[], debugOptions?: Co
550
561
  /** The set of node types claimed by the given plugins. */
551
562
  declare function pluginNodeTypes(plugins?: AIGuiPlugin[]): Set<string>;
552
563
 
564
+ //#endregion
565
+ //#region src/export-image.d.ts
566
+ /**
567
+ * Turn what a plugin drew into a PNG the reader can keep.
568
+ *
569
+ * Charts and diagrams are the point of rendering a model's answer, and the first thing anyone
570
+ * wants to do with one is save it. There is no browser API for "download this SVG as an image":
571
+ * it has to be serialized, loaded through an `Image`, and painted onto a canvas. Every host that
572
+ * needs it rewrites those twenty lines, and gets the same details wrong — a transparent
573
+ * background that turns black in a viewer, a blurry export on a retina screen, or a light
574
+ * background hardcoded into a page that has since gone dark.
575
+ */
576
+ interface ExportImageOptions {
577
+ /**
578
+ * Device pixels per CSS pixel. Defaults to the screen's own ratio, so an export looks as sharp
579
+ * as what it was copied from rather than half the resolution on a retina display.
580
+ */
581
+ scale?: number;
582
+ /**
583
+ * What to paint behind the drawing. An SVG is usually transparent, which reads as black in most
584
+ * image viewers. Pass the page's own background — or "transparent" to keep the alpha channel.
585
+ */
586
+ background?: string;
587
+ /** Overrides for the drawing's own size, in CSS pixels. */
588
+ width?: number;
589
+ height?: number;
590
+ type?: "image/png" | "image/jpeg" | "image/webp";
591
+ quality?: number;
592
+ }
593
+ interface ExportedImage {
594
+ dataUrl: string;
595
+ width: number;
596
+ height: number;
597
+ }
598
+ /**
599
+ * Render an SVG element to a raster data URL.
600
+ *
601
+ * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
602
+ * it in — is what gets exported.
603
+ */
604
+ declare function exportSVGToImage(svg: SVGElement, options?: ExportImageOptions): Promise<ExportedImage>;
605
+ /**
606
+ * Export every drawing inside a rendered answer.
607
+ *
608
+ * A host holds the element it handed to the renderer, not the individual charts, and the plugins
609
+ * decide what lands in it.
610
+ */
611
+ declare function exportRenderedImages(root: ParentNode, options?: ExportImageOptions): Promise<ExportedImage[]>;
612
+ /** Save an exported image under the given file name. */
613
+ declare function downloadImage(image: ExportedImage, filename: string): void;
614
+
553
615
  //#endregion
554
616
  //#region src/diff.d.ts
555
617
  /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
@@ -694,4 +756,4 @@ declare function mockModelStream(events: Iterable<ModelStreamEvent> | AsyncItera
694
756
  declare function readableBytes(chunks: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>): ReadableStream<Uint8Array>;
695
757
 
696
758
  //#endregion
697
- export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderer, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SourceBlock, StreamParseOptions, StreamRouter, Usage, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
759
+ export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SourceBlock, StreamParseOptions, StreamRouter, Usage, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
package/dist/index.js CHANGED
@@ -1380,7 +1380,7 @@ function collectNodeRenderers(plugins = [], debugOptions = {}) {
1380
1380
  map[k] = render;
1381
1381
  continue;
1382
1382
  }
1383
- map[k] = (node) => {
1383
+ map[k] = (node, context) => {
1384
1384
  const emit = (type, data) => target?.emitDebug(type, data) ?? debug?.emit(type, data);
1385
1385
  emit("plugin-render-started", {
1386
1386
  plugin: p.name,
@@ -1388,7 +1388,7 @@ function collectNodeRenderers(plugins = [], debugOptions = {}) {
1388
1388
  nodeKey: node.key
1389
1389
  });
1390
1390
  try {
1391
- const output = render(node);
1391
+ const output = render(node, context);
1392
1392
  if (isPromise(output)) return output.then((resolved) => {
1393
1393
  emit("async-output-resolved", {
1394
1394
  plugin: p.name,
@@ -1648,6 +1648,85 @@ function normalizeLineEndings(src) {
1648
1648
  return src.replace(/\r\n|\r/g, "\n");
1649
1649
  }
1650
1650
 
1651
+ //#endregion
1652
+ //#region src/export-image.ts
1653
+ /** The intrinsic size of an SVG element, falling back to its attributes and then to a default. */
1654
+ function measure(svg, options) {
1655
+ const box = typeof svg.getBoundingClientRect === "function" ? svg.getBoundingClientRect() : void 0;
1656
+ const attribute = (name) => Number.parseFloat(svg.getAttribute(name) ?? "");
1657
+ const fromViewBox = (index) => {
1658
+ const parts = (svg.getAttribute("viewBox") ?? "").split(/[\s,]+/).map(Number);
1659
+ return parts.length === 4 && Number.isFinite(parts[index]) ? parts[index] : Number.NaN;
1660
+ };
1661
+ const pick = (...candidates) => candidates.find((value) => Number.isFinite(value) && value > 0);
1662
+ return {
1663
+ width: Math.round(pick(options.width ?? Number.NaN, box?.width ?? Number.NaN, attribute("width"), fromViewBox(2), 720)),
1664
+ height: Math.round(pick(options.height ?? Number.NaN, box?.height ?? Number.NaN, attribute("height"), fromViewBox(3), 400))
1665
+ };
1666
+ }
1667
+ /**
1668
+ * Render an SVG element to a raster data URL.
1669
+ *
1670
+ * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
1671
+ * it in — is what gets exported.
1672
+ */
1673
+ async function exportSVGToImage(svg, options = {}) {
1674
+ const { width, height } = measure(svg, options);
1675
+ const scale = options.scale && options.scale > 0 ? options.scale : globalThis.devicePixelRatio || 1;
1676
+ const source = new XMLSerializer().serializeToString(svg);
1677
+ const namespaced = /\sxmlns=/.test(source) ? source : source.replace(/^<svg/, "<svg xmlns=\"http://www.w3.org/2000/svg\"");
1678
+ const url = URL.createObjectURL(new Blob([namespaced], { type: "image/svg+xml;charset=utf-8" }));
1679
+ try {
1680
+ const image = await loadImage(url);
1681
+ const canvas = document.createElement("canvas");
1682
+ canvas.width = Math.max(1, Math.round(width * scale));
1683
+ canvas.height = Math.max(1, Math.round(height * scale));
1684
+ const context = canvas.getContext("2d");
1685
+ if (!context) throw new Error("This browser cannot rasterize images");
1686
+ const background = options.background ?? "#ffffff";
1687
+ if (background !== "transparent") {
1688
+ context.fillStyle = background;
1689
+ context.fillRect(0, 0, canvas.width, canvas.height);
1690
+ }
1691
+ context.scale(scale, scale);
1692
+ context.drawImage(image, 0, 0, width, height);
1693
+ return {
1694
+ dataUrl: canvas.toDataURL(options.type ?? "image/png", options.quality),
1695
+ width: canvas.width,
1696
+ height: canvas.height
1697
+ };
1698
+ } finally {
1699
+ URL.revokeObjectURL(url);
1700
+ }
1701
+ }
1702
+ /**
1703
+ * Export every drawing inside a rendered answer.
1704
+ *
1705
+ * A host holds the element it handed to the renderer, not the individual charts, and the plugins
1706
+ * decide what lands in it.
1707
+ */
1708
+ async function exportRenderedImages(root, options = {}) {
1709
+ const drawings = Array.from(root.querySelectorAll("svg"));
1710
+ const exported = [];
1711
+ for (const drawing of drawings) exported.push(await exportSVGToImage(drawing, options));
1712
+ return exported;
1713
+ }
1714
+ /** Save an exported image under the given file name. */
1715
+ function downloadImage(image, filename) {
1716
+ const link = document.createElement("a");
1717
+ link.download = filename;
1718
+ link.href = image.dataUrl;
1719
+ link.click();
1720
+ }
1721
+ function loadImage(url) {
1722
+ return new Promise((resolve, reject) => {
1723
+ const image = new Image();
1724
+ image.onload = () => resolve(image);
1725
+ image.onerror = () => reject(new Error("The drawing could not be decoded"));
1726
+ image.src = url;
1727
+ });
1728
+ }
1729
+
1651
1730
  //#endregion
1652
1731
  //#region src/diff.ts
1653
1732
  /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
@@ -2491,4 +2570,4 @@ function delay(ms, signal) {
2491
2570
  }
2492
2571
 
2493
2572
  //#endregion
2494
- export { ActionAbortedError, ActionAlreadyRegisteredError, ActionDestroyedError, ActionExecutionError, ActionNotFoundError, ActionRegistry, ActionRuntime, ActionRuntimeError, ActionTimeoutError, ActionValidationError, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardJSONError, CardLimitError, CardNotFoundError, CardRegistry, CardRevisionConflictError, CardSnapshotError, CardStore, CardStoreError, CardTypeConflictError, CardValidationError, DebugEmitter, Renderer, StreamRouter, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
2573
+ export { ActionAbortedError, ActionAlreadyRegisteredError, ActionDestroyedError, ActionExecutionError, ActionNotFoundError, ActionRegistry, ActionRuntime, ActionRuntimeError, ActionTimeoutError, ActionValidationError, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardJSONError, CardLimitError, CardNotFoundError, CardRegistry, CardRevisionConflictError, CardSnapshotError, CardStore, CardStoreError, CardTypeConflictError, CardValidationError, DebugEmitter, Renderer, StreamRouter, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/core",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Headless streaming renderer core for LLM-generated UI — markdown/JSON repair, card registry, AST diff, plugin engine, sanitizer.",
5
5
  "keywords": [
6
6
  "llm",