@ai-matrx/chat 0.6.0 → 0.6.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.6.1
4
+ ### rendered-output P2 WP4: every artifact type attaches
5
+
6
+ - `RenderedArtifactSource` carries `artifact_type`, the type's own `options` (first = default,
7
+ per-type labels: "Code", "Source", "Data", …; `unavailable` names why one is not offered yet)
8
+ and, for an UNSAVED block, `record_type: "inline"` + `body: { code, text }` — its content/text
9
+ ride `request.context` as a named entry (never `user_input`). Saved items keep the
10
+ `canvas_item` / `html_page` resource reference.
11
+ - `readableText(value)` — the readable text form of structured data (server twin
12
+ `canvas_sources.readable_text`).
13
+ - Composer: a `rendered_artifact` chip is `RenderedArtifactChip` — a representation part
14
+ switches content ↔ text, or captures a Screenshot now through the capture kept from attach time.
15
+ - `attachRenderedArtifact` uploads a local (`kind: "file"`) capture through the file funnel when
16
+ no React upload hook is passed (canvas menus, message block actions).
17
+
18
+ Consumer action: optional — pass `artifact_type` + `options` (and `body` for unsaved blocks).
19
+
20
+ ### rendered-output P2 WP3: Full Print draws canvases and marked surfaces
21
+
22
+ - Full Print's pre-pass (`conversation/utils/frame-print`) now also swaps every `<canvas>` for the
23
+ picture it holds (an honest stand-in when another origin drew into it) and every
24
+ `[data-matrx-print-picture]` element for the host's picture of it — new port
25
+ `setElementPrintCapture` in `@ai-matrx/chat/host/frame-print` (a Leaflet map's tiles).
26
+
27
+ Consumer action: optional — plug `setElementPrintCapture` to draw marked surfaces.
28
+
3
29
  ## 0.6.0
4
30
 
5
31
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -0,0 +1,8 @@
1
+ import type { ManagedResource } from "../../../types/instance.types.js";
2
+ export declare function RenderedArtifactChip({ conversationId, resource, onRemove, onOpen, pressed, }: {
3
+ conversationId: string;
4
+ resource: ManagedResource;
5
+ onRemove: () => void;
6
+ onOpen: () => void;
7
+ pressed: boolean;
8
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,109 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { createElement, useState } from "react";
4
+ import { Check, ChevronDown, Shapes } from "lucide-react";
5
+ import { Popover, PopoverContent, PopoverTrigger } from "@ai-matrx/design-system";
6
+ import { Tile } from "@ai-matrx/design-system/controls";
7
+ import { useAppStore } from "../../../../store/hooks.js";
8
+ import {
9
+ isRenderedArtifactRepresentation,
10
+ isRenderedArtifactSource,
11
+ renderedArtifactCaptureFor,
12
+ renderedArtifactOptionsOf,
13
+ renderedArtifactRepresentationLabel
14
+ } from "../../../utils/renderedArtifactContext.js";
15
+ import { ComposerChip, ComposerChipPart } from "../smart-input/ComposerChip.js";
16
+ import { compactChipLabel } from "../../messages-display/user/compact-chip-label.js";
17
+ import { attachRenderedArtifact } from "./useAttachRenderedArtifact.js";
18
+ import {
19
+ removeResource,
20
+ setResourcePreview,
21
+ updateResourceOptions
22
+ } from "../../../redux/execution-system/instance-resources/instance-resources.slice.js";
23
+ function RenderedArtifactChip({
24
+ conversationId,
25
+ resource,
26
+ onRemove,
27
+ onOpen,
28
+ pressed
29
+ }) {
30
+ const store = useAppStore();
31
+ const [open, setOpen] = useState(false);
32
+ const source = isRenderedArtifactSource(resource.source) ? resource.source : null;
33
+ if (!source) return null;
34
+ const current = isRenderedArtifactRepresentation(
35
+ resource.options.representation
36
+ ) ? resource.options.representation : "code";
37
+ const title = source.title || "Artifact";
38
+ const capture = renderedArtifactCaptureFor(source);
39
+ const choose = async (value) => {
40
+ setOpen(false);
41
+ if (value === current) return;
42
+ const { resourceId } = resource;
43
+ if (value !== "screenshot") {
44
+ store.dispatch(updateResourceOptions({ conversationId, resourceId, options: { representation: value } }));
45
+ store.dispatch(
46
+ setResourcePreview({
47
+ conversationId,
48
+ resourceId,
49
+ preview: `${title} \xB7 ${renderedArtifactRepresentationLabel(source, value)}`
50
+ })
51
+ );
52
+ return;
53
+ }
54
+ if (!capture) return;
55
+ const ok = await attachRenderedArtifact(
56
+ { dispatch: store.dispatch, getState: store.getState },
57
+ conversationId,
58
+ { source, representation: "screenshot", capture }
59
+ );
60
+ if (ok) store.dispatch(removeResource({ conversationId, resourceId }));
61
+ };
62
+ const stop = (event) => event.stopPropagation();
63
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
64
+ /* @__PURE__ */ jsx(
65
+ ComposerChip,
66
+ {
67
+ tone: "quiet",
68
+ onClick: onOpen,
69
+ "aria-label": `Attached ${source.artifact_type ?? "artifact"}: ${title}`,
70
+ pressed,
71
+ icon: createElement(Shapes),
72
+ label: compactChipLabel(title),
73
+ remove: { label: `Remove ${title}`, onRemove },
74
+ parts: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
75
+ ComposerChipPart,
76
+ {
77
+ tone: "quiet",
78
+ onClick: stop,
79
+ onPointerDown: stop,
80
+ "aria-label": `What the AI receives from ${title}: ${renderedArtifactRepresentationLabel(source, current)}`,
81
+ children: [
82
+ /* @__PURE__ */ jsx("span", { children: renderedArtifactRepresentationLabel(source, current) }),
83
+ /* @__PURE__ */ jsx(ChevronDown, { className: "!size-3" })
84
+ ]
85
+ }
86
+ ) })
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsx(PopoverContent, { sizing: "content", side: "top", align: "start", sideOffset: 6, className: "p-1", onClick: stop, children: renderedArtifactOptionsOf(source).map((option) => {
90
+ const blocked = option.unavailable ?? (option.value === "screenshot" && !capture ? "Attach again to capture" : void 0);
91
+ return /* @__PURE__ */ jsx(
92
+ Tile,
93
+ {
94
+ variant: "quiet",
95
+ icon: option.value === current ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx("span", {}),
96
+ title: option.label,
97
+ selected: option.value === current,
98
+ line: blocked ?? option.hint,
99
+ disabled: Boolean(blocked),
100
+ onClick: () => void choose(option.value)
101
+ },
102
+ option.value
103
+ );
104
+ }) })
105
+ ] });
106
+ }
107
+ export {
108
+ RenderedArtifactChip
109
+ };
@@ -22,6 +22,7 @@ import { revokeTrackedObjectUrl } from "@ai-matrx/media/files";
22
22
  import { resolveContextItemDef } from "../../context-items/registry.js";
23
23
  import { remarkKindDisplay } from "../../context-items/remark-display.js";
24
24
  import { dismissRemarkChip, remarkSourceOf } from "../../../redux/execution-system/instance-resources/remarks.js";
25
+ import { RenderedArtifactChip } from "./RenderedArtifactChip.js";
25
26
  import {
26
27
  AttachedDocumentChip
27
28
  } from "./AttachedDocumentChip.js";
@@ -293,6 +294,25 @@ function SmartAgentResourceChips({
293
294
  onSettingsChange: (settings) => handleDocumentSettings(resource.resourceId, settings)
294
295
  },
295
296
  resource.resourceId
297
+ ) : resource.blockType === "rendered_artifact" && resource.status === "ready" ? /* @__PURE__ */ jsx(
298
+ motion.div,
299
+ {
300
+ initial: { opacity: 0, scale: 0.85 },
301
+ animate: { opacity: 1, scale: 1 },
302
+ exit: { opacity: 0, scale: 0.85 },
303
+ className: "inline-flex shrink-0",
304
+ children: /* @__PURE__ */ jsx(
305
+ RenderedArtifactChip,
306
+ {
307
+ conversationId,
308
+ resource,
309
+ onRemove: () => handleRemove(resource.resourceId),
310
+ onOpen: () => openDrawerForResource(resource.resourceId),
311
+ pressed: isShowingResource(resource.resourceId)
312
+ }
313
+ )
314
+ },
315
+ resource.resourceId
296
316
  ) : /* @__PURE__ */ jsx(
297
317
  ResourceChip,
298
318
  {
@@ -9,10 +9,34 @@ import {
9
9
  } from "../../../redux/execution-system/instance-resources/instance-resources.slice.js";
10
10
  import { generateResourceId } from "../../../redux/execution-system/utils/ids.js";
11
11
  import {
12
- RENDERED_ARTIFACT_BLOCK_TYPE
12
+ RENDERED_ARTIFACT_BLOCK_TYPE,
13
+ registerRenderedArtifactCapture,
14
+ renderedArtifactRepresentationLabel
13
15
  } from "../../../utils/renderedArtifactContext.js";
16
+ import { fileHandler } from "@ai-matrx/media/files/engine/handler/handler";
17
+ import { composeUploadFolderPath } from "@ai-matrx/media/files";
18
+ import {
19
+ ensureExecutionOrganization,
20
+ requireExecutionOrganizationId
21
+ } from "../../../redux/execution-system/utils/required-organization.js";
14
22
  import { attachResourceToConversation } from "./attach-resource.js";
15
23
  import { useUploadAgentResources } from "./usePasteImageResource.js";
24
+ async function uploadCapturedImage(store, conversationId, file) {
25
+ const state = store.getState();
26
+ const organizationId = await ensureExecutionOrganization(state, conversationId) ?? requireExecutionOrganizationId(state, conversationId);
27
+ const uploaded = await fileHandler.upload(
28
+ { kind: "file", file },
29
+ {
30
+ folderPath: composeUploadFolderPath("attachments", "screenshots"),
31
+ visibility: "personal",
32
+ metadata: { scope: { organization_id: organizationId } },
33
+ createShareLink: true,
34
+ shareLinkPermissionLevel: "viewer"
35
+ }
36
+ );
37
+ if (!uploaded.fileId) throw new Error("The screenshot upload returned no file id");
38
+ return { fileId: uploaded.fileId, url: uploaded.url ?? null };
39
+ }
16
40
  function screenshotName(title) {
17
41
  const base = title.trim().replace(/[^\w\s.-]+/g, "").trim().slice(0, 60) || "page";
18
42
  return `${base} screenshot.png`;
@@ -37,6 +61,7 @@ async function attachRenderedArtifact(store, conversationId, { source, represent
37
61
  toast.error("Open a chat to attach this to.");
38
62
  return false;
39
63
  }
64
+ if (capture) registerRenderedArtifactCapture(source, capture);
40
65
  if (representation !== "screenshot") {
41
66
  const resourceId = generateResourceId();
42
67
  dispatch(
@@ -52,7 +77,7 @@ async function attachRenderedArtifact(store, conversationId, { source, represent
52
77
  setResourcePreview({
53
78
  conversationId,
54
79
  resourceId,
55
- preview: `${source.title || "Page"} \xB7 ${representation === "code" ? "Code" : "Text"}`
80
+ preview: `${source.title || "Artifact"} \xB7 ${renderedArtifactRepresentationLabel(source, representation)}`
56
81
  })
57
82
  );
58
83
  return true;
@@ -77,18 +102,18 @@ async function attachRenderedArtifact(store, conversationId, { source, represent
77
102
  if (state.instanceResources.byConversationId[conversationId]?.[placeholderId] === void 0) {
78
103
  return false;
79
104
  }
80
- if (result.kind === "file") {
81
- if (!uploadFiles) throw new Error("This capture needs an upload, which is not available here");
105
+ if (result.kind === "file" && uploadFiles) {
82
106
  dispatch(removeResource({ conversationId, resourceId: placeholderId }));
83
107
  await uploadFiles([result.file]);
84
108
  return true;
85
109
  }
110
+ const stored = result.kind === "file" ? await uploadCapturedImage(store, conversationId, result.file) : result;
86
111
  const attached = await attachResourceToConversation(store, conversationId, {
87
112
  type: "file",
88
113
  data: {
89
- id: result.fileId,
114
+ id: stored.fileId,
90
115
  filename: name,
91
- url: result.url ?? void 0,
116
+ url: stored.url ?? void 0,
92
117
  type: "image/png",
93
118
  mime_type: "image/png",
94
119
  details: { filename: name }
@@ -1,41 +1,79 @@
1
1
  /**
2
2
  * Rendered-artifact context items (rendered-output standard, guarantee 3).
3
3
  *
4
- * An HTML page or HTML canvas item is attachable to a chat turn three ways:
5
- * - `code` — the document, as a resource reference (canvas_item preferred),
6
- * - `text` — what a reader sees, the server resolver's `text` representation,
7
- * - `screenshot` — the page as rendered, captured at ATTACH time by the host's
8
- * capture port and attached as an explicit image part
4
+ * Any artifact the person sees — an HTML page, a canvas item, a table / code /
5
+ * diagram / quiz block inside a message — is attachable to a chat turn in the
6
+ * representations ITS TYPE offers (the host declares them per artifact type on
7
+ * the source's `options`; HTML's are the default list below):
8
+ * - `code` — the artifact's content: the HTML document, the code, the
9
+ * diagram source, the markdown, or the kind's data as JSON
10
+ * (labelled per type: "Code", "Source", "Data", …),
11
+ * - `text` — a readable plain-text form, where one exists,
12
+ * - `screenshot` — the artifact as rendered, captured at ATTACH time by the
13
+ * host's capture port and attached as an explicit image part
9
14
  * (`input_image`, the paste-image path). Never captured at send.
10
15
  *
11
- * `code` / `text` ride `request.context` as a resource reference
12
- * (`selectResourceContextPayload`); they never become `user_input`.
16
+ * A SAVED artifact (`canvas_item` / `html_page`) rides `request.context` as a
17
+ * resource reference with the chosen representation; the server resolver reads
18
+ * it. An UNSAVED one (a block in a message that was never materialised) is an
19
+ * `inline` source: its content/text ride `request.context` as a named entry.
20
+ * Neither ever becomes `user_input` (THE USER-INPUT LAW).
13
21
  */
14
- import { createResourceReference } from "../agent-context/resource-reference.js";
15
22
  import type { RenderedArtifactRepresentation, ResourceRepresentation } from "../types/instance.types.js";
16
23
  export declare const RENDERED_ARTIFACT_BLOCK_TYPE = "rendered_artifact";
17
- export type RenderedArtifactRecordType = "canvas_item" | "html_page";
24
+ export type RenderedArtifactRecordType = "canvas_item" | "html_page" | "inline";
25
+ export interface RenderedArtifactRepresentationOption {
26
+ value: RenderedArtifactRepresentation;
27
+ label: string;
28
+ hint: string;
29
+ /** Present when this type cannot offer it yet — shown, with the reason, never hidden. */
30
+ unavailable?: string;
31
+ }
32
+ /** The body of an `inline` (unsaved) artifact — what its context entries carry. */
33
+ export interface RenderedArtifactInlineBody {
34
+ /** The content representation: code, source, markdown, or the data as JSON. */
35
+ code: string;
36
+ /** The readable plain-text form, when the type has one. */
37
+ text?: string | null;
38
+ }
18
39
  /** The `source` of a `rendered_artifact` resource. */
19
40
  export interface RenderedArtifactSource {
20
41
  kind: "rendered_artifact";
21
42
  record_type: RenderedArtifactRecordType;
43
+ /** The record id; for `inline`, a stable key of the block. */
22
44
  record_id: string;
23
45
  title: string;
46
+ /** The artifact type (canvas content type / `__kind`) — names it to the model. */
47
+ artifact_type?: string;
48
+ /** What this artifact offers, in menu order, first = default. Absent → HTML's list. */
49
+ options?: RenderedArtifactRepresentationOption[];
50
+ /** Only for `inline`. */
51
+ body?: RenderedArtifactInlineBody;
24
52
  }
25
- export interface RenderedArtifactRepresentationOption {
26
- value: RenderedArtifactRepresentation;
27
- label: string;
28
- hint: string;
29
- }
30
- /** The options a rendered artifact offers, data-driven so a new one is one entry. */
53
+ /** HTML's options — the default when a source declares none. */
31
54
  export declare function availableRenderedArtifactRepresentations(): RenderedArtifactRepresentationOption[];
55
+ /** The options a source offers (its own, else HTML's). */
56
+ export declare function renderedArtifactOptionsOf(source: RenderedArtifactSource): RenderedArtifactRepresentationOption[];
57
+ /** The chip's short name for a representation of this source. */
58
+ export declare function renderedArtifactRepresentationLabel(source: RenderedArtifactSource, representation: RenderedArtifactRepresentation): string;
32
59
  export declare function isRenderedArtifactSource(source: unknown): source is RenderedArtifactSource;
33
60
  export declare function isRenderedArtifactRepresentation(value: unknown): value is RenderedArtifactRepresentation;
34
61
  /**
35
- * The `request.context` entry for a code/text attachment, or null when the
62
+ * The `request.context` entry for a content/text attachment, or null when the
36
63
  * source is malformed or the choice is `screenshot` (an image part, not context).
37
64
  */
38
65
  export declare function renderedArtifactContextEntry(source: unknown, representation: ResourceRepresentation | undefined): {
39
66
  key: string;
40
- reference: ReturnType<typeof createResourceReference>;
67
+ reference: unknown;
41
68
  } | null;
69
+ /**
70
+ * A readable plain-text form of structured data (a kind value, a table, a
71
+ * quiz): one `label: value` line per leaf, nested objects indented, arrays as
72
+ * numbered items, `__kind` named once at the top. Server twin:
73
+ * `canvas_sources.readable_text` (same rules, same corpus in both tests).
74
+ */
75
+ export declare function readableText(value: unknown): string;
76
+ type CaptureFn = () => Promise<unknown>;
77
+ export declare function registerRenderedArtifactCapture(source: Pick<RenderedArtifactSource, "record_type" | "record_id">, capture: CaptureFn): void;
78
+ export declare function renderedArtifactCaptureFor<T = unknown>(source: Pick<RenderedArtifactSource, "record_type" | "record_id">): (() => Promise<T>) | null;
79
+ export {};
@@ -7,10 +7,23 @@ function availableRenderedArtifactRepresentations() {
7
7
  { value: "text", label: "Text", hint: "The words on the page" }
8
8
  ];
9
9
  }
10
+ function renderedArtifactOptionsOf(source) {
11
+ return source.options && source.options.length > 0 ? source.options : availableRenderedArtifactRepresentations();
12
+ }
13
+ function renderedArtifactRepresentationLabel(source, representation) {
14
+ const option = renderedArtifactOptionsOf(source).find((o) => o.value === representation);
15
+ if (option) return option.label;
16
+ return representation === "code" ? "Code" : representation === "text" ? "Text" : "Screenshot";
17
+ }
10
18
  function isRenderedArtifactSource(source) {
11
19
  if (!source || typeof source !== "object") return false;
12
20
  const s = source;
13
- return s.kind === "rendered_artifact" && (s.record_type === "canvas_item" || s.record_type === "html_page") && typeof s.record_id === "string" && s.record_id.length > 0;
21
+ if (s.kind !== "rendered_artifact") return false;
22
+ if (typeof s.record_id !== "string" || s.record_id.length === 0) return false;
23
+ if (s.record_type === "canvas_item" || s.record_type === "html_page") return true;
24
+ if (s.record_type !== "inline") return false;
25
+ const body = s.body;
26
+ return !!body && typeof body.code === "string";
14
27
  }
15
28
  function isRenderedArtifactRepresentation(value) {
16
29
  return value === "code" || value === "screenshot" || value === "text";
@@ -19,12 +32,28 @@ function serverRepresentation(recordType, representation) {
19
32
  if (representation === "text") return "text";
20
33
  return recordType === "html_page" ? "html" : "content";
21
34
  }
35
+ function inlineEntryValue(source, representation) {
36
+ const body = source.body;
37
+ const text = representation === "text" ? body.text ?? body.code : body.code;
38
+ const label = renderedArtifactRepresentationLabel(source, representation);
39
+ const type = source.artifact_type ? ` ${source.artifact_type}` : "";
40
+ return `Attached${type} "${source.title || "artifact"}" (${label.toLowerCase()}):
41
+
42
+ ${text}`;
43
+ }
22
44
  function renderedArtifactContextEntry(source, representation) {
23
45
  if (!isRenderedArtifactSource(source)) return null;
24
46
  const rep = isRenderedArtifactRepresentation(
25
47
  representation
26
48
  ) ? representation : "code";
27
49
  if (rep === "screenshot") return null;
50
+ if (source.record_type === "inline") {
51
+ const safeId = source.record_id.replace(/[^\w-]+/g, "_").slice(0, 64);
52
+ return {
53
+ key: `artifact_${safeId}_${rep === "code" ? "content" : "text"}`,
54
+ reference: inlineEntryValue(source, rep)
55
+ };
56
+ }
28
57
  return {
29
58
  key: `resource_${source.record_type}_${source.record_id}_${rep}`,
30
59
  reference: createResourceReference(source.record_type, source.record_id, {
@@ -32,10 +61,62 @@ function renderedArtifactContextEntry(source, representation) {
32
61
  })
33
62
  };
34
63
  }
64
+ function readableText(value) {
65
+ const lines = [];
66
+ const humanize = (key) => key.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
67
+ const walk = (node, depth, label) => {
68
+ const pad = " ".repeat(depth);
69
+ const prefix = label ? `${label}: ` : "";
70
+ if (node === null || node === void 0 || node === "") return;
71
+ if (typeof node !== "object") {
72
+ lines.push(`${pad}${prefix}${String(node)}`);
73
+ return;
74
+ }
75
+ if (Array.isArray(node)) {
76
+ if (node.length === 0) return;
77
+ if (label) lines.push(`${pad}${label}:`);
78
+ node.forEach((item, index) => {
79
+ if (item !== null && typeof item === "object") {
80
+ lines.push(`${pad}${label ? " " : ""}${index + 1}.`);
81
+ walk(item, depth + (label ? 2 : 1), null);
82
+ } else {
83
+ walk(item, depth + (label ? 1 : 0), `${index + 1}`);
84
+ }
85
+ });
86
+ return;
87
+ }
88
+ const record = node;
89
+ if (label) lines.push(`${pad}${label}:`);
90
+ for (const [key, child] of Object.entries(record)) {
91
+ if (key === "__kind") {
92
+ if (depth === 0 && !label && typeof child === "string") lines.push(`(${child})`);
93
+ continue;
94
+ }
95
+ walk(child, depth + (label ? 1 : 0), humanize(key));
96
+ }
97
+ };
98
+ walk(value, 0, null);
99
+ return lines.join("\n");
100
+ }
101
+ const captures = /* @__PURE__ */ new Map();
102
+ function sourceKey(source) {
103
+ return `${source.record_type}:${source.record_id}`;
104
+ }
105
+ function registerRenderedArtifactCapture(source, capture) {
106
+ captures.set(sourceKey(source), capture);
107
+ }
108
+ function renderedArtifactCaptureFor(source) {
109
+ return captures.get(sourceKey(source)) ?? null;
110
+ }
35
111
  export {
36
112
  RENDERED_ARTIFACT_BLOCK_TYPE,
37
113
  availableRenderedArtifactRepresentations,
38
114
  isRenderedArtifactRepresentation,
39
115
  isRenderedArtifactSource,
40
- renderedArtifactContextEntry
116
+ readableText,
117
+ registerRenderedArtifactCapture,
118
+ renderedArtifactCaptureFor,
119
+ renderedArtifactContextEntry,
120
+ renderedArtifactOptionsOf,
121
+ renderedArtifactRepresentationLabel
41
122
  };
@@ -1,10 +1,15 @@
1
1
  /**
2
- * Full Print's frame pre-pass (the `renderAllDiagrams` of live pages): every
3
- * `<iframe>` inside the captured message is shown as its picture — from the
4
- * host's frame capture port — or as an honest stand-in, for exactly as long
5
- * as the capture runs. The frames are hidden, never removed (removing one
6
- * reloads its page), and come back untouched when `restore` runs.
2
+ * Full Print's live-surface pre-pass (the `renderAllDiagrams` of live pages):
3
+ * every `<iframe>` inside the captured message is shown as its picture — from
4
+ * the host's frame capture port — or as an honest stand-in; every `<canvas>`
5
+ * (a chart, a document editor's page) as the picture it holds right now; every
6
+ * `[data-matrx-print-picture]` element (a map) as the host's picture of it.
7
+ * Each lasts exactly as long as the capture runs. The originals are hidden,
8
+ * never removed (removing a frame reloads its page), and come back untouched
9
+ * when `restore` runs.
7
10
  */
11
+ /** What prints in a canvas's place when its pixels cannot be read (another origin drew into it). */
12
+ export declare const CANVAS_PRINT_STAND_IN = "Drawing unavailable \u2014 print it from its card.";
8
13
  export declare function framesToPictures(root: HTMLElement): Promise<{
9
14
  restore: () => void;
10
15
  frames: number;
@@ -1,11 +1,41 @@
1
- import { FRAME_PRINT_STAND_IN, getFramePrintCapture } from "../../host/frame-print.js";
1
+ import { FRAME_PRINT_STAND_IN, getElementPrintCapture, getFramePrintCapture } from "../../host/frame-print.js";
2
+ const CANVAS_PRINT_STAND_IN = "Drawing unavailable \u2014 print it from its card.";
3
+ function standInFor(text, width, height) {
4
+ const stand = document.createElement("div");
5
+ stand.textContent = text;
6
+ stand.style.cssText = `display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:${width ? `${width}px` : "100%"};max-width:100%;min-height:${Math.min(Math.max(height, 96), 240)}px;padding:16px;border:1px dashed #94a3b8;border-radius:8px;color:#475569;font:italic 13px/1.4 system-ui,sans-serif;text-align:center;background:#f8fafc`;
7
+ return stand;
8
+ }
9
+ async function pictureOf(src, alt, width, height) {
10
+ const img = document.createElement("img");
11
+ img.src = src;
12
+ img.alt = alt;
13
+ img.style.cssText = `display:block;width:${width ? `${width}px` : "100%"};max-width:100%;height:${height ? `${height}px` : "auto"}`;
14
+ await img.decode().catch(() => void 0);
15
+ return img;
16
+ }
17
+ function swap(original, stand) {
18
+ stand.setAttribute("data-matrx-frame-print", "");
19
+ const display = original.style.display;
20
+ original.style.display = "none";
21
+ original.after(stand);
22
+ return () => {
23
+ stand.remove();
24
+ original.style.display = display;
25
+ };
26
+ }
2
27
  async function framesToPictures(root) {
3
- const frames = [...root.querySelectorAll("iframe")];
4
- if (frames.length === 0) return { restore: () => void 0, frames: 0 };
28
+ const marked = [...root.querySelectorAll("[data-matrx-print-picture]")];
29
+ const insideMarked = (node) => marked.some((element) => element !== node && element.contains(node));
30
+ const frames = [...root.querySelectorAll("iframe")].filter((frame) => !insideMarked(frame));
31
+ const canvases = [...root.querySelectorAll("canvas")].filter((canvas) => !insideMarked(canvas));
32
+ const total = frames.length + canvases.length + marked.length;
33
+ if (total === 0) return { restore: () => void 0, frames: 0 };
5
34
  const capture = getFramePrintCapture();
35
+ const elementCapture = getElementPrintCapture();
6
36
  const undo = [];
7
- await Promise.all(
8
- frames.map(async (frame) => {
37
+ await Promise.all([
38
+ ...frames.map(async (frame) => {
9
39
  let src = null;
10
40
  if (capture) {
11
41
  try {
@@ -16,31 +46,39 @@ async function framesToPictures(root) {
16
46
  }
17
47
  const width = frame.clientWidth || frame.offsetWidth;
18
48
  const height = frame.clientHeight || frame.offsetHeight;
19
- let stand;
20
- if (src) {
21
- const img = document.createElement("img");
22
- img.src = src;
23
- img.alt = frame.title || "Web page";
24
- img.style.cssText = `display:block;width:${width ? `${width}px` : "100%"};max-width:100%;height:auto`;
25
- await img.decode().catch(() => void 0);
26
- stand = img;
27
- } else {
28
- stand = document.createElement("div");
29
- stand.textContent = FRAME_PRINT_STAND_IN;
30
- stand.style.cssText = `display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:${width ? `${width}px` : "100%"};max-width:100%;min-height:${Math.min(Math.max(height, 96), 240)}px;padding:16px;border:1px dashed #94a3b8;border-radius:8px;color:#475569;font:italic 13px/1.4 system-ui,sans-serif;text-align:center;background:#f8fafc`;
49
+ const stand = src ? await pictureOf(src, frame.title || "Web page", width, null) : standInFor(FRAME_PRINT_STAND_IN, width, height);
50
+ undo.push(swap(frame, stand));
51
+ }),
52
+ ...canvases.map(async (canvas) => {
53
+ const width = canvas.clientWidth || canvas.offsetWidth;
54
+ const height = canvas.clientHeight || canvas.offsetHeight;
55
+ if (!width || !height) return;
56
+ let src = null;
57
+ try {
58
+ src = canvas.toDataURL("image/png");
59
+ } catch (error) {
60
+ console.error("[print] a drawing could not be read for print", error);
61
+ }
62
+ const stand = src ? await pictureOf(src, canvas.getAttribute("aria-label") || "Drawing", width, height) : standInFor(CANVAS_PRINT_STAND_IN, width, height);
63
+ undo.push(swap(canvas, stand));
64
+ }),
65
+ ...marked.map(async (element) => {
66
+ if (!elementCapture) return;
67
+ let src = null;
68
+ try {
69
+ src = await elementCapture(element);
70
+ } catch (error) {
71
+ console.error("[print] a live surface could not be captured for print", error);
31
72
  }
32
- stand.setAttribute("data-matrx-frame-print", "");
33
- const display = frame.style.display;
34
- frame.style.display = "none";
35
- frame.after(stand);
36
- undo.push(() => {
37
- stand.remove();
38
- frame.style.display = display;
39
- });
73
+ if (!src) return;
74
+ const width = element.clientWidth || element.offsetWidth;
75
+ const height = element.clientHeight || element.offsetHeight;
76
+ undo.push(swap(element, await pictureOf(src, element.getAttribute("aria-label") || "Picture", width, height || null)));
40
77
  })
41
- );
42
- return { restore: () => undo.forEach((fn) => fn()), frames: frames.length };
78
+ ]);
79
+ return { restore: () => undo.forEach((fn) => fn()), frames: total };
43
80
  }
44
81
  export {
82
+ CANVAS_PRINT_STAND_IN,
45
83
  framesToPictures
46
84
  };
@@ -15,3 +15,13 @@ export declare const FRAME_PRINT_STAND_IN = "Page image unavailable \u2014 print
15
15
  /** Plugs (or, with null, removes) the host's frame capture engine. */
16
16
  export declare function setFramePrintCapture(capture: FramePrintCapture | null): void;
17
17
  export declare function getFramePrintCapture(): FramePrintCapture | null;
18
+ /**
19
+ * A picture of one marked element (`data-matrx-print-picture`) — a surface a
20
+ * DOM capture draws wrong or blank (a map whose tiles come from another
21
+ * origin). The host plugs the drawing in; without it the element is captured
22
+ * as it is.
23
+ */
24
+ export type ElementPrintCapture = (element: HTMLElement) => Promise<string | null>;
25
+ /** Plugs (or, with null, removes) the host's capture for `[data-matrx-print-picture]` elements. */
26
+ export declare function setElementPrintCapture(capture: ElementPrintCapture | null): void;
27
+ export declare function getElementPrintCapture(): ElementPrintCapture | null;
@@ -6,8 +6,17 @@ function setFramePrintCapture(capture) {
6
6
  function getFramePrintCapture() {
7
7
  return globalThis[PORT] ?? null;
8
8
  }
9
+ const ELEMENT_PORT = /* @__PURE__ */ Symbol.for("ai-matrx.chat.element-print-capture");
10
+ function setElementPrintCapture(capture) {
11
+ globalThis[ELEMENT_PORT] = capture;
12
+ }
13
+ function getElementPrintCapture() {
14
+ return globalThis[ELEMENT_PORT] ?? null;
15
+ }
9
16
  export {
10
17
  FRAME_PRINT_STAND_IN,
18
+ getElementPrintCapture,
11
19
  getFramePrintCapture,
20
+ setElementPrintCapture,
12
21
  setFramePrintCapture
13
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as per-module subpaths @ai-matrx/chat/<domain>/<module> over a host contract (/host, /host/react) whose only required value is a db. One chat, one renderer: the old kernel entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) are retired.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -166,16 +166,15 @@
166
166
  "server-only": "^0.0.1",
167
167
  "uuid": "^14.0.2",
168
168
  "zod": "^4.6.5",
169
- "@ai-matrx/alchemy": "latest",
170
- "@ai-matrx/agents": "latest",
171
169
  "@ai-matrx/associations": "latest",
172
170
  "@ai-matrx/browser-audio": "latest",
173
171
  "@ai-matrx/canvas": "latest",
174
172
  "@ai-matrx/content-ir": "latest",
175
173
  "@ai-matrx/content-ir-react": "latest",
174
+ "@ai-matrx/alchemy": "latest",
176
175
  "@ai-matrx/data": "latest",
177
- "@ai-matrx/diff": "latest",
178
176
  "@ai-matrx/design-system": "latest",
177
+ "@ai-matrx/diff": "latest",
179
178
  "@ai-matrx/icons": "latest",
180
179
  "@ai-matrx/kit": "latest",
181
180
  "@ai-matrx/media": "latest",
@@ -184,7 +183,8 @@
184
183
  "@ai-matrx/records": "latest",
185
184
  "@ai-matrx/records-ui": "latest",
186
185
  "@ai-matrx/rich-content": "latest",
187
- "@ai-matrx/rich-editor": "latest"
186
+ "@ai-matrx/rich-editor": "latest",
187
+ "@ai-matrx/agents": "latest"
188
188
  },
189
189
  "devDependencies": {
190
190
  "@arethetypeswrong/cli": "^0.18.5",
@@ -300,5 +300,5 @@
300
300
  "check:side-panel-bundle": "node ./scripts/check-side-panel-bundle.mjs",
301
301
  "check:side-effects": "node ../scripts/check-side-effects-listed.mjs . && node ../scripts/check-side-effects-listed.mjs --self-test"
302
302
  },
303
- "gitHead": "168e2544282fe09d4c753a4e2fb5daf43aede766"
303
+ "gitHead": "f891cf7d6f77d82f035823fda1565422baa3d62e"
304
304
  }