@assistant-ui/react-generative-ui 0.0.23 → 0.1.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/README.md +2 -0
- package/dist/a2ui.d.ts +1 -4
- package/dist/a2ui.d.ts.map +1 -1
- package/dist/a2ui.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -10
- package/dist/index.server.d.ts +1 -2
- package/dist/index.server.d.ts.map +1 -1
- package/dist/index.server.js +1 -10
- package/dist/ir.d.ts +1 -111
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js +1 -175
- package/dist/slack.d.ts +1 -4
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +1 -4
- package/dist/teams.d.ts +1 -4
- package/dist/teams.d.ts.map +1 -1
- package/dist/teams.js +1 -4
- package/package.json +5 -5
- package/src/a2ui.ts +1 -22
- package/src/index.server.ts +1 -2
- package/src/index.test.ts +75 -0
- package/src/index.ts +1 -2
- package/src/ir.ts +1 -279
- package/src/slack.ts +1 -39
- package/src/teams.ts +1 -35
- package/dist/JSONGenerativeUI.client.d.ts +0 -28
- package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.client.js +0 -101
- package/dist/JSONGenerativeUI.client.js.map +0 -1
- package/dist/JSONGenerativeUI.server.d.ts +0 -21
- package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.server.js +0 -32
- package/dist/JSONGenerativeUI.server.js.map +0 -1
- package/dist/JSONGenerativeUI.shared.d.ts +0 -63
- package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.shared.js +0 -31
- package/dist/JSONGenerativeUI.shared.js.map +0 -1
- package/dist/RadioGroupScope.d.ts +0 -6
- package/dist/RadioGroupScope.d.ts.map +0 -1
- package/dist/RadioGroupScope.js +0 -25
- package/dist/RadioGroupScope.js.map +0 -1
- package/dist/a2ui/BindingContext.d.ts +0 -11
- package/dist/a2ui/BindingContext.d.ts.map +0 -1
- package/dist/a2ui/BindingContext.js +0 -12
- package/dist/a2ui/BindingContext.js.map +0 -1
- package/dist/a2ui/PresentRenderer.d.ts +0 -11
- package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
- package/dist/a2ui/PresentRenderer.js +0 -39
- package/dist/a2ui/PresentRenderer.js.map +0 -1
- package/dist/a2ui/convert.d.ts +0 -15
- package/dist/a2ui/convert.d.ts.map +0 -1
- package/dist/a2ui/convert.js +0 -728
- package/dist/a2ui/convert.js.map +0 -1
- package/dist/a2ui/dataModel.d.ts +0 -9
- package/dist/a2ui/dataModel.d.ts.map +0 -1
- package/dist/a2ui/dataModel.js +0 -44
- package/dist/a2ui/dataModel.js.map +0 -1
- package/dist/a2ui/reducer.d.ts +0 -8
- package/dist/a2ui/reducer.d.ts.map +0 -1
- package/dist/a2ui/reducer.js +0 -242
- package/dist/a2ui/reducer.js.map +0 -1
- package/dist/a2ui/snapshot.d.ts +0 -3
- package/dist/a2ui/snapshot.d.ts.map +0 -1
- package/dist/a2ui/snapshot.js +0 -33
- package/dist/a2ui/snapshot.js.map +0 -1
- package/dist/a2ui/types.d.ts +0 -88
- package/dist/a2ui/types.d.ts.map +0 -1
- package/dist/a2ui/types.js +0 -6
- package/dist/a2ui/types.js.map +0 -1
- package/dist/a2ui/valueFunctions.d.ts +0 -16
- package/dist/a2ui/valueFunctions.d.ts.map +0 -1
- package/dist/a2ui/valueFunctions.js +0 -332
- package/dist/a2ui/valueFunctions.js.map +0 -1
- package/dist/actionRegistry.d.ts +0 -29
- package/dist/actionRegistry.d.ts.map +0 -1
- package/dist/actionRegistry.js +0 -41
- package/dist/actionRegistry.js.map +0 -1
- package/dist/answeredValues.d.ts +0 -16
- package/dist/answeredValues.d.ts.map +0 -1
- package/dist/answeredValues.js +0 -37
- package/dist/answeredValues.js.map +0 -1
- package/dist/buildPresentParameters.d.ts +0 -21
- package/dist/buildPresentParameters.d.ts.map +0 -1
- package/dist/buildPresentParameters.js +0 -95
- package/dist/buildPresentParameters.js.map +0 -1
- package/dist/constants.d.ts +0 -15
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -19
- package/dist/constants.js.map +0 -1
- package/dist/convert/boundSpec.d.ts +0 -48
- package/dist/convert/boundSpec.d.ts.map +0 -1
- package/dist/convert/boundSpec.js +0 -108
- package/dist/convert/boundSpec.js.map +0 -1
- package/dist/convert/copyBounded.d.ts +0 -25
- package/dist/convert/copyBounded.d.ts.map +0 -1
- package/dist/convert/copyBounded.js +0 -38
- package/dist/convert/copyBounded.js.map +0 -1
- package/dist/convert/isElement.d.ts +0 -3
- package/dist/convert/isElement.d.ts.map +0 -1
- package/dist/convert/isElement.js +0 -6
- package/dist/convert/isElement.js.map +0 -1
- package/dist/convert/takeRun.d.ts +0 -6
- package/dist/convert/takeRun.d.ts.map +0 -1
- package/dist/convert/takeRun.js +0 -20
- package/dist/convert/takeRun.js.map +0 -1
- package/dist/defineGenerativeComponents.d.ts +0 -29
- package/dist/defineGenerativeComponents.d.ts.map +0 -1
- package/dist/defineGenerativeComponents.js +0 -34
- package/dist/defineGenerativeComponents.js.map +0 -1
- package/dist/fieldReferences.d.ts +0 -9
- package/dist/fieldReferences.d.ts.map +0 -1
- package/dist/fieldReferences.js +0 -53
- package/dist/fieldReferences.js.map +0 -1
- package/dist/generativeUIToJSX.d.ts +0 -18
- package/dist/generativeUIToJSX.d.ts.map +0 -1
- package/dist/generativeUIToJSX.js +0 -76
- package/dist/generativeUIToJSX.js.map +0 -1
- package/dist/index.shared.d.ts +0 -15
- package/dist/index.shared.d.ts.map +0 -1
- package/dist/index.shared.js +0 -9
- package/dist/ir.js.map +0 -1
- package/dist/renderGenerativeUI.d.ts +0 -14
- package/dist/renderGenerativeUI.d.ts.map +0 -1
- package/dist/renderGenerativeUI.js +0 -83
- package/dist/renderGenerativeUI.js.map +0 -1
- package/dist/scopeSchema.d.ts +0 -6
- package/dist/scopeSchema.d.ts.map +0 -1
- package/dist/scopeSchema.js +0 -56
- package/dist/scopeSchema.js.map +0 -1
- package/dist/slack/constants.d.ts +0 -93
- package/dist/slack/constants.d.ts.map +0 -1
- package/dist/slack/constants.js +0 -146
- package/dist/slack/constants.js.map +0 -1
- package/dist/slack/decodeBlockAction.d.ts +0 -10
- package/dist/slack/decodeBlockAction.d.ts.map +0 -1
- package/dist/slack/decodeBlockAction.js +0 -129
- package/dist/slack/decodeBlockAction.js.map +0 -1
- package/dist/slack/fromSlackBlocks.d.ts +0 -27
- package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
- package/dist/slack/fromSlackBlocks.js +0 -483
- package/dist/slack/fromSlackBlocks.js.map +0 -1
- package/dist/slack/toSlackBlocks.d.ts +0 -4
- package/dist/slack/toSlackBlocks.d.ts.map +0 -1
- package/dist/slack/toSlackBlocks.js +0 -901
- package/dist/slack/toSlackBlocks.js.map +0 -1
- package/dist/slack/types.d.ts +0 -204
- package/dist/slack/types.d.ts.map +0 -1
- package/dist/slack/types.js +0 -0
- package/dist/teams/constants.d.ts +0 -34
- package/dist/teams/constants.d.ts.map +0 -1
- package/dist/teams/constants.js +0 -68
- package/dist/teams/constants.js.map +0 -1
- package/dist/teams/decodeSubmitData.d.ts +0 -26
- package/dist/teams/decodeSubmitData.d.ts.map +0 -1
- package/dist/teams/decodeSubmitData.js +0 -70
- package/dist/teams/decodeSubmitData.js.map +0 -1
- package/dist/teams/temporalId.d.ts +0 -5
- package/dist/teams/temporalId.d.ts.map +0 -1
- package/dist/teams/temporalId.js +0 -28
- package/dist/teams/temporalId.js.map +0 -1
- package/dist/teams/toAdaptiveCard.d.ts +0 -45
- package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
- package/dist/teams/toAdaptiveCard.js +0 -642
- package/dist/teams/toAdaptiveCard.js.map +0 -1
- package/dist/teams/toTeamsAttachments.d.ts +0 -10
- package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
- package/dist/teams/toTeamsAttachments.js +0 -99
- package/dist/teams/toTeamsAttachments.js.map +0 -1
- package/dist/teams/types.d.ts +0 -224
- package/dist/teams/types.d.ts.map +0 -1
- package/dist/teams/types.js +0 -0
- package/dist/temporal.d.ts +0 -33
- package/dist/temporal.d.ts.map +0 -1
- package/dist/temporal.js +0 -152
- package/dist/temporal.js.map +0 -1
- package/dist/types.d.ts +0 -99
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -0
- package/dist/vocabulary/alert.d.ts +0 -32
- package/dist/vocabulary/alert.d.ts.map +0 -1
- package/dist/vocabulary/alert.js +0 -160
- package/dist/vocabulary/alert.js.map +0 -1
- package/dist/vocabulary/collectFormValues.d.ts +0 -23
- package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
- package/dist/vocabulary/collectFormValues.js +0 -44
- package/dist/vocabulary/collectFormValues.js.map +0 -1
- package/dist/vocabulary/data.d.ts +0 -66
- package/dist/vocabulary/data.d.ts.map +0 -1
- package/dist/vocabulary/data.js +0 -503
- package/dist/vocabulary/data.js.map +0 -1
- package/dist/vocabulary/dispatch.d.ts +0 -9
- package/dist/vocabulary/dispatch.d.ts.map +0 -1
- package/dist/vocabulary/dispatch.js +0 -50
- package/dist/vocabulary/dispatch.js.map +0 -1
- package/dist/vocabulary/fact.d.ts +0 -19
- package/dist/vocabulary/fact.d.ts.map +0 -1
- package/dist/vocabulary/fact.js +0 -51
- package/dist/vocabulary/fact.js.map +0 -1
- package/dist/vocabulary/form.d.ts +0 -11
- package/dist/vocabulary/form.d.ts.map +0 -1
- package/dist/vocabulary/form.js +0 -23
- package/dist/vocabulary/form.js.map +0 -1
- package/dist/vocabulary/formatValue.d.ts +0 -7
- package/dist/vocabulary/formatValue.d.ts.map +0 -1
- package/dist/vocabulary/formatValue.js +0 -76
- package/dist/vocabulary/formatValue.js.map +0 -1
- package/dist/vocabulary/icon.d.ts +0 -41
- package/dist/vocabulary/icon.d.ts.map +0 -1
- package/dist/vocabulary/icon.js +0 -272
- package/dist/vocabulary/icon.js.map +0 -1
- package/dist/vocabulary/index.d.ts +0 -12
- package/dist/vocabulary/index.d.ts.map +0 -1
- package/dist/vocabulary/index.js +0 -36
- package/dist/vocabulary/index.js.map +0 -1
- package/dist/vocabulary/interactive.d.ts +0 -230
- package/dist/vocabulary/interactive.d.ts.map +0 -1
- package/dist/vocabulary/interactive.js +0 -530
- package/dist/vocabulary/interactive.js.map +0 -1
- package/dist/vocabulary/layout.d.ts +0 -79
- package/dist/vocabulary/layout.d.ts.map +0 -1
- package/dist/vocabulary/layout.js +0 -139
- package/dist/vocabulary/layout.js.map +0 -1
- package/dist/vocabulary/list.d.ts +0 -14
- package/dist/vocabulary/list.d.ts.map +0 -1
- package/dist/vocabulary/list.js +0 -50
- package/dist/vocabulary/list.js.map +0 -1
- package/dist/vocabulary/media.d.ts +0 -25
- package/dist/vocabulary/media.d.ts.map +0 -1
- package/dist/vocabulary/media.js +0 -42
- package/dist/vocabulary/media.js.map +0 -1
- package/dist/vocabulary/text.d.ts +0 -57
- package/dist/vocabulary/text.d.ts.map +0 -1
- package/dist/vocabulary/text.js +0 -49
- package/dist/vocabulary/text.js.map +0 -1
- package/dist/vocabulary/toTextContent.d.ts +0 -2
- package/dist/vocabulary/toTextContent.d.ts.map +0 -1
- package/dist/vocabulary/toTextContent.js +0 -6
- package/dist/vocabulary/toTextContent.js.map +0 -1
- package/src/JSONGenerativeUI.client.tsx +0 -168
- package/src/JSONGenerativeUI.server.tsx +0 -39
- package/src/JSONGenerativeUI.shared.ts +0 -90
- package/src/JSONGenerativeUI.test.tsx +0 -1298
- package/src/RadioGroupScope.server.test.tsx +0 -24
- package/src/RadioGroupScope.tsx +0 -34
- package/src/a2ui/BindingContext.ts +0 -26
- package/src/a2ui/PresentRenderer.tsx +0 -83
- package/src/a2ui/convert.test.ts +0 -2923
- package/src/a2ui/convert.ts +0 -1207
- package/src/a2ui/dataModel.ts +0 -69
- package/src/a2ui/reducer.test.ts +0 -422
- package/src/a2ui/reducer.ts +0 -363
- package/src/a2ui/snapshot.test.ts +0 -52
- package/src/a2ui/snapshot.ts +0 -52
- package/src/a2ui/types.ts +0 -114
- package/src/a2ui/valueFunctions.test.ts +0 -271
- package/src/a2ui/valueFunctions.ts +0 -523
- package/src/actionRegistry.test.ts +0 -90
- package/src/actionRegistry.ts +0 -88
- package/src/answeredValues.test.ts +0 -33
- package/src/answeredValues.tsx +0 -70
- package/src/buildPresentParameters.test.ts +0 -246
- package/src/buildPresentParameters.ts +0 -130
- package/src/constants.ts +0 -17
- package/src/convert/boundSpec.test.ts +0 -192
- package/src/convert/boundSpec.ts +0 -134
- package/src/convert/copyBounded.test.ts +0 -131
- package/src/convert/copyBounded.ts +0 -38
- package/src/convert/isElement.ts +0 -6
- package/src/convert/takeRun.test.ts +0 -64
- package/src/convert/takeRun.ts +0 -19
- package/src/defineGenerativeComponents.ts +0 -38
- package/src/fieldReferences.test.ts +0 -66
- package/src/fieldReferences.ts +0 -83
- package/src/generativeUIToJSX.test.ts +0 -268
- package/src/generativeUIToJSX.ts +0 -152
- package/src/index.server.test.ts +0 -106
- package/src/index.shared.ts +0 -61
- package/src/ir.test.ts +0 -203
- package/src/renderGenerativeUI.test.tsx +0 -373
- package/src/renderGenerativeUI.tsx +0 -165
- package/src/scopeSchema.test.ts +0 -72
- package/src/scopeSchema.ts +0 -71
- package/src/slack/a2uiSlack.test.ts +0 -198
- package/src/slack/constants.ts +0 -194
- package/src/slack/datePicker.test.ts +0 -612
- package/src/slack/decodeBlockAction.test.ts +0 -233
- package/src/slack/decodeBlockAction.ts +0 -232
- package/src/slack/fromSlackBlocks.test.ts +0 -1465
- package/src/slack/fromSlackBlocks.ts +0 -789
- package/src/slack/toSlackBlocks.test.ts +0 -3065
- package/src/slack/toSlackBlocks.ts +0 -1818
- package/src/slack/types.ts +0 -264
- package/src/teams/constants.ts +0 -93
- package/src/teams/decodeSubmitData.test.ts +0 -397
- package/src/teams/decodeSubmitData.ts +0 -110
- package/src/teams/temporal.test.ts +0 -766
- package/src/teams/temporalId.ts +0 -56
- package/src/teams/toAdaptiveCard.test.ts +0 -1897
- package/src/teams/toAdaptiveCard.ts +0 -1057
- package/src/teams/toTeamsAttachments.test.ts +0 -74
- package/src/teams/toTeamsAttachments.ts +0 -133
- package/src/teams/types.ts +0 -266
- package/src/temporal.test.ts +0 -319
- package/src/temporal.ts +0 -259
- package/src/types.ts +0 -119
- package/src/vocabulary/alert.test.tsx +0 -427
- package/src/vocabulary/alert.tsx +0 -217
- package/src/vocabulary/collectFormValues.test.ts +0 -389
- package/src/vocabulary/collectFormValues.ts +0 -116
- package/src/vocabulary/data.test.tsx +0 -858
- package/src/vocabulary/data.tsx +0 -819
- package/src/vocabulary/datepicker.dom.test.tsx +0 -1148
- package/src/vocabulary/dispatch.ts +0 -83
- package/src/vocabulary/fact.test.tsx +0 -94
- package/src/vocabulary/fact.tsx +0 -54
- package/src/vocabulary/form.test.tsx +0 -113
- package/src/vocabulary/form.tsx +0 -39
- package/src/vocabulary/formatValue.test.ts +0 -72
- package/src/vocabulary/formatValue.ts +0 -127
- package/src/vocabulary/icon.test.tsx +0 -64
- package/src/vocabulary/icon.tsx +0 -174
- package/src/vocabulary/index.test.ts +0 -65
- package/src/vocabulary/index.ts +0 -33
- package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
- package/src/vocabulary/interactive.dom.test.tsx +0 -1747
- package/src/vocabulary/interactive.test.tsx +0 -377
- package/src/vocabulary/interactive.tsx +0 -1048
- package/src/vocabulary/layout.test.tsx +0 -323
- package/src/vocabulary/layout.tsx +0 -247
- package/src/vocabulary/list.test.tsx +0 -222
- package/src/vocabulary/list.tsx +0 -50
- package/src/vocabulary/media.test.tsx +0 -85
- package/src/vocabulary/media.tsx +0 -56
- package/src/vocabulary/text.test.tsx +0 -102
- package/src/vocabulary/text.tsx +0 -60
- package/src/vocabulary/toTextContent.ts +0 -2
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { RadioGroupScope } from "./RadioGroupScope";
|
|
2
|
-
import { getPartialJsonObjectMeta } from "assistant-stream/utils";
|
|
3
|
-
import { Fragment, type ReactNode } from "react";
|
|
4
|
-
import { hasFieldReference, resolveFieldReferences } from "./fieldReferences";
|
|
5
|
-
import {
|
|
6
|
-
normalizeUINode,
|
|
7
|
-
type Action,
|
|
8
|
-
type NormalizedUIElement,
|
|
9
|
-
type NormalizedUINode,
|
|
10
|
-
} from "./ir";
|
|
11
|
-
import type { GenerativeUILibrary, GenerativeUIRenderContext } from "./types";
|
|
12
|
-
|
|
13
|
-
const DEFAULT_CONTEXT: GenerativeUIRenderContext = { status: "done" };
|
|
14
|
-
|
|
15
|
-
const isElement = (node: NormalizedUINode): node is NormalizedUIElement =>
|
|
16
|
-
typeof node === "object" && node !== null && !Array.isArray(node);
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Renders a generative-ui tree against a {@link GenerativeUILibrary}.
|
|
20
|
-
*
|
|
21
|
-
* The model emits each node as a flat object `{ $type, ...props }`. We first
|
|
22
|
-
* normalize that wire form into the canonical {@link NormalizedUINode} (with
|
|
23
|
-
* `children` lifted to a reserved top-level key), then render: each `type` is
|
|
24
|
-
* looked up in the library and its `props` are passed to the component's
|
|
25
|
-
* `render(props, context)`, with `children` rendered recursively so components
|
|
26
|
-
* can nest.
|
|
27
|
-
*/
|
|
28
|
-
export function renderGenerativeUI(
|
|
29
|
-
node: unknown,
|
|
30
|
-
library: GenerativeUILibrary,
|
|
31
|
-
context: GenerativeUIRenderContext = DEFAULT_CONTEXT,
|
|
32
|
-
): ReactNode {
|
|
33
|
-
// Tool args are parsed incrementally, and the parse meta records which path
|
|
34
|
-
// is still mid-arrival, so normalization can hold back a node whose `$type`
|
|
35
|
-
// string has not finished streaming.
|
|
36
|
-
const meta = getPartialJsonObjectMeta(node as Record<symbol, unknown>);
|
|
37
|
-
const partialPath = meta?.state === "partial" ? meta.partialPath : undefined;
|
|
38
|
-
const rendered = renderNode(
|
|
39
|
-
normalizeUINode(node, partialPath),
|
|
40
|
-
library,
|
|
41
|
-
withFieldFallbacks(context),
|
|
42
|
-
);
|
|
43
|
-
return rendered !== null && typeof rendered === "object" ? (
|
|
44
|
-
<RadioGroupScope>{rendered}</RadioGroupScope>
|
|
45
|
-
) : (
|
|
46
|
-
rendered
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// A component that dispatches `$action` itself reads no control, so each `$field` reference left in it becomes its fallback.
|
|
51
|
-
const withFieldFallbacks = (
|
|
52
|
-
context: GenerativeUIRenderContext,
|
|
53
|
-
): GenerativeUIRenderContext => {
|
|
54
|
-
const { dispatch } = context;
|
|
55
|
-
if (dispatch === undefined) return context;
|
|
56
|
-
return {
|
|
57
|
-
...context,
|
|
58
|
-
dispatch: (action) =>
|
|
59
|
-
dispatch(
|
|
60
|
-
hasFieldReference(action)
|
|
61
|
-
? (resolveFieldReferences(action, {}) as Action)
|
|
62
|
-
: action,
|
|
63
|
-
),
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
function renderNode(
|
|
68
|
-
node: NormalizedUINode,
|
|
69
|
-
library: GenerativeUILibrary,
|
|
70
|
-
context: GenerativeUIRenderContext,
|
|
71
|
-
): ReactNode {
|
|
72
|
-
if (node == null || typeof node === "boolean") return null;
|
|
73
|
-
if (typeof node === "string" || typeof node === "number") return node;
|
|
74
|
-
if (Array.isArray(node)) {
|
|
75
|
-
// Use a model-provided stable key when present. Otherwise, keep the
|
|
76
|
-
// positional fallback: pairing the index with the node's kind means that
|
|
77
|
-
// when the kind at an index changes, React remounts instead of handing a
|
|
78
|
-
// streaming node's hook state to a different component.
|
|
79
|
-
return node.map((child, index) => (
|
|
80
|
-
<Fragment key={nodeKey(child, index)}>
|
|
81
|
-
{renderNode(child, library, context)}
|
|
82
|
-
</Fragment>
|
|
83
|
-
));
|
|
84
|
-
}
|
|
85
|
-
return isElement(node) ? renderElement(node, library, context) : null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function renderElement(
|
|
89
|
-
element: NormalizedUIElement,
|
|
90
|
-
library: GenerativeUILibrary,
|
|
91
|
-
context: GenerativeUIRenderContext,
|
|
92
|
-
): ReactNode {
|
|
93
|
-
const entry = Object.hasOwn(library, element.type)
|
|
94
|
-
? library[element.type]
|
|
95
|
-
: undefined;
|
|
96
|
-
if (!entry) {
|
|
97
|
-
reportUnknownComponent(element.type, Object.keys(library));
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Components that opt out of prop streaming wait until their props are
|
|
102
|
-
// complete rather than rendering from a partial parse.
|
|
103
|
-
if (!entry.streamProperties && context.status === "streaming") return null;
|
|
104
|
-
|
|
105
|
-
// `children` is a reserved top-level key on the normalized element, not a
|
|
106
|
-
// prop, so it is not in `props`. Inject the framework props last so the model
|
|
107
|
-
// can never override them. `$action` is likewise reserved and stripped from
|
|
108
|
-
// the prop bag during normalization, so it is re-injected here for components
|
|
109
|
-
// that carry behavior (e.g. `Button`).
|
|
110
|
-
const props: Record<string, unknown> = {
|
|
111
|
-
...element.props,
|
|
112
|
-
$status: context.status,
|
|
113
|
-
};
|
|
114
|
-
if (context.dispatch !== undefined) {
|
|
115
|
-
props["$dispatch"] = context.dispatch;
|
|
116
|
-
}
|
|
117
|
-
if (element.action !== undefined) {
|
|
118
|
-
props["$action"] = element.action;
|
|
119
|
-
}
|
|
120
|
-
if (element.children !== undefined) {
|
|
121
|
-
props["children"] = renderNode(element.children, library, context);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return <GenerativeUIComponentRenderer render={entry.render} props={props} />;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Mounts a single node's `render` on its own fiber so the function may use
|
|
129
|
-
* hooks and hold state independently of its siblings and parent.
|
|
130
|
-
*/
|
|
131
|
-
function GenerativeUIComponentRenderer({
|
|
132
|
-
render,
|
|
133
|
-
props,
|
|
134
|
-
}: {
|
|
135
|
-
render: (props: any) => ReactNode;
|
|
136
|
-
props: Record<string, unknown>;
|
|
137
|
-
}): ReactNode {
|
|
138
|
-
return render(props);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** A coarse kind tag for a child, used in its list key so a node changing kind
|
|
142
|
-
* at a given index forces a remount rather than a wrong-fiber reuse. */
|
|
143
|
-
function nodeKind(node: NormalizedUINode): string {
|
|
144
|
-
if (node == null || typeof node === "boolean") return "";
|
|
145
|
-
if (typeof node === "string" || typeof node === "number") return "#text";
|
|
146
|
-
if (Array.isArray(node)) return "#array";
|
|
147
|
-
return isElement(node) ? node.type : "";
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function nodeKey(node: NormalizedUINode, index: number): string {
|
|
151
|
-
return isElement(node) &&
|
|
152
|
-
(typeof node.key === "string" || typeof node.key === "number")
|
|
153
|
-
? `model:${node.key}`
|
|
154
|
-
: `${index}:${nodeKind(node)}`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function reportUnknownComponent(type: string, available: string[]): void {
|
|
158
|
-
if (process.env["NODE_ENV"] !== "production") {
|
|
159
|
-
// eslint-disable-next-line no-console
|
|
160
|
-
console.error(
|
|
161
|
-
`[@assistant-ui/react-generative-ui] Unknown component "${type}". ` +
|
|
162
|
-
`Available components: ${available.join(", ") || "(none)"}.`,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
}
|
package/src/scopeSchema.test.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type { JSONSchema7 } from "json-schema";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { scopeSchema } from "./scopeSchema";
|
|
4
|
-
|
|
5
|
-
const ref = { $ref: "#/definitions/a~1b~0c" };
|
|
6
|
-
const scopedRef = { $ref: "#/$defs/component0/definitions/a~1b~0c" };
|
|
7
|
-
const scope = (schema: JSONSchema7) =>
|
|
8
|
-
scopeSchema(schema, "#/$defs/component0");
|
|
9
|
-
|
|
10
|
-
describe("scopeSchema", () => {
|
|
11
|
-
it.each([
|
|
12
|
-
"properties",
|
|
13
|
-
"patternProperties",
|
|
14
|
-
"definitions",
|
|
15
|
-
"$defs",
|
|
16
|
-
"dependentSchemas",
|
|
17
|
-
])("rebases schemas inside %s without changing the input", (keyword) => {
|
|
18
|
-
const original = {
|
|
19
|
-
[keyword]: { value: ref, allowed: true, forbidden: false },
|
|
20
|
-
};
|
|
21
|
-
const result = scope(original);
|
|
22
|
-
expect(result.schema).toEqual({
|
|
23
|
-
[keyword]: { value: scopedRef, allowed: true, forbidden: false },
|
|
24
|
-
});
|
|
25
|
-
expect(result.referenced).toBe(true);
|
|
26
|
-
expect(original).toEqual({
|
|
27
|
-
[keyword]: { value: ref, allowed: true, forbidden: false },
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
it.each(["allOf", "anyOf", "oneOf", "items", "prefixItems"])(
|
|
31
|
-
"rebases schemas in %s arrays",
|
|
32
|
-
(keyword) => {
|
|
33
|
-
expect(scope({ [keyword]: [ref, true, false] }).schema).toEqual({
|
|
34
|
-
[keyword]: [scopedRef, true, false],
|
|
35
|
-
});
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
it.each([
|
|
39
|
-
"items",
|
|
40
|
-
"additionalProperties",
|
|
41
|
-
"additionalItems",
|
|
42
|
-
"unevaluatedProperties",
|
|
43
|
-
"unevaluatedItems",
|
|
44
|
-
"contentSchema",
|
|
45
|
-
"contains",
|
|
46
|
-
"propertyNames",
|
|
47
|
-
"not",
|
|
48
|
-
"if",
|
|
49
|
-
"then",
|
|
50
|
-
"else",
|
|
51
|
-
])("rebases the %s subschema", (keyword) => {
|
|
52
|
-
expect(scope({ [keyword]: ref }).schema).toEqual({ [keyword]: scopedRef });
|
|
53
|
-
});
|
|
54
|
-
it("preserves property dependencies while rebasing schema dependencies", () => {
|
|
55
|
-
expect(
|
|
56
|
-
scope({ dependencies: { first: ["second"], other: ref } }).schema,
|
|
57
|
-
).toEqual({ dependencies: { first: ["second"], other: scopedRef } });
|
|
58
|
-
});
|
|
59
|
-
it("leaves literal data, external references, and named anchors unchanged", () => {
|
|
60
|
-
const schema: JSONSchema7 = {
|
|
61
|
-
default: ref,
|
|
62
|
-
examples: [ref],
|
|
63
|
-
enum: [ref],
|
|
64
|
-
const: ref,
|
|
65
|
-
properties: {
|
|
66
|
-
remote: { $ref: "https://example.com/schema.json#/definitions/value" },
|
|
67
|
-
anchor: { $ref: "#value" },
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
expect(scope(schema)).toEqual({ schema, referenced: false });
|
|
71
|
-
});
|
|
72
|
-
});
|
package/src/scopeSchema.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
|
|
2
|
-
|
|
3
|
-
// The converter does not pin a JSON Schema draft, so the 2020-12 applicators
|
|
4
|
-
// missing from the JSONSchema7 typings must be traversed as well.
|
|
5
|
-
type TraversableSchema = JSONSchema7 & {
|
|
6
|
-
prefixItems?: JSONSchema7Definition[];
|
|
7
|
-
dependentSchemas?: { [key: string]: JSONSchema7Definition };
|
|
8
|
-
unevaluatedItems?: JSONSchema7Definition;
|
|
9
|
-
unevaluatedProperties?: JSONSchema7Definition;
|
|
10
|
-
contentSchema?: JSONSchema7Definition;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function scopeSchema(schema: JSONSchema7, path: string) {
|
|
14
|
-
let referenced = false;
|
|
15
|
-
const visit = (definition: JSONSchema7Definition): JSONSchema7Definition => {
|
|
16
|
-
if (typeof definition === "boolean") return definition;
|
|
17
|
-
const schema = definition as TraversableSchema;
|
|
18
|
-
const result = { ...schema };
|
|
19
|
-
if (schema.$ref === "#" || schema.$ref?.startsWith("#/")) {
|
|
20
|
-
referenced = true;
|
|
21
|
-
result.$ref = path + schema.$ref.slice(1);
|
|
22
|
-
}
|
|
23
|
-
// Only schema-bearing keywords are traversed; defaults and examples are data.
|
|
24
|
-
for (const key of [
|
|
25
|
-
"properties",
|
|
26
|
-
"patternProperties",
|
|
27
|
-
"definitions",
|
|
28
|
-
"$defs",
|
|
29
|
-
"dependentSchemas",
|
|
30
|
-
] as const) {
|
|
31
|
-
const entries = schema[key];
|
|
32
|
-
if (entries)
|
|
33
|
-
result[key] = Object.fromEntries(
|
|
34
|
-
Object.entries(entries).map(([name, value]) => [name, visit(value)]),
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"] as const) {
|
|
38
|
-
if (schema[key]) result[key] = schema[key].map(visit);
|
|
39
|
-
}
|
|
40
|
-
for (const key of [
|
|
41
|
-
"additionalProperties",
|
|
42
|
-
"additionalItems",
|
|
43
|
-
"unevaluatedProperties",
|
|
44
|
-
"unevaluatedItems",
|
|
45
|
-
"contentSchema",
|
|
46
|
-
"contains",
|
|
47
|
-
"propertyNames",
|
|
48
|
-
"not",
|
|
49
|
-
"if",
|
|
50
|
-
"then",
|
|
51
|
-
"else",
|
|
52
|
-
] as const) {
|
|
53
|
-
const value = schema[key];
|
|
54
|
-
if (value !== undefined) result[key] = visit(value);
|
|
55
|
-
}
|
|
56
|
-
if (schema.items !== undefined)
|
|
57
|
-
result.items = Array.isArray(schema.items)
|
|
58
|
-
? schema.items.map(visit)
|
|
59
|
-
: visit(schema.items);
|
|
60
|
-
if (schema.dependencies)
|
|
61
|
-
result.dependencies = Object.fromEntries(
|
|
62
|
-
Object.entries(schema.dependencies).map(([name, value]) => [
|
|
63
|
-
name,
|
|
64
|
-
Array.isArray(value) ? value : visit(value),
|
|
65
|
-
]),
|
|
66
|
-
);
|
|
67
|
-
return result;
|
|
68
|
-
};
|
|
69
|
-
const scoped = visit(schema) as JSONSchema7;
|
|
70
|
-
return { schema: scoped, referenced };
|
|
71
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { convertSurfaceToUISpec } from "../a2ui/convert";
|
|
3
|
-
import { decodeBlockAction } from "./decodeBlockAction";
|
|
4
|
-
import { toSlackBlocks } from "./toSlackBlocks";
|
|
5
|
-
|
|
6
|
-
const convertForm = () => {
|
|
7
|
-
const { spec, warnings: a2uiWarnings } = convertSurfaceToUISpec({
|
|
8
|
-
components: new Map([
|
|
9
|
-
["root", { id: "root", component: "Column", children: ["name", "send"] }],
|
|
10
|
-
[
|
|
11
|
-
"name",
|
|
12
|
-
{
|
|
13
|
-
id: "name",
|
|
14
|
-
component: "TextField",
|
|
15
|
-
label: "Name",
|
|
16
|
-
value: { path: "/form/name" },
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
[
|
|
20
|
-
"send",
|
|
21
|
-
{
|
|
22
|
-
id: "send",
|
|
23
|
-
component: "Button",
|
|
24
|
-
label: "Send",
|
|
25
|
-
action: {
|
|
26
|
-
event: {
|
|
27
|
-
name: "send",
|
|
28
|
-
context: { name: { path: "/form/name" } },
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
]),
|
|
34
|
-
dataModel: { form: { name: "Ada" } },
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
expect(a2uiWarnings).toEqual([]);
|
|
38
|
-
expect(spec).toMatchObject({
|
|
39
|
-
children: [
|
|
40
|
-
{ $type: "Input", name: "/form/name", defaultValue: "Ada" },
|
|
41
|
-
{
|
|
42
|
-
$type: "Button",
|
|
43
|
-
$action: {
|
|
44
|
-
context: { name: { $field: "/form/name", fallback: "Ada" } },
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
});
|
|
49
|
-
if (!spec) throw new Error("Expected an A2UI tree");
|
|
50
|
-
|
|
51
|
-
const { blocks, warnings: slackWarnings } = toSlackBlocks(spec);
|
|
52
|
-
expect(slackWarnings).toEqual([]);
|
|
53
|
-
const input = blocks.find((block) => block.type === "input");
|
|
54
|
-
const actions = blocks.find((block) => block.type === "actions");
|
|
55
|
-
if (!input || input.type !== "input" || !input.block_id)
|
|
56
|
-
throw new Error("Expected a named Slack input block");
|
|
57
|
-
if (!actions || actions.type !== "actions")
|
|
58
|
-
throw new Error("Expected a Slack actions block");
|
|
59
|
-
const button = actions.elements[0];
|
|
60
|
-
if (!button || button.type !== "button")
|
|
61
|
-
throw new Error("Expected a Slack button");
|
|
62
|
-
|
|
63
|
-
return { blockId: input.block_id, actionId: input.element.action_id, button };
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
describe("A2UI actions on Slack", () => {
|
|
67
|
-
it("resolves a bound CheckBox as a boolean when a button is pressed", () => {
|
|
68
|
-
const { spec, warnings } = convertSurfaceToUISpec({
|
|
69
|
-
components: new Map([
|
|
70
|
-
[
|
|
71
|
-
"root",
|
|
72
|
-
{ id: "root", component: "Column", children: ["accepted", "send"] },
|
|
73
|
-
],
|
|
74
|
-
[
|
|
75
|
-
"accepted",
|
|
76
|
-
{
|
|
77
|
-
id: "accepted",
|
|
78
|
-
component: "CheckBox",
|
|
79
|
-
label: "Accepted",
|
|
80
|
-
value: { path: "/form/accepted" },
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
[
|
|
84
|
-
"send",
|
|
85
|
-
{
|
|
86
|
-
id: "send",
|
|
87
|
-
component: "Button",
|
|
88
|
-
label: "Send",
|
|
89
|
-
action: {
|
|
90
|
-
event: {
|
|
91
|
-
name: "send",
|
|
92
|
-
context: { accepted: { path: "/form/accepted" } },
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
]),
|
|
98
|
-
dataModel: { form: { accepted: true } },
|
|
99
|
-
});
|
|
100
|
-
expect(warnings).toEqual([]);
|
|
101
|
-
if (!spec) throw new Error("Expected an A2UI tree");
|
|
102
|
-
const { blocks, warnings: slackWarnings } = toSlackBlocks(spec);
|
|
103
|
-
expect(slackWarnings).toEqual([]);
|
|
104
|
-
const checkboxBlock = blocks.find(
|
|
105
|
-
(block) =>
|
|
106
|
-
block.type === "actions" &&
|
|
107
|
-
block.elements.some((element) => element.type === "checkboxes"),
|
|
108
|
-
);
|
|
109
|
-
const buttonBlock = blocks.find(
|
|
110
|
-
(block) =>
|
|
111
|
-
block.type === "actions" &&
|
|
112
|
-
block.elements.some((element) => element.type === "button"),
|
|
113
|
-
);
|
|
114
|
-
if (
|
|
115
|
-
!checkboxBlock ||
|
|
116
|
-
checkboxBlock.type !== "actions" ||
|
|
117
|
-
!checkboxBlock.block_id ||
|
|
118
|
-
!buttonBlock ||
|
|
119
|
-
buttonBlock.type !== "actions"
|
|
120
|
-
) {
|
|
121
|
-
throw new Error("Expected named checkbox and button blocks");
|
|
122
|
-
}
|
|
123
|
-
const checkbox = checkboxBlock.elements.find(
|
|
124
|
-
(element) => element.type === "checkboxes",
|
|
125
|
-
);
|
|
126
|
-
const button = buttonBlock.elements.find(
|
|
127
|
-
(element) => element.type === "button",
|
|
128
|
-
);
|
|
129
|
-
if (!checkbox || checkbox.type !== "checkboxes" || !button) {
|
|
130
|
-
throw new Error("Expected a checkbox and button");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
for (const [selectedOptions, accepted] of [
|
|
134
|
-
[[], false],
|
|
135
|
-
[checkbox.options[0] ? [checkbox.options[0]] : [], true],
|
|
136
|
-
] as const) {
|
|
137
|
-
expect(
|
|
138
|
-
decodeBlockAction(button, {
|
|
139
|
-
[checkboxBlock.block_id]: {
|
|
140
|
-
[checkbox.action_id]: {
|
|
141
|
-
type: "checkboxes",
|
|
142
|
-
selected_options: selectedOptions,
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
}),
|
|
146
|
-
).toMatchObject({ context: { accepted } });
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("decodes a named Checkbox action with a list in $input", () => {
|
|
151
|
-
const blockId = `aui:0:${JSON.stringify([["toggle", "agree", "Checkbox"]])}`;
|
|
152
|
-
for (const [selectedOptions, input] of [
|
|
153
|
-
[[], []],
|
|
154
|
-
[[{ value: "agree" }], ["agree"]],
|
|
155
|
-
] as const) {
|
|
156
|
-
expect(
|
|
157
|
-
decodeBlockAction({
|
|
158
|
-
type: "checkboxes",
|
|
159
|
-
block_id: blockId,
|
|
160
|
-
action_id: "toggle",
|
|
161
|
-
selected_options: selectedOptions,
|
|
162
|
-
}),
|
|
163
|
-
).toEqual({ type: "toggle", $input: input });
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("sends the user's edited TextField value", () => {
|
|
168
|
-
const { blockId, actionId, button } = convertForm();
|
|
169
|
-
const stateValues = {
|
|
170
|
-
[blockId]: {
|
|
171
|
-
[actionId]: {
|
|
172
|
-
type: "plain_text_input",
|
|
173
|
-
value: "Grace",
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
expect(decodeBlockAction(button, stateValues)).toEqual({
|
|
179
|
-
type: "a2ui:action",
|
|
180
|
-
name: "send",
|
|
181
|
-
surfaceId: "",
|
|
182
|
-
sourceComponentId: "send",
|
|
183
|
-
context: { name: "Grace" },
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("uses the agent's fallback when Slack provides no state", () => {
|
|
188
|
-
const { button } = convertForm();
|
|
189
|
-
|
|
190
|
-
expect(decodeBlockAction(button)).toEqual({
|
|
191
|
-
type: "a2ui:action",
|
|
192
|
-
name: "send",
|
|
193
|
-
surfaceId: "",
|
|
194
|
-
sourceComponentId: "send",
|
|
195
|
-
context: { name: "Ada" },
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
});
|
package/src/slack/constants.ts
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
SlackAlertBlock,
|
|
3
|
-
SlackButtonElement,
|
|
4
|
-
SlackCardBlock,
|
|
5
|
-
SlackCarouselBlock,
|
|
6
|
-
SlackContextBlock,
|
|
7
|
-
SlackDataTableBlock,
|
|
8
|
-
SlackDataTableCell,
|
|
9
|
-
SlackImageBlock,
|
|
10
|
-
SlackSectionBlock,
|
|
11
|
-
SlackTextObject,
|
|
12
|
-
} from "./types";
|
|
13
|
-
|
|
14
|
-
/** The block limit for a Slack message. */
|
|
15
|
-
export const MESSAGE_BLOCK_CAP = 50;
|
|
16
|
-
|
|
17
|
-
/** The block limit for a Slack modal. */
|
|
18
|
-
export const MODAL_BLOCK_CAP = 100;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The maximum number of top-level blocks accepted when parsing Slack Block
|
|
22
|
-
* Kit JSON back into generative UI, guarding against a hostile or malformed
|
|
23
|
-
* payload.
|
|
24
|
-
*/
|
|
25
|
-
export const INBOUND_BLOCK_CAP = 200;
|
|
26
|
-
|
|
27
|
-
/** The character limit for header text. */
|
|
28
|
-
export const HEADER_TEXT_CAP = 150;
|
|
29
|
-
|
|
30
|
-
/** The character limit for section text. */
|
|
31
|
-
export const SECTION_TEXT_CAP = 3000;
|
|
32
|
-
|
|
33
|
-
/** The maximum number of fields in one section. */
|
|
34
|
-
export const FACT_FIELD_CAP = 10;
|
|
35
|
-
|
|
36
|
-
/** The character limit for one section field. */
|
|
37
|
-
export const FACT_FIELD_TEXT_CAP = 2000;
|
|
38
|
-
|
|
39
|
-
/** The character limit for button labels and option labels. */
|
|
40
|
-
export const INTERACTIVE_TEXT_CAP = 75;
|
|
41
|
-
|
|
42
|
-
/** The character limit for an action_id. */
|
|
43
|
-
export const ACTION_ID_CAP = 255;
|
|
44
|
-
|
|
45
|
-
/** The character limit for a button's serialized value payload. */
|
|
46
|
-
export const BUTTON_VALUE_CAP = 2000;
|
|
47
|
-
|
|
48
|
-
/** The maximum number of elements in one actions block. */
|
|
49
|
-
export const ACTIONS_ELEMENT_CAP = 25;
|
|
50
|
-
|
|
51
|
-
/** The maximum number of options in a static select. */
|
|
52
|
-
export const SELECT_OPTION_CAP = 100;
|
|
53
|
-
|
|
54
|
-
/** The maximum number of options in a radio-button group. */
|
|
55
|
-
export const RADIO_OPTION_CAP = 10;
|
|
56
|
-
|
|
57
|
-
/** The maximum number of options in a checkbox group. */
|
|
58
|
-
export const CHECKBOX_OPTION_CAP = 10;
|
|
59
|
-
|
|
60
|
-
/** The character limit for a select or input placeholder. */
|
|
61
|
-
export const PLACEHOLDER_TEXT_CAP = 150;
|
|
62
|
-
|
|
63
|
-
/** The character limit for an input's label. */
|
|
64
|
-
export const INPUT_LABEL_CAP = 2000;
|
|
65
|
-
|
|
66
|
-
/** The maximum number of elements in one context block. */
|
|
67
|
-
export const CONTEXT_ELEMENT_CAP = 10;
|
|
68
|
-
|
|
69
|
-
/** The character limit for one context element's text. */
|
|
70
|
-
export const CONTEXT_TEXT_CAP = 3000;
|
|
71
|
-
|
|
72
|
-
/** The character limit for card titles and subtitles. */
|
|
73
|
-
export const CARD_TITLE_CAP = 150;
|
|
74
|
-
|
|
75
|
-
/** The character limit for a card's body text. */
|
|
76
|
-
export const CARD_BODY_CAP = 200;
|
|
77
|
-
|
|
78
|
-
/** The character limit for a card's subtext. */
|
|
79
|
-
export const CARD_SUBTEXT_CAP = 200;
|
|
80
|
-
|
|
81
|
-
/** The maximum number of buttons in a card's actions. */
|
|
82
|
-
export const CARD_ACTIONS_CAP = 3;
|
|
83
|
-
|
|
84
|
-
/** The minimum number of elements in a renderable carousel. */
|
|
85
|
-
export const CAROUSEL_CARD_MIN = 1;
|
|
86
|
-
|
|
87
|
-
/** The maximum number of elements in a carousel. */
|
|
88
|
-
export const CAROUSEL_CARD_CAP = 10;
|
|
89
|
-
|
|
90
|
-
/** The character limit for an alert's text. */
|
|
91
|
-
export const ALERT_TEXT_CAP = 200;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The maximum number of data rows in a data-table block; the emitted `rows`
|
|
95
|
-
* array additionally carries one header row.
|
|
96
|
-
*/
|
|
97
|
-
export const DATA_TABLE_ROW_CAP = 200;
|
|
98
|
-
|
|
99
|
-
/** The maximum number of columns in a data-table block. */
|
|
100
|
-
export const DATA_TABLE_COLUMN_CAP = 20;
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* The cumulative character limit (summed across every cell's text) shared by
|
|
104
|
-
* every data-table block in one payload.
|
|
105
|
-
*/
|
|
106
|
-
export const DATA_TABLE_CHAR_BUDGET = 20000;
|
|
107
|
-
|
|
108
|
-
/** The caption emitted on every native data-table block. */
|
|
109
|
-
export const TABLE_CAPTION = "Table";
|
|
110
|
-
|
|
111
|
-
/** The cumulative character limit for native markdown blocks. */
|
|
112
|
-
export const MARKDOWN_TEXT_BUDGET = 12000;
|
|
113
|
-
|
|
114
|
-
/** Builds the message-surface fallback for an alert. */
|
|
115
|
-
export function buildAlertFallback(
|
|
116
|
-
title: string,
|
|
117
|
-
description: string,
|
|
118
|
-
tone: "info" | "success" | "warning" | "danger",
|
|
119
|
-
): readonly [SlackContextBlock, SlackSectionBlock] {
|
|
120
|
-
const emoji = {
|
|
121
|
-
info: "ℹ️",
|
|
122
|
-
success: "✅",
|
|
123
|
-
warning: "⚠️",
|
|
124
|
-
danger: "🛑",
|
|
125
|
-
}[tone];
|
|
126
|
-
return [
|
|
127
|
-
{
|
|
128
|
-
type: "context",
|
|
129
|
-
elements: [
|
|
130
|
-
{ type: "mrkdwn", text: title ? `${emoji} *${title}*` : emoji },
|
|
131
|
-
],
|
|
132
|
-
},
|
|
133
|
-
{ type: "section", text: { type: "mrkdwn", text: description } },
|
|
134
|
-
];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Builds a 2026 Slack modal-only alert block, mapping `tone` to its `level`. */
|
|
138
|
-
export function buildAlertBlock(
|
|
139
|
-
text: string,
|
|
140
|
-
tone: "info" | "success" | "warning" | "danger",
|
|
141
|
-
): SlackAlertBlock {
|
|
142
|
-
const level = tone === "danger" ? "error" : tone;
|
|
143
|
-
return { type: "alert", text: { type: "mrkdwn", text }, level };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
type CardBlockFields = {
|
|
147
|
-
readonly heroImage?: SlackImageBlock;
|
|
148
|
-
readonly title?: SlackTextObject;
|
|
149
|
-
readonly subtitle?: SlackTextObject;
|
|
150
|
-
readonly body?: SlackTextObject;
|
|
151
|
-
readonly subtext?: SlackTextObject;
|
|
152
|
-
readonly actions?: readonly SlackButtonElement[];
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/** Builds a 2026 Slack card block from its already-clamped fields. */
|
|
156
|
-
export function buildCardBlock(fields: CardBlockFields): SlackCardBlock {
|
|
157
|
-
const { heroImage, title, subtitle, body, subtext, actions } = fields;
|
|
158
|
-
return {
|
|
159
|
-
type: "card",
|
|
160
|
-
...(heroImage !== undefined ? { hero_image: heroImage } : {}),
|
|
161
|
-
...(title !== undefined ? { title } : {}),
|
|
162
|
-
...(subtitle !== undefined ? { subtitle } : {}),
|
|
163
|
-
...(body !== undefined ? { body } : {}),
|
|
164
|
-
...(subtext !== undefined ? { subtext } : {}),
|
|
165
|
-
...(actions !== undefined ? { actions } : {}),
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Builds a 2026 Slack carousel block. */
|
|
170
|
-
export function buildCarouselBlock(
|
|
171
|
-
elements: readonly SlackCardBlock[],
|
|
172
|
-
): SlackCarouselBlock {
|
|
173
|
-
return { type: "carousel", elements };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Builds a 2026 Slack data-table block; `rows[0]` must be the header row. */
|
|
177
|
-
export function buildDataTableBlock(
|
|
178
|
-
rows: readonly (readonly SlackDataTableCell[])[],
|
|
179
|
-
): SlackDataTableBlock {
|
|
180
|
-
return { type: "data_table", caption: TABLE_CAPTION, rows };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Builds the final note used when a surface block budget is exceeded. */
|
|
184
|
-
export function buildBlockBudgetNote(omitted: number): SlackContextBlock {
|
|
185
|
-
return {
|
|
186
|
-
type: "context",
|
|
187
|
-
elements: [
|
|
188
|
-
{
|
|
189
|
-
type: "mrkdwn",
|
|
190
|
-
text: `${omitted} ${omitted === 1 ? "block was" : "blocks were"} omitted.`,
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
};
|
|
194
|
-
}
|