@ai-gui/vanilla 0.4.4 → 0.6.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
@@ -30,7 +30,7 @@ function renderOutputToElement(out, sanitize, mountContext = {}) {
30
30
  switch (out.kind) {
31
31
  case "html": {
32
32
  const el = document.createElement("div");
33
- el.innerHTML = sanitizeOutput(out.html, sanitize);
33
+ el.innerHTML = (0, __ai_gui_core.sanitizeRenderedHtml)(out.html, sanitize, out.trusted);
34
34
  return el;
35
35
  }
36
36
  case "element": {
@@ -61,10 +61,6 @@ function renderOutputToElement(out, sanitize, mountContext = {}) {
61
61
  }
62
62
  }
63
63
  }
64
- function sanitizeOutput(html, sanitize) {
65
- if (sanitize === false) return html;
66
- return (0, __ai_gui_core.sanitizeHtml)(html, typeof sanitize === "object" ? sanitize : void 0);
67
- }
68
64
 
69
65
  //#endregion
70
66
  //#region src/render-node-dom.ts
@@ -78,7 +74,7 @@ function renderNodeToElement(node, ctx) {
78
74
  return loading;
79
75
  }
80
76
  try {
81
- const out = r(node);
77
+ const out = r(node, { theme: ctx.theme });
82
78
  if (typeof out?.then === "function") {
83
79
  const host = document.createElement("div");
84
80
  host.setAttribute("data-aigui-async-pending", "");
@@ -427,7 +423,9 @@ function updateElementInPlace(el, node, ctx) {
427
423
  //#endregion
428
424
  //#region src/create-renderer.ts
429
425
  function createRenderer(el, options = {}) {
430
- const { actionRuntime, cardStore, onCardAction,...rendererOpts } = options;
426
+ const { actionRuntime, cardStore, onCardAction, theme, onRender,...rendererOpts } = options;
427
+ let rendered = "";
428
+ let latestNodes = [];
431
429
  let actionScope = {
432
430
  owner: {},
433
431
  controller: new AbortController()
@@ -447,7 +445,10 @@ function createRenderer(el, options = {}) {
447
445
  const renderer = new __ai_gui_core.Renderer({
448
446
  ...rendererOpts,
449
447
  onPatch: (_patches, nodes) => {
450
- if (!destroyed) reconcile(el, nodes, ctx, state);
448
+ if (destroyed) return;
449
+ latestNodes = nodes;
450
+ reconcile(el, nodes, ctx, state);
451
+ onRender?.(nodes);
451
452
  }
452
453
  });
453
454
  const ctx = {
@@ -457,7 +458,8 @@ function createRenderer(el, options = {}) {
457
458
  plugins: options.plugins,
458
459
  nodeRenderers: (0, __ai_gui_core.collectNodeRenderers)(options.plugins, { debugTarget: renderer }),
459
460
  sanitize: options.sanitize,
460
- sanitized: true
461
+ sanitized: true,
462
+ theme
461
463
  };
462
464
  const disposeAll = () => {
463
465
  for (const entry of state.els.values()) disposeEl(entry.el);
@@ -473,8 +475,33 @@ function createRenderer(el, options = {}) {
473
475
  debugSource: "renderer",
474
476
  subscribeDebug: (listener) => renderer.subscribeDebug(listener),
475
477
  push: (c) => {
476
- if (!destroyed) renderer.push(c);
478
+ if (!destroyed) {
479
+ renderer.push(c);
480
+ rendered += c;
481
+ }
482
+ },
483
+ setText: (text) => {
484
+ if (destroyed || text === rendered) return;
485
+ if (text.startsWith(rendered)) renderer.push(text.slice(rendered.length));
486
+ else {
487
+ resetActionScope();
488
+ disposeAll();
489
+ renderer.reset();
490
+ state.els.clear();
491
+ el.replaceChildren();
492
+ renderer.push(text);
493
+ }
494
+ rendered = text;
495
+ },
496
+ setTheme: (next) => {
497
+ if (destroyed || ctx.theme === next) return;
498
+ ctx.theme = next;
499
+ disposeAll();
500
+ state.els.clear();
501
+ el.replaceChildren();
502
+ reconcile(el, latestNodes, ctx, state);
477
503
  },
504
+ exportImages: (exportOptions) => (0, __ai_gui_core.exportRenderedImages)(el, exportOptions),
478
505
  feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
479
506
  reset: () => {
480
507
  if (!destroyed) {
@@ -483,11 +510,13 @@ function createRenderer(el, options = {}) {
483
510
  renderer.reset();
484
511
  state.els.clear();
485
512
  el.replaceChildren();
513
+ rendered = "";
486
514
  }
487
515
  },
488
516
  destroy: () => {
489
517
  if (!destroyed) {
490
518
  destroyed = true;
519
+ rendered = "";
491
520
  actionScope.controller.abort();
492
521
  renderer.reset();
493
522
  disposeAll();
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AIGuiPlugin, ASTNode, ActionRuntime, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, RenderMountContext, RenderOutput, RendererOptions } from "@ai-gui/core";
1
+ import { AIGuiPlugin, ASTNode, ActionRuntime, CardAction, CardRegistry, CardStore, DebugEventListener, ExportImageOptions, ExportedImage, FeedOptions, FeedSource, NodeRenderer, RenderMountContext, RenderOutput, RendererOptions } from "@ai-gui/core";
2
2
 
3
3
  //#region src/render-node-dom.d.ts
4
4
  interface VanillaCardAction {
@@ -28,6 +28,8 @@ interface DomRenderContext {
28
28
  nodeRenderers?: Record<string, NodeRenderer>;
29
29
  sanitize?: RendererOptions["sanitize"];
30
30
  sanitized?: boolean;
31
+ /** The host's colour scheme, handed to every plugin that renders a node. */
32
+ theme?: string;
31
33
  }
32
34
  declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTMLElement;
33
35
 
@@ -37,12 +39,28 @@ interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
37
39
  actionRuntime?: ActionRuntime;
38
40
  cardStore?: CardStore;
39
41
  onCardAction?: DomRenderContext["onCardAction"];
42
+ /** The host's colour scheme, handed to every plugin. Change it later with `setTheme`. */
43
+ theme?: string;
44
+ /** Called with the nodes on screen whenever they change. */
45
+ onRender?: (nodes: ASTNode[]) => void;
40
46
  }
41
47
  interface VanillaRenderer {
42
48
  readonly debugSource: "renderer";
43
49
  subscribeDebug: (listener: DebugEventListener) => () => void;
44
50
  push: (chunk: string) => void;
51
+ /**
52
+ * Render this whole text, pushing only what it adds to what is already on screen.
53
+ *
54
+ * Streaming an answer means calling this with a longer string each time. Working out the delta
55
+ * is the renderer's job — a caller doing it itself has to remember what it sent and notice when
56
+ * the next text is not a continuation.
57
+ */
58
+ setText: (text: string) => void;
59
+ /** Redraw the plugins for another colour scheme. */
60
+ setTheme: (theme: string | undefined) => void;
45
61
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
62
+ /** Export every drawing currently rendered, as PNG data URLs. */
63
+ exportImages: (options?: ExportImageOptions) => Promise<ExportedImage[]>;
46
64
  reset: () => void;
47
65
  destroy: () => void;
48
66
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AIGuiPlugin, ASTNode, ActionRuntime, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, RenderMountContext, RenderOutput, RendererOptions } from "@ai-gui/core";
1
+ import { AIGuiPlugin, ASTNode, ActionRuntime, CardAction, CardRegistry, CardStore, DebugEventListener, ExportImageOptions, ExportedImage, FeedOptions, FeedSource, NodeRenderer, RenderMountContext, RenderOutput, RendererOptions } from "@ai-gui/core";
2
2
 
3
3
  //#region src/render-node-dom.d.ts
4
4
  interface VanillaCardAction {
@@ -28,6 +28,8 @@ interface DomRenderContext {
28
28
  nodeRenderers?: Record<string, NodeRenderer>;
29
29
  sanitize?: RendererOptions["sanitize"];
30
30
  sanitized?: boolean;
31
+ /** The host's colour scheme, handed to every plugin that renders a node. */
32
+ theme?: string;
31
33
  }
32
34
  declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTMLElement;
33
35
 
@@ -37,12 +39,28 @@ interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
37
39
  actionRuntime?: ActionRuntime;
38
40
  cardStore?: CardStore;
39
41
  onCardAction?: DomRenderContext["onCardAction"];
42
+ /** The host's colour scheme, handed to every plugin. Change it later with `setTheme`. */
43
+ theme?: string;
44
+ /** Called with the nodes on screen whenever they change. */
45
+ onRender?: (nodes: ASTNode[]) => void;
40
46
  }
41
47
  interface VanillaRenderer {
42
48
  readonly debugSource: "renderer";
43
49
  subscribeDebug: (listener: DebugEventListener) => () => void;
44
50
  push: (chunk: string) => void;
51
+ /**
52
+ * Render this whole text, pushing only what it adds to what is already on screen.
53
+ *
54
+ * Streaming an answer means calling this with a longer string each time. Working out the delta
55
+ * is the renderer's job — a caller doing it itself has to remember what it sent and notice when
56
+ * the next text is not a continuation.
57
+ */
58
+ setText: (text: string) => void;
59
+ /** Redraw the plugins for another colour scheme. */
60
+ setTheme: (theme: string | undefined) => void;
45
61
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
62
+ /** Export every drawing currently rendered, as PNG data URLs. */
63
+ exportImages: (options?: ExportImageOptions) => Promise<ExportedImage[]>;
46
64
  reset: () => void;
47
65
  destroy: () => void;
48
66
  }
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Renderer, collectNodeRenderers, sanitizeHtml } from "@ai-gui/core";
1
+ import { Renderer, collectNodeRenderers, exportRenderedImages, sanitizeHtml, sanitizeRenderedHtml } from "@ai-gui/core";
2
2
 
3
3
  //#region src/render-output.ts
4
4
  /** Translate a framework-neutral RenderOutput (from a plugin node renderer) into a DOM element. */
@@ -6,7 +6,7 @@ function renderOutputToElement(out, sanitize, mountContext = {}) {
6
6
  switch (out.kind) {
7
7
  case "html": {
8
8
  const el = document.createElement("div");
9
- el.innerHTML = sanitizeOutput(out.html, sanitize);
9
+ el.innerHTML = sanitizeRenderedHtml(out.html, sanitize, out.trusted);
10
10
  return el;
11
11
  }
12
12
  case "element": {
@@ -37,10 +37,6 @@ function renderOutputToElement(out, sanitize, mountContext = {}) {
37
37
  }
38
38
  }
39
39
  }
40
- function sanitizeOutput(html, sanitize) {
41
- if (sanitize === false) return html;
42
- return sanitizeHtml(html, typeof sanitize === "object" ? sanitize : void 0);
43
- }
44
40
 
45
41
  //#endregion
46
42
  //#region src/render-node-dom.ts
@@ -54,7 +50,7 @@ function renderNodeToElement(node, ctx) {
54
50
  return loading;
55
51
  }
56
52
  try {
57
- const out = r(node);
53
+ const out = r(node, { theme: ctx.theme });
58
54
  if (typeof out?.then === "function") {
59
55
  const host = document.createElement("div");
60
56
  host.setAttribute("data-aigui-async-pending", "");
@@ -403,7 +399,9 @@ function updateElementInPlace(el, node, ctx) {
403
399
  //#endregion
404
400
  //#region src/create-renderer.ts
405
401
  function createRenderer(el, options = {}) {
406
- const { actionRuntime, cardStore, onCardAction,...rendererOpts } = options;
402
+ const { actionRuntime, cardStore, onCardAction, theme, onRender,...rendererOpts } = options;
403
+ let rendered = "";
404
+ let latestNodes = [];
407
405
  let actionScope = {
408
406
  owner: {},
409
407
  controller: new AbortController()
@@ -423,7 +421,10 @@ function createRenderer(el, options = {}) {
423
421
  const renderer = new Renderer({
424
422
  ...rendererOpts,
425
423
  onPatch: (_patches, nodes) => {
426
- if (!destroyed) reconcile(el, nodes, ctx, state);
424
+ if (destroyed) return;
425
+ latestNodes = nodes;
426
+ reconcile(el, nodes, ctx, state);
427
+ onRender?.(nodes);
427
428
  }
428
429
  });
429
430
  const ctx = {
@@ -433,7 +434,8 @@ function createRenderer(el, options = {}) {
433
434
  plugins: options.plugins,
434
435
  nodeRenderers: collectNodeRenderers(options.plugins, { debugTarget: renderer }),
435
436
  sanitize: options.sanitize,
436
- sanitized: true
437
+ sanitized: true,
438
+ theme
437
439
  };
438
440
  const disposeAll = () => {
439
441
  for (const entry of state.els.values()) disposeEl(entry.el);
@@ -449,8 +451,33 @@ function createRenderer(el, options = {}) {
449
451
  debugSource: "renderer",
450
452
  subscribeDebug: (listener) => renderer.subscribeDebug(listener),
451
453
  push: (c) => {
452
- if (!destroyed) renderer.push(c);
454
+ if (!destroyed) {
455
+ renderer.push(c);
456
+ rendered += c;
457
+ }
458
+ },
459
+ setText: (text) => {
460
+ if (destroyed || text === rendered) return;
461
+ if (text.startsWith(rendered)) renderer.push(text.slice(rendered.length));
462
+ else {
463
+ resetActionScope();
464
+ disposeAll();
465
+ renderer.reset();
466
+ state.els.clear();
467
+ el.replaceChildren();
468
+ renderer.push(text);
469
+ }
470
+ rendered = text;
471
+ },
472
+ setTheme: (next) => {
473
+ if (destroyed || ctx.theme === next) return;
474
+ ctx.theme = next;
475
+ disposeAll();
476
+ state.els.clear();
477
+ el.replaceChildren();
478
+ reconcile(el, latestNodes, ctx, state);
453
479
  },
480
+ exportImages: (exportOptions) => exportRenderedImages(el, exportOptions),
454
481
  feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
455
482
  reset: () => {
456
483
  if (!destroyed) {
@@ -459,11 +486,13 @@ function createRenderer(el, options = {}) {
459
486
  renderer.reset();
460
487
  state.els.clear();
461
488
  el.replaceChildren();
489
+ rendered = "";
462
490
  }
463
491
  },
464
492
  destroy: () => {
465
493
  if (!destroyed) {
466
494
  destroyed = true;
495
+ rendered = "";
467
496
  actionScope.controller.abort();
468
497
  renderer.reset();
469
498
  disposeAll();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/vanilla",
3
- "version": "0.4.4",
3
+ "version": "0.6.0",
4
4
  "description": "Framework-free DOM adapter for AIGUI — stream LLM output straight into the DOM.",
5
5
  "keywords": [
6
6
  "llm",
@@ -50,7 +50,7 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@ai-gui/core": "0.4.4"
53
+ "@ai-gui/core": "0.6.0"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsdown",